diff --git a/demo/README.md b/demo/README.md index 5c7e20c..4f0382f 100644 --- a/demo/README.md +++ b/demo/README.md @@ -30,8 +30,8 @@ Three tabs, sharing one "Agent settings" panel: The **Agent settings** panel drives the core's features live: tool consent (⚡ autopilot / ask before changes / ask everything / read-only), thinking level, token / tool-call / step limits, context window and auto-compaction, -skills (built-in examples, your own, or imported `SKILL.md`), the chess -analysts — and the **chat UI**: tool calls, token usage, saved chats +skills (each tab lists its own built-in examples; your own or imported +`SKILL.md` ones apply to every tab), the chess analysts — and the **chat UI**: tool calls, token usage, saved chats (IndexedDB), the files panel, theme, and the labels (English / Russian — every string of the chat is a prop). diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 72befcd..b7fce8e 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -30,10 +30,12 @@ import { buildCloudModel, createLocalModel } from './providers' import { RU_LABELS } from './i18n' import { Markdown } from './markdown' import { + enabledSkillsFor, useDemoSettings, useSettingsConfig, type AnalystMode, type ThinkingChoice, + type View, } from './settings' import { pdfToMarkdown, workspace } from './workspace' @@ -50,7 +52,7 @@ The user's workspace is reachable with the fs_* tools (attachments are under /at const chessPrompt = ( analysts: AnalystMode, -) => `You play chess as Black against the user (White) on the board shown in this app. Each run starts right after the user's move. +) => `You play chess as Black against the user (White) on the board shown in this app. Each run starts right after the user's move and is ONE step: reply to that move. On your turn: 1) call get_position; 2) pick 2-4 candidate moves and ${ analysts === 'off' ? 'check them with evaluate_moves' @@ -59,7 +61,6 @@ On your turn: 1) call get_position; 2) pick 2-4 candidate moves and ${ Then reply with one or two friendly sentences: your idea and any threat. Never ask the user anything. If make_move fails, read its error (it lists the legal moves) and try again. If the game is over, say so and do not move.` -type View = 'notes' | 'mcp' | 'chess' const VIEWS: View[] = ['notes', 'mcp', 'chess'] /** @@ -131,12 +132,24 @@ export const App = () => { ) // The agents can read the attachments and write files into the workspace. const fileTools = useMemo(() => createFileTools(workspace), []) + // Each tab gets only its own skills (chess coaching has no place in an MCP + // chat); custom skills apply everywhere. + const skillsByTab = useMemo( + () => ({ + notes: enabledSkillsFor(settings, 'notes'), + mcp: enabledSkillsFor(settings, 'mcp'), + chess: enabledSkillsFor(settings, 'chess'), + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [settings.customSkills, settings.enabledSkills], + ) const base = (tab: View): Partial => ({ model: resolvedModel as ModelInput, credentials: credentials.store, memory: memories[tab], sessionId: tab, ...settingsConfig, + skills: skillsByTab[tab], // The consent mode is applied live by useAgent — no rebuild on a switch. toolApproval: { mode: settings.approvalMode }, // Stream the agent's internal phases to the console — handy for poking. @@ -154,7 +167,7 @@ export const App = () => { describeState: board.describeState, systemPrompt: NOTES_PROMPT, }, - { deps }, + { deps: [...deps, skillsByTab.notes] }, ) // ── Tab 2: any number of MCP servers (+ an optional mock catalogue) ──────── @@ -172,7 +185,7 @@ export const App = () => { tools: mcpTools, systemPrompt: MCP_PROMPT, }, - { deps: [...deps, mcpTools] }, + { deps: [...deps, mcpTools, skillsByTab.mcp] }, ) // ── Tab 3: chess against the agent, with analyst subagents ───────────────── @@ -234,8 +247,12 @@ export const App = () => { tools: { ...game.tools, ...analysts }, describeState: game.describeState, systemPrompt: chessPrompt(settings.analysts), + // A turn is one step: read, weigh, move. No second step to re-read the + // board (it then found White to move and "waited"), no replanner calls. + maxPlanSteps: 1, + replan: false, }, - { deps: [...deps, analysts] }, + { deps: [...deps, analysts, skillsByTab.chess] }, ) const chessRun = chessAgent.run @@ -355,19 +372,27 @@ export const App = () => {
- { - notesAgent.reload() - mcpAgent.reload() - chessAgent.reload() - }} - /> - +
+ { + notesAgent.reload() + mcpAgent.reload() + chessAgent.reload() + }} + ready={Boolean(resolvedModel)} + /> + +
{view === 'notes' && ( div { + min-width: 0; } .app__logo { - font-size: 34px; + font-size: 30px; line-height: 1; + flex: none; } .app__title { margin: 0; - font-size: 22px; + font-size: 20px; } .app__subtitle { margin: 2px 0 0; color: var(--muted); font-size: 13px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .app__links { display: flex; gap: 14px; font-weight: 500; + flex: none; } .app__main { flex: 1 1 auto; min-height: 0; display: grid; - grid-template-columns: minmax(360px, 540px) 1fr; + grid-template-columns: minmax(380px, 560px) minmax(0, 1fr); gap: 16px; } -@media (max-width: 820px) { - .app { - height: auto; - } - .app__main { - grid-template-columns: 1fr; - } - .app__chat { - height: 78vh; - min-height: 420px; - } - /* On a phone the board is the game: show it before the chat. */ - .app__main--chess .app__right { - order: -1; - } -} - .app__left { display: flex; flex-direction: column; - gap: 12px; + gap: 10px; min-height: 0; } +/* Settings fold to a line each; opened, they scroll inside at most half the + column, so the chat always keeps the rest. */ +.app__setup { + flex: 0 1 auto; + min-height: 0; + max-height: 50%; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 10px; +} .app__chat { - flex: 1 1 auto; - min-height: 460px; + flex: 1 1 0; + min-height: 320px; } .app__right { min-height: 0; + overflow-y: auto; +} +.app__footer { + flex: none; +} + +@media (max-width: 900px) { + .app { + height: auto; + min-height: 100dvh; + padding: 10px 12px; + } + .app__subtitle { + white-space: normal; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + } + .app__links { + display: none; + } + .app__main { + display: flex; + flex-direction: column; + } + /* The column's parts become items of the page flow, ordered per tab. */ + .app__left { + display: contents; + } + .app__setup { + order: 0; + max-height: none; + overflow: visible; + } + .app__right { + order: 1; + overflow: visible; + } + .app__chat { + order: 2; + flex: none; + height: 75dvh; + min-height: 420px; + } + /* Notes: you talk first and look at the board after. */ + .app__main--notes .app__right { + order: 3; + } } /* ── Settings panel ───────────────────────────────────────────────────────── */ @@ -127,10 +185,40 @@ a { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; - padding: 14px; + padding: 10px 14px; + flex: none; +} +.settings > * + * { + margin-top: 10px; +} +.settings__summary { display: flex; - flex-direction: column; - gap: 10px; + align-items: baseline; + gap: 8px; + cursor: pointer; + font-weight: 600; + min-width: 0; + list-style-position: inside; +} +.settings__summary-title { + flex: none; +} +.settings__summary-value { + font-weight: 400; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.settings__summary-status { + margin-left: auto; + flex: none; + font-weight: 500; + font-size: 12px; + color: #b45309; +} +.settings__summary-status.is-ok { + color: #16a34a; } .settings__field { display: flex; @@ -291,11 +379,14 @@ a { /* ── View tabs ────────────────────────────────────────────────────────────── */ .app__tabs { + /* Never squeezed by the content below; scrolls sideways on a narrow screen. */ + flex: none; display: flex; gap: 6px; - margin-bottom: 12px; overflow-x: auto; + overflow-y: hidden; scrollbar-width: none; + padding: 1px; } .app__tab { flex: none; @@ -568,16 +659,13 @@ a { /* ── Agent settings ───────────────────────────────────────────────────────── */ .agentset { + /* Block, not flex: a flex
keeps a gap for its hidden content. */ + flex: none; + display: block; background: var(--panel); border: 1px solid var(--border); border-radius: 12px; - padding: 12px 14px; - display: flex; - flex-direction: column; - gap: 10px; -} -.agentset[open] { - gap: 10px; + padding: 10px 14px; } .agentset > *:not(summary) { margin-top: 10px; @@ -702,9 +790,15 @@ a { .cboard { display: grid; - grid-template-columns: repeat(8, 1fr); - width: min(100%, 480px); + /* Eight equal rows as well as columns: without them a rank with pieces is + taller than an empty one and the board jumps as pieces move. */ + grid-template-columns: repeat(8, minmax(0, 1fr)); + grid-template-rows: repeat(8, minmax(0, 1fr)); + /* Square, as wide as the panel allows and short enough to fit the screen. */ + width: min(100%, 640px, calc(100dvh - 330px)); + min-width: 240px; aspect-ratio: 1; + container-type: inline-size; border-radius: 8px; overflow: hidden; border: 1px solid var(--border); @@ -712,6 +806,9 @@ a { } .cboard__sq { position: relative; + min-width: 0; + min-height: 0; + overflow: hidden; border: none; padding: 0; display: grid; @@ -752,7 +849,8 @@ a { box-shadow: inset 0 0 0 4px rgba(79, 70, 229, 0.55); } .cboard__piece { - font-size: clamp(22px, 5vw, 44px); + /* A cell is 12.5% of the board: size pieces from the board, not the window. */ + font-size: clamp(18px, 9cqi, 64px); line-height: 1; pointer-events: none; } diff --git a/demo/src/components/AgentSettingsPanel.tsx b/demo/src/components/AgentSettingsPanel.tsx index 2d3a5c1..865bb19 100644 --- a/demo/src/components/AgentSettingsPanel.tsx +++ b/demo/src/components/AgentSettingsPanel.tsx @@ -4,11 +4,19 @@ import { loadSkillFromUrl, parseSkillMarkdown, } from '@dudko.dev/agent-web-react' -import { allSkills, type DemoSettings, type ThinkingChoice } from '../settings' +import { + allSkills, + skillsOfView, + type DemoSettings, + type ThinkingChoice, + type View, +} from '../settings' import { BUILTIN_SKILLS, SKILL_TEMPLATE } from '../skills' export interface AgentSettingsPanelProps { settings: DemoSettings + /** The open tab: its skills are listed, and the notes speak about its tools. */ + view: View update: (patch: Partial) => void onReset: () => void } @@ -20,10 +28,19 @@ const k = (n: number) => (n >= 1_000_000 ? `${n / 1_000_000}M` : `${n / 1000}k`) /** * Everything the agent loop exposes, as settings: tool consent (the autopilot - * switch), thinking, run limits, context compaction and skills. Shared by all - * three tabs. + * switch), thinking, run limits, context compaction and skills. The loop + * settings are shared by all three tabs; the skills shown are the open tab's. */ -export const AgentSettingsPanel = ({ settings, update, onReset }: AgentSettingsPanelProps) => { +export const AgentSettingsPanel = ({ + settings, + view, + update, + onReset, +}: AgentSettingsPanelProps) => { + const skills = skillsOfView(settings, view) + const enabledHere = skills.filter((s) => settings.enabledSkills.includes(s.name)).length + const writeExample = + view === 'chess' ? 'a chess move' : view === 'mcp' ? 'a write to your MCP server' : 'a note' const [draft, setDraft] = useState('') const [skillUrl, setSkillUrl] = useState('') const [skillError, setSkillError] = useState() @@ -72,7 +89,7 @@ export const AgentSettingsPanel = ({ settings, update, onReset }: AgentSettingsP Agent settings{' '} {settings.approvalMode === 'autopilot' ? 'autopilot' : settings.approvalMode} · thinking{' '} - {settings.thinking} · {settings.enabledSkills.length} skill(s) + {settings.thinking} · {enabledHere} skill(s) @@ -86,7 +103,7 @@ export const AgentSettingsPanel = ({ settings, update, onReset }: AgentSettingsP {settings.approvalMode === 'autopilot' ? 'Autopilot: the agent runs every tool on its own.' : settings.approvalMode === 'ask-writes' - ? 'Read-only tools run freely; you approve anything that changes state (a note, a chess move, a write to your MCP server).' + ? `Read-only tools run freely; you approve anything that changes state (${writeExample}).` : settings.approvalMode === 'ask-all' ? 'You approve every single tool call.' : 'Read-only: changes are refused without asking — the agent can only look.'}{' '} @@ -244,7 +261,7 @@ export const AgentSettingsPanel = ({ settings, update, onReset }: AgentSettingsP
Skills
    - {allSkills(settings).map((s) => ( + {skills.map((s) => (