WebSearch renders favicon'd link chips + count + collapsible analysis; WebFetch a favicon/host card; ToolSearch matched-tool chips (backend now flattens tool_reference blocks); Artifact a published-page link card. New 'Web & search widgets' visibility switch gates the search-flavoured ones. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
204 lines
6.2 KiB
TypeScript
204 lines
6.2 KiB
TypeScript
import { useMemo } from "react";
|
||
import { create } from "zustand";
|
||
import { createJSONStorage, persist } from "zustand/middleware";
|
||
import { serverStorage } from "./serverStorage";
|
||
|
||
/**
|
||
* What the conversation page renders. Every switch is ON by default; the store
|
||
* only records what the user has *hidden*, so a new toggle added later starts
|
||
* visible for everyone without a migration.
|
||
*
|
||
* The state is persisted server-side (like Settings), so the reading setup —
|
||
* "no costs, no timestamps, no animations" — follows the user across devices.
|
||
*/
|
||
export type VisKey =
|
||
// metadata carried by a message / tool call
|
||
| "cost"
|
||
| "tokens"
|
||
| "duration"
|
||
| "timestamp"
|
||
| "model"
|
||
| "convModel"
|
||
| "convEffort"
|
||
| "toolId"
|
||
| "resultSize"
|
||
| "diffStat"
|
||
| "contextSplit"
|
||
| "footprint"
|
||
// content in the thread
|
||
| "thinking"
|
||
| "toolCalls"
|
||
| "bashCalls"
|
||
| "toolOutput"
|
||
| "widgets"
|
||
| "searchWidgets"
|
||
| "forms"
|
||
| "errors"
|
||
| "forkBanner"
|
||
| "msgNav"
|
||
// chrome floating over the thread
|
||
| "taskPanel"
|
||
| "fastForward"
|
||
| "deployGizmo"
|
||
| "contextGizmo"
|
||
| "costCurve"
|
||
| "animations"
|
||
| "graphPhysics"
|
||
| "avatar"
|
||
| "avatarDrag"
|
||
| "railAvatars";
|
||
|
||
export interface VisItem {
|
||
key: VisKey;
|
||
label: string;
|
||
hint: string;
|
||
}
|
||
|
||
export interface VisGroup {
|
||
title: string;
|
||
items: VisItem[];
|
||
}
|
||
|
||
export const VIS_GROUPS: VisGroup[] = [
|
||
{
|
||
title: "Metadata",
|
||
items: [
|
||
{ key: "cost", label: "Price", hint: "per-turn dollar cost" },
|
||
{ key: "tokens", label: "Tokens", hint: "per-turn token count" },
|
||
{ key: "duration", label: "Duration", hint: "how long a turn or tool call took" },
|
||
{ key: "timestamp", label: "Timestamp", hint: "date + time on each message" },
|
||
{ key: "model", label: "Model", hint: "the model that produced the turn" },
|
||
{
|
||
key: "convModel",
|
||
label: "Model tags",
|
||
hint: "the model(s) this conversation ran on, in the header",
|
||
},
|
||
{
|
||
key: "convEffort",
|
||
label: "Effort tags",
|
||
hint: "the --effort level(s) this conversation's turns ran at",
|
||
},
|
||
{ key: "toolId", label: "Tool ID", hint: "the tool_use id of a call" },
|
||
{ key: "resultSize", label: "Result size", hint: "lines of output a tool returned" },
|
||
{ key: "diffStat", label: "Diff stat", hint: "+N −M on file edits" },
|
||
{
|
||
key: "contextSplit",
|
||
label: "Context split",
|
||
hint: "context vs message tokens on your first prompt",
|
||
},
|
||
{
|
||
key: "footprint",
|
||
label: "Footprint",
|
||
hint: "estimated carbon, water & energy of the conversation",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
title: "Content",
|
||
items: [
|
||
{ key: "thinking", label: "Thinking blocks", hint: "the model's reasoning" },
|
||
{ key: "toolCalls", label: "Tool calls", hint: "every tool card in the thread" },
|
||
{ key: "bashCalls", label: "Run-command calls", hint: "Bash / shell tool cards" },
|
||
{ key: "toolOutput", label: "Tool output", hint: "the result body inside a card" },
|
||
{ key: "widgets", label: "Rich widgets", hint: "screenshots, images, videos, plans, notifications" },
|
||
{
|
||
key: "searchWidgets",
|
||
label: "Web & search widgets",
|
||
hint: "web-search link chips, fetched-page cards, tool-search matches",
|
||
},
|
||
{ key: "forms", label: "Form cards", hint: "interactive ask-form questions + their answers" },
|
||
{ key: "errors", label: "Error states", hint: "red styling + error dots" },
|
||
{ key: "forkBanner", label: "Fork origin", hint: "banner linking a forked conversation to its source" },
|
||
{
|
||
key: "msgNav",
|
||
label: "Message jumper",
|
||
hint: "up/down arrows above your messages, to hop turn by turn",
|
||
},
|
||
],
|
||
},
|
||
{
|
||
title: "Gizmos",
|
||
items: [
|
||
{ key: "taskPanel", label: "Task panel", hint: "floating task checklist" },
|
||
{ key: "fastForward", label: "Fast-forward", hint: "hold-to-3× button" },
|
||
{
|
||
key: "deployGizmo",
|
||
label: "Deploy preview",
|
||
hint: "open the deployed site, bottom-right",
|
||
},
|
||
{
|
||
key: "contextGizmo",
|
||
label: "Context meter",
|
||
hint: "how full the model's context window is, bottom-right",
|
||
},
|
||
{
|
||
key: "costCurve",
|
||
label: "Accumulated cost",
|
||
hint: "running-total curve + $1/$5/$10 guides over the cost-growth bars",
|
||
},
|
||
{ key: "animations", label: "Animations", hint: "typewriter + staggered reveal" },
|
||
{
|
||
key: "graphPhysics",
|
||
label: "Graph physics",
|
||
hint: "live force simulation on the graph view",
|
||
},
|
||
{
|
||
key: "avatar",
|
||
label: "Agent avatar",
|
||
hint: "Hemp Henry roaming the bottom of the screen",
|
||
},
|
||
{
|
||
key: "avatarDrag",
|
||
label: "Pick up avatar",
|
||
hint: "drag the roaming avatar around — it dangles while held and drops back to the floor",
|
||
},
|
||
{
|
||
key: "railAvatars",
|
||
label: "Running avatars",
|
||
hint: "one avatar per running conversation on the home page, following its state",
|
||
},
|
||
],
|
||
},
|
||
];
|
||
|
||
export const VIS_KEYS: VisKey[] = VIS_GROUPS.flatMap((g) => g.items.map((i) => i.key));
|
||
|
||
interface VisState {
|
||
/** Keys the user has switched off. Everything not listed is visible. */
|
||
hidden: VisKey[];
|
||
toggle: (k: VisKey) => void;
|
||
setAll: (visible: boolean) => void;
|
||
}
|
||
|
||
export const useVisibility = create<VisState>()(
|
||
persist(
|
||
(set) => ({
|
||
hidden: [],
|
||
toggle: (k) =>
|
||
set((s) => ({
|
||
hidden: s.hidden.includes(k) ? s.hidden.filter((x) => x !== k) : [...s.hidden, k],
|
||
})),
|
||
setAll: (visible) => set({ hidden: visible ? [] : [...VIS_KEYS] }),
|
||
}),
|
||
{
|
||
name: "ai-agent-visibility",
|
||
storage: createJSONStorage(() => serverStorage),
|
||
},
|
||
),
|
||
);
|
||
|
||
/** Every switch as a plain `key → visible` record (one store subscription). */
|
||
export function useVis(): Record<VisKey, boolean> {
|
||
const hidden = useVisibility((s) => s.hidden);
|
||
return useMemo(() => {
|
||
const out = {} as Record<VisKey, boolean>;
|
||
for (const k of VIS_KEYS) out[k] = !hidden.includes(k);
|
||
return out;
|
||
}, [hidden]);
|
||
}
|
||
|
||
/** A single switch, for components that only care about one. */
|
||
export function useVisible(k: VisKey): boolean {
|
||
return useVisibility((s) => !s.hidden.includes(k));
|
||
}
|