feat: per-model max output tokens and a conversation-time thinking level backed by agent settings (#28)
Co-authored-by: Alice <alice@prismshadow.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,10 @@
|
||||
* configured-key-first list with a bottom "show all" row — see ModelSelect) — once
|
||||
* the Session is created the model is locked, and the same spot switches to a read-only
|
||||
* logo + name display;
|
||||
* Draft state also renders a thinking-level picker left of the model selector (backed by the
|
||||
* Agent settings: picking a level writes through to the Agent config and applies to the session
|
||||
* created on first send); in session state the level is fixed (llmConfig is assembled once per
|
||||
* session), shown as a read-only tag from session_meta;
|
||||
* `/` opens the slash command menu (`/compact` compresses context, replacing the button; each
|
||||
* installed skill gets its own entry; pressing Enter on `/<skill_name>` toggles that skill's
|
||||
* selection without sending). Matching is positional like `@`: a slash opens the menu from any
|
||||
@@ -60,6 +64,7 @@ import { ProviderLogo } from "../../components/ui/provider-logo";
|
||||
import { hasConfiguredKey, sameModelRef, visibleChatModels } from "../models/model-grouping";
|
||||
import { filterAgents, matchMention, splitLeadingMention } from "./agent-mentions";
|
||||
import { matchSlash, removeSlashToken } from "./slash-token";
|
||||
import { THINKING_LEVELS, thinkingLevelLabel } from "./thinking-level";
|
||||
import {
|
||||
BOOK_ICON,
|
||||
buildSkillsMessage,
|
||||
@@ -379,6 +384,95 @@ function ModelSelect({
|
||||
);
|
||||
}
|
||||
|
||||
/** Spark glyph for the thinking-level picker (24x24 line path, consistent with the toolbar icon set). */
|
||||
const SPARK_ICON = "M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9L12 3z";
|
||||
|
||||
/**
|
||||
* Conversation-time thinking-level picker (draft state only, docked left of the model
|
||||
* selector): shows the **selected Agent's** current `model.thinking_level` and writes a picked
|
||||
* level straight through to the Agent settings — llmConfig is assembled once per session, so
|
||||
* the level applies to the session created on first send and becomes the Agent's new default
|
||||
* (switch-becomes-default). Per review: a title bar names the control, and the menu lists
|
||||
* exactly the five levels with short names only (no descriptions, no "default" row) — an
|
||||
* Agent without an explicit override shows an em dash until a level is picked.
|
||||
*/
|
||||
function ThinkingLevelSelect({
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
}: {
|
||||
/** Current level ("" = no override yet); null = the Agent config is still loading. */
|
||||
value: string | null;
|
||||
onChange: (level: string) => void;
|
||||
disabled: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const label =
|
||||
value === null ? "…" : (thinkingLevelLabel(S.chat.thinkingLevelNames, value) ?? "—");
|
||||
return (
|
||||
<Dropdown
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
menuClass="right-0 top-full mt-1 w-36 origin-top-right"
|
||||
button={
|
||||
<button
|
||||
type="button"
|
||||
title={`${S.chat.thinkingLevel}:${label}`}
|
||||
aria-label={S.chat.thinkingLevel}
|
||||
disabled={disabled || value === null}
|
||||
onClick={() => setOpen(!open)}
|
||||
className="flex h-8 max-w-36 shrink-0 items-center gap-1.5 rounded-md px-2 text-xs text-gray-500 transition-colors duration-150 hover:bg-gray-100 hover:text-gray-800 disabled:cursor-not-allowed disabled:opacity-50 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200"
|
||||
>
|
||||
<GlyphIcon d={SPARK_ICON} size={14} className="shrink-0" />
|
||||
{/* When the card is narrower than @md, only the icon remains (title shows the full state). */}
|
||||
<span className="hidden min-w-0 truncate @md:block">{label}</span>
|
||||
<svg
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
className="shrink-0"
|
||||
aria-hidden
|
||||
>
|
||||
<path
|
||||
d="M3 4.5l3 3 3-3"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
>
|
||||
{/* Title bar: names the control (the rows themselves are just the five short names). */}
|
||||
<div className="border-b border-gray-100 px-3 pb-1.5 pt-0.5 text-xs font-semibold text-gray-500 dark:border-gray-800 dark:text-gray-400">
|
||||
{S.chat.thinkingLevel}
|
||||
</div>
|
||||
{THINKING_LEVELS.map((level) => (
|
||||
<button
|
||||
key={level}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onChange(level);
|
||||
setOpen(false);
|
||||
}}
|
||||
className={`flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs transition-colors duration-150 hover:bg-gray-100 dark:hover:bg-gray-800 ${
|
||||
level === value
|
||||
? "font-medium text-gray-900 dark:text-gray-100"
|
||||
: "text-gray-600 dark:text-gray-400"
|
||||
}`}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{S.chat.thinkingLevelNames[level] ?? level}
|
||||
</span>
|
||||
<span className="w-3 shrink-0 text-center">{level === value ? "✓" : ""}</span>
|
||||
</button>
|
||||
))}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi-select skills dropdown (bottom toolbar, after approval mode): styled like the model
|
||||
* selector — button = book icon + "Skills" label + selected-count badge (no badge at 0; when the
|
||||
@@ -588,6 +682,9 @@ export function ChatInput({
|
||||
models,
|
||||
onChangeModel,
|
||||
defaultModel,
|
||||
thinkingLevel,
|
||||
onChangeThinkingLevel,
|
||||
sessionThinkingLevel,
|
||||
contextWindow,
|
||||
contextNow,
|
||||
contextStale = false,
|
||||
@@ -629,6 +726,24 @@ export function ChatInput({
|
||||
onChangeModel?: (ref: ModelRefDto) => void;
|
||||
/** Project default model (marked "default" on the selector's candidate item). */
|
||||
defaultModel?: ModelRefDto;
|
||||
/**
|
||||
* Draft state: the selected Agent's current thinking level ("" = no override / provider
|
||||
* default; null while the Agent config is loading — the picker renders disabled). Supplied
|
||||
* together with onChangeThinkingLevel; without the callback the picker isn't rendered.
|
||||
*/
|
||||
thinkingLevel?: string | null;
|
||||
/**
|
||||
* Draft state: writes the picked level straight through to the Agent settings (the parent
|
||||
* persists it via the agent-config API; the session created on first send picks it up and it
|
||||
* becomes the Agent's new default).
|
||||
*/
|
||||
onChangeThinkingLevel?: (level: string) => void;
|
||||
/**
|
||||
* Session state: the session's fixed thinking level (captured from session_meta on replay;
|
||||
* llmConfig is assembled once per session, so it cannot change mid-session). Rendered as a
|
||||
* read-only tag next to the locked model; null/undefined = unknown (nothing shown).
|
||||
*/
|
||||
sessionThinkingLevel?: string | null;
|
||||
/** Model's context window (from models config; when not configured, the ring's cap falls back to 128000 via resolveContextWindow). */
|
||||
contextWindow?: number;
|
||||
/** Current context usage (total of the most recent main-session Request). */
|
||||
@@ -1265,6 +1380,31 @@ export function ChatInput({
|
||||
{...(contextWindow !== undefined ? { window: contextWindow } : {})}
|
||||
/>
|
||||
)}
|
||||
{/* Draft state: conversation-time thinking level (backed by Agent settings), docked left of the model selector. */}
|
||||
{models && onChangeModel && onChangeThinkingLevel && (
|
||||
<ThinkingLevelSelect
|
||||
value={thinkingLevel ?? null}
|
||||
onChange={onChangeThinkingLevel}
|
||||
disabled={busy}
|
||||
/>
|
||||
)}
|
||||
{/* Session state: the session's fixed thinking level (from session_meta), read-only next
|
||||
to the locked model; hidden when it isn't one of the five levels (e.g. "default"). */}
|
||||
{!onChangeModel &&
|
||||
(() => {
|
||||
const label = thinkingLevelLabel(S.chat.thinkingLevelNames, sessionThinkingLevel);
|
||||
return (
|
||||
label && (
|
||||
<span
|
||||
title={`${S.chat.thinkingLevel}:${label}`}
|
||||
className="hidden h-8 shrink-0 items-center gap-1 px-1 text-xs text-gray-400 @md:flex dark:text-gray-500"
|
||||
>
|
||||
<GlyphIcon d={SPARK_ICON} size={13} className="shrink-0" />
|
||||
{label}
|
||||
</span>
|
||||
)
|
||||
);
|
||||
})()}
|
||||
{/* Left of the send button: model selector in draft state; once the Session is created the model is locked, shown read-only (still with the provider logo). */}
|
||||
{models && onChangeModel ? (
|
||||
<ModelSelect
|
||||
|
||||
Reference in New Issue
Block a user