From 045e564dafae614cf2b5c25e92c0e632b8e6bdff Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 01:13:36 +0000 Subject: [PATCH] fix(demo): one-screen desktop layout, phone flow, square board, per-tab skills - Desktop: the app is exactly one screen tall; the tabs never shrink; the model card folds to one line once a key is stored (or a local model is loaded) and, with the agent settings, scrolls inside at most half the column, so the chat always has the rest; the workspace column scrolls on its own. - Phone: one column in the page scroll, ordered per tab (chess: board, then the chat; notes: the chat, then the board; MCP: servers, then the chat). - Chess board: eight equal rows as well as columns (a rank with pieces was taller than an empty one, so the board jumped), square, sized to fit the screen, pieces scaled from the board. - Skills belong to tabs: chess-coach to chess, board-style to notes, precise-answers to MCP; custom skills to every tab. Each agent gets only its tab's skills and the settings list only those. - Chess turns are one plan step without the replanner: fewer calls, and no second step that re-read the board, found White to move and "waited". Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01A7jHa5Pim4ufrdgxg561G5 --- demo/README.md | 4 +- demo/src/App.tsx | 61 +++++-- demo/src/app.css | 184 ++++++++++++++++----- demo/src/components/AgentSettingsPanel.tsx | 31 +++- demo/src/components/Settings.tsx | 29 +++- demo/src/settings.ts | 18 +- demo/src/skills.ts | 7 + 7 files changed, 254 insertions(+), 80 deletions(-) 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) => (