diff --git a/docs/changelog.json b/docs/changelog.json index 3f070e4..6d9f7f6 100644 --- a/docs/changelog.json +++ b/docs/changelog.json @@ -11,6 +11,12 @@ "title": "The agent menu shows the agents you use, with the rest under More agents", "summary": "The agent menu above the message box now lists your six most-used agents instead of every agent at once. More agents opens the full list with a search box; pin agents there to choose exactly which ones the menu shows, or go back to the most-used ones. Cloud agents stay under Cloud unless you pin them. On a phone, the agent sheet gains a search box. The choice is remembered per browser." }, + { + "date": "2026-10-13", + "area": "Chat", + "title": "The new chat page shows what Shell:B can do", + "summary": "Below the agent's starter cards, a new chat now has a What Shell:B can do section with three tabs. Try something offers everyday tasks (prepare for a job interview, polish a cover letter, identify a sea creature, step back into the Neolithic, get notes on your writing, research a topic, summarise a document, make a picture, get a quick answer); each one switches the chat to the agent best at it and sends. Agents lists every agent with what it is for: picking one sets it for the chat and its starter cards change to match. Apps lists every app with a line on what it does, and opens it." + }, { "date": "2026-10-13", "area": "Chat", diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 2199993..6ffca89 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -42,6 +42,7 @@ import { ContextMeter } from './components/composer/ContextMeter'; import { isCell, type GuestPanel } from './cell'; import { GuestPanelHost } from './components/CellGuest'; import { DocPanel } from './components/DocPanel'; +import { NewChatHome } from './components/NewChatHome'; import { onDocView, type DocTarget } from './docView'; const Studio = lazy(() => import('./studio/Studio')); @@ -473,6 +474,15 @@ export function App() { } }, [conv]); + // a "Try something" card on the empty chat: the chat switches to the card's agent, and the prompt goes once it has + const [pendingStart, setPendingStart] = useState<{ agentId: string; prompt: string } | null>(null); + const startWith = useCallback((a: Agent, prompt: string) => { selectAgent(a); setPendingStart({ agentId: a.id, prompt }); }, [selectAgent]); + useEffect(() => { + if (!pendingStart || agent?.id !== pendingStart.agentId) return; + setPendingStart(null); + send(pendingStart.prompt, []); + }, [pendingStart, agent, send]); + /** `incognito`: an incognito copy of the chat (server/privacy.py); the original is left as it was */ const forkChat = useCallback(async (convId: string, messageId?: string, incognito = false) => { try { @@ -891,23 +901,8 @@ export function App() {
{ const el = e.currentTarget; setAtBottom(el.scrollHeight - el.scrollTop - el.clientHeight < 80); }}> {messages.length === 0 ? ( -
- -

{greeting()}.

-

- {agent.name} · {agent.description} -

-
- {quick.map(q => ( - - ))} -
-

{t('Overthink. Overcomplicate. Overengineer.')}

-
+ send(prompt, [])} onStart={startWith} onSelectAgent={selectAgent} onOpenApp={openApp} /> ) : (
{foldSummarized(messages).map(m => ( diff --git a/ui/src/components/NewChatHome.tsx b/ui/src/components/NewChatHome.tsx new file mode 100644 index 0000000..8880a93 --- /dev/null +++ b/ui/src/components/NewChatHome.tsx @@ -0,0 +1,182 @@ +import { useRef, useState } from 'react'; +import clsx from 'clsx'; +import { t } from '../i18n'; +import type { Agent } from '../types'; +import type { Suggestion } from '../suggestions'; +import { APPS, GROUP_LABEL, type AppGroup, type AppId } from './apps'; +import { AgentAvatar } from './common'; + +/** + * The empty chat: the agent's own starter cards, then "What Shell:B can do" for people who don't know yet. Three tabs: + * Try something (everyday tasks, each answered by the agent best at it), Agents (who is who) and Apps (everything on the + * rail and in All apps). Everyone sees the same page. + */ + +interface Starter { title: string; blurb: string; prompt: string; agent: string } + +// Everyday tasks across the range of people who use Shell:B. General on purpose: this screen can be seen by anyone. +const STARTERS: Starter[] = [ + { agent: 'shellb', title: 'Prepare for a job interview', blurb: 'A practice run with feedback after each answer.', + prompt: "Run a mock job interview with me. First ask me about the role and the organisation, then ask one question at a time and give me honest feedback after each answer." }, + { agent: 'shellb', title: 'Polish a cover letter', blurb: 'Matched to the job ad, still in your voice.', + prompt: 'I will paste a cover letter and the job ad it is for. Tighten the letter, match it to what the ad asks for, and keep my voice.' }, + { agent: 'thalassa', title: 'Identify a sea creature', blurb: 'Species names checked against the World Register of Marine Species.', + prompt: 'Help me identify a marine animal. I will tell you where I saw it, its size, colour and shape. Give me the likely species with scientific names and how to tell them apart.' }, + { agent: 'mimir', title: 'Step back into the Neolithic', blurb: 'Ancient sites, skies and coastlines, worked out on this machine.', + prompt: 'When Newgrange was built, about 5,200 years ago, where did the midwinter sun rise as seen from it, and how different was the sea level then?' }, + { agent: 'shellb', title: 'Get notes on my writing', blurb: "An editor's read on pacing, voice and clarity.", + prompt: "I will paste a passage from something I am writing. Give me an editor's notes on pacing, voice and clarity, then suggest three lines I could cut." }, + { agent: 'minerva', title: 'Research a topic in depth', blurb: 'A report built from many sources, with citations.', + prompt: 'Research a topic in depth for me: search widely, read the primary sources and write a short report with citations. Ask me for the topic first.' }, + { agent: 'shellb', title: 'Summarise a long document', blurb: 'The key points, decisions and deadlines.', + prompt: 'I will attach a long document. Summarise it in one paragraph, then list the decisions, deadlines and open questions in it.' }, + { agent: 'nyx', title: 'Make a picture', blurb: 'Images, music and sound effects, made locally.', + prompt: 'Paint a watercolour of a quiet harbour at dawn: fishing boats, gulls and soft pastel light.' }, + { agent: 'spark', title: 'Get a quick answer', blurb: 'Fast replies for small questions.', + prompt: 'Convert 180 °C to Fahrenheit, and tell me which gas mark that is.' }, +]; + +// each grid numbers its own cards in themes that number them (synthfield's 01, 02…) +const NUMBERED = { counterReset: 'sf-mission' }; + +// opt-in agents stay in the agent menu but are never advertised on a screen others can see +const NOT_SHOWCASED = new Set(['aphrodite']); +const APP_GROUPS: AppGroup[] = ['make', 'know', 'run']; + +type Tab = 'try' | 'agents' | 'apps'; + +const card = 'sf-card rounded-xl border border-[var(--border-subtle)] bg-[var(--bg-secondary)] hover:bg-[var(--bg-hover)] hover:border-[var(--border-strong)] transition group text-left'; + +export function NewChatHome({ agent, agents, quick, greeting, onSend, onStart, onSelectAgent, onOpenApp }: { + agent: Agent; + agents: Agent[]; + quick: Suggestion[]; + greeting: string; + /** a starter card for the agent on screen */ + onSend: (prompt: string) => void; + /** a "Try something" card: switch the new chat to that agent, then send */ + onStart: (agent: Agent, prompt: string) => void; + onSelectAgent: (agent: Agent) => void; + onOpenApp: (app: AppId) => void; +}) { + const [tab, setTab] = useState('try'); + const top = useRef(null); + + const showcased = agents.filter(a => !a.unavailable && !NOT_SHOWCASED.has(a.id)); + const byId = new Map(showcased.map(a => [a.id, a])); + const starters = STARTERS.flatMap(s => { const a = byId.get(s.agent); return a ? [{ ...s, a }] : []; }); + const local = showcased.filter(a => !a.cloud), cloud = showcased.filter(a => a.cloud); + + const tabs: { id: Tab; label: string }[] = [ + { id: 'try', label: t('Try something') }, { id: 'agents', label: t('Agents') }, { id: 'apps', label: t('Apps') }, + ]; + + const agentCard = (a: Agent) => ( + + ); + + return ( +
+ +

{greeting}.

+

+ {agent.name} · {agent.description} +

+
+ {quick.map(q => ( + + ))} +
+ +
+
+

{t('What Shell:B can do')}

+
+ {tabs.map(x => ( + + ))} +
+
+ + {tab === 'try' && ( +
+ {starters.map(s => ( + + ))} +
+ )} + + {tab === 'agents' && ( +
+

+ {t('Each agent is good at something different. Pick one for this chat, or type @ and a name to ask one for a single reply.')} +

+
{local.map(agentCard)}
+ {cloud.length > 0 && (<> +

{t('Cloud')}

+
{cloud.map(agentCard)}
+ )} + +
+ )} + + {tab === 'apps' && ( +
+ {APP_GROUPS.map(g => { + const apps = APPS.filter(a => a.group === g); + if (!apps.length) return null; + return ( +
+

{t(GROUP_LABEL[g])}

+
+ {apps.map(a => { + const Icon = a.icon; + return ( + + ); + })} +
+
+ ); + })} +
+ )} +
+ +

{t('Overthink. Overcomplicate. Overengineer.')}

+
+ ); +} diff --git a/ui/src/i18n/fr.json b/ui/src/i18n/fr.json index 37d491f..8b3db6f 100644 --- a/ui/src/i18n/fr.json +++ b/ui/src/i18n/fr.json @@ -4053,5 +4053,46 @@ "Each agent is a set of instructions that runs on your own model, here on this machine. Installing one adds it to your agent menu; you can change or delete it any time.": "Chaque agent est un ensemble d’instructions qui tourne sur votre propre modèle, ici sur cette machine. L’installer l’ajoute à votre menu d’agents ; vous pouvez le modifier ou le supprimer à tout moment.", "No agents published yet. Make one in the Agent Lab and share it on GitHub.": "Aucun agent publié pour l’instant. Créez-en un dans le Labo des agents et partagez-le sur GitHub.", "Can use: {tools}": "Peut utiliser : {tools}", - "{name} is in your agent menu.": "{name} est dans votre menu d’agents." + "{name} is in your agent menu.": "{name} est dans votre menu d’agents.", + "Try something": "Essayer", + "What Shell:B can do": "Ce que Shell:B sait faire", + "Chat with {name}": "Discuter avec {name}", + "Each agent is good at something different. Pick one for this chat, or type @ and a name to ask one for a single reply.": "Chaque agent a son domaine. Choisissez-en un pour cette discussion, ou tapez @ suivi d’un nom pour lui demander une seule réponse.", + "Create or tune agents in the Agent Lab": "Créer ou ajuster des agents dans le Labo des agents", + "Prepare for a job interview": "Préparer un entretien d’embauche", + "A practice run with feedback after each answer.": "Un entraînement, avec un retour après chaque réponse.", + "Run a mock job interview with me. First ask me about the role and the organisation, then ask one question at a time and give me honest feedback after each answer.": "Fais-moi passer un entretien d’embauche fictif. Demande-moi d’abord le poste et l’organisation, puis pose une question à la fois et donne-moi un retour honnête après chaque réponse.", + "Polish a cover letter": "Peaufiner une lettre de motivation", + "Matched to the job ad, still in your voice.": "Adaptée à l’annonce, toujours avec votre voix.", + "I will paste a cover letter and the job ad it is for. Tighten the letter, match it to what the ad asks for, and keep my voice.": "Je vais coller une lettre de motivation et l’annonce à laquelle elle répond. Resserre la lettre, adapte-la à ce que demande l’annonce et garde ma voix.", + "Identify a sea creature": "Identifier un animal marin", + "Species names checked against the World Register of Marine Species.": "Noms d’espèces vérifiés dans le Registre mondial des espèces marines.", + "Help me identify a marine animal. I will tell you where I saw it, its size, colour and shape. Give me the likely species with scientific names and how to tell them apart.": "Aide-moi à identifier un animal marin. Je vais te dire où je l’ai vu, sa taille, sa couleur et sa forme. Donne-moi les espèces probables avec leur nom scientifique et comment les distinguer.", + "Step back into the Neolithic": "Remonter au Néolithique", + "Ancient sites, skies and coastlines, worked out on this machine.": "Sites anciens, ciels et rivages d’autrefois, calculés sur cette machine.", + "When Newgrange was built, about 5,200 years ago, where did the midwinter sun rise as seen from it, and how different was the sea level then?": "Quand Newgrange a été construit, il y a environ 5 200 ans, où se levait le soleil du solstice d’hiver vu du monument, et quelle était alors la différence de niveau de la mer ?", + "Get notes on my writing": "Avoir un avis sur mon texte", + "An editor's read on pacing, voice and clarity.": "Une lecture d’éditeur : rythme, voix et clarté.", + "I will paste a passage from something I am writing. Give me an editor's notes on pacing, voice and clarity, then suggest three lines I could cut.": "Je vais coller un passage de ce que j’écris. Donne-moi des notes d’éditeur sur le rythme, la voix et la clarté, puis propose trois lignes que je pourrais couper.", + "Research a topic in depth": "Approfondir un sujet", + "A report built from many sources, with citations.": "Un rapport tiré de nombreuses sources, avec citations.", + "Research a topic in depth for me: search widely, read the primary sources and write a short report with citations. Ask me for the topic first.": "Fais une recherche approfondie pour moi : cherche largement, lis les sources primaires et rédige un court rapport avec citations. Demande-moi d’abord le sujet.", + "Summarise a long document": "Résumer un long document", + "The key points, decisions and deadlines.": "Les points clés, les décisions et les échéances.", + "I will attach a long document. Summarise it in one paragraph, then list the decisions, deadlines and open questions in it.": "Je vais joindre un long document. Résume-le en un paragraphe, puis liste les décisions, les échéances et les questions ouvertes qu’il contient.", + "Make a picture": "Créer une image", + "Images, music and sound effects, made locally.": "Images, musique et effets sonores, créés en local.", + "Paint a watercolour of a quiet harbour at dawn: fishing boats, gulls and soft pastel light.": "Peins une aquarelle d’un port paisible à l’aube : bateaux de pêche, mouettes et douce lumière pastel.", + "Get a quick answer": "Obtenir une réponse rapide", + "Fast replies for small questions.": "Des réponses rapides aux petites questions.", + "Convert 180 °C to Fahrenheit, and tell me which gas mark that is.": "Convertis 180 °C en Fahrenheit et dis-moi à quel thermostat cela correspond.", + "Engineering": "Ingénierie", + "Research & Analysis": "Recherche et analyse", + "Vision": "Vision", + "Generative Art": "Art génératif", + "Quick Answers": "Réponses rapides", + "Asset Scout": "Éclaireur de ressources", + "Worlds & Cartography": "Mondes et cartographie", + "Marine Biology": "Biologie marine", + "Studio Architect": "Architecte du Studio" } diff --git a/ui/src/mobile/MobileApp.tsx b/ui/src/mobile/MobileApp.tsx index 1bbbb65..f39e16d 100644 --- a/ui/src/mobile/MobileApp.tsx +++ b/ui/src/mobile/MobileApp.tsx @@ -59,6 +59,7 @@ import { CircleShareHost } from '../components/CircleCards'; import { MobileAppsSheet } from './MobileAppsSheet'; import { MobileArtifactSheet } from './MobileArtifactSheet'; import { DocPanel } from '../components/DocPanel'; +import { NewChatHome } from '../components/NewChatHome'; import { onDocView, type DocTarget } from '../docView'; import { MobileAgentPicker } from './MobileAgentPicker'; import { MobileRadioBar } from './MobileRadioBar'; @@ -511,6 +512,21 @@ export function MobileApp() { [conv, setConv] ); + // a "Try something" card on the empty chat: the chat switches to the card's agent, and the prompt goes once it has + const [pendingStart, setPendingStart] = useState<{ agentId: string; prompt: string } | null>(null); + const startWith = useCallback( + (a: Agent, prompt: string) => { + selectAgent(a); + setPendingStart({ agentId: a.id, prompt }); + }, + [selectAgent] + ); + useEffect(() => { + if (!pendingStart || agent?.id !== pendingStart.agentId) return; + setPendingStart(null); + send(pendingStart.prompt, []); + }, [pendingStart, agent, send]); + const moveToGroup = useCallback( async (convId: string, gid: string | null) => { await api.patchConversation(convId, { groupId: gid }); @@ -915,38 +931,16 @@ export function MobileApp() { }} > {messages.length === 0 ? ( -
- -

- {greeting()}. -

-

- {agent.name} ·{' '} - {agent.description} -

- - {/* Suggestion Chips */} -
- {quick.map(q => ( - - ))} -
- -

- {t('Overthink. Overcomplicate. Overengineer.')} -

-
+ send(prompt, [])} + onStart={startWith} + onSelectAgent={selectAgent} + onOpenApp={openApp} + /> ) : (
{foldSummarized(messages).map(m => ( diff --git a/ui/src/themes/synthfield.css b/ui/src/themes/synthfield.css index 8159218..5aaa08c 100644 --- a/ui/src/themes/synthfield.css +++ b/ui/src/themes/synthfield.css @@ -244,7 +244,7 @@ html[data-skin='synthfield'] .sf-card { transition: border-color .15s, transform .15s, background-color .15s; } html[data-skin='synthfield'] .sf-card::before { - content: '0' counter(sf-mission); position: absolute; top: 14px; right: 16px; + content: counter(sf-mission, decimal-leading-zero); position: absolute; top: 14px; right: 16px; font-family: 'JetBrains Mono Variable', monospace; font-size: 10px; letter-spacing: 0.1em; color: var(--text-muted); } html[data-skin='synthfield'] .sf-card::after {