From 84b82318459d088227bb78d08a6c3f6db7739536 Mon Sep 17 00:00:00 2001 From: Suleiman Shahbari Date: Mon, 13 Jul 2026 01:45:14 +0300 Subject: [PATCH 1/2] feat(framework): agent-pushed markdown views in the dashboard right rail The agent can display ad-hoc markdown in the side panel with a non-blocking show-markdown block (a plan, a summary, a writeup), parsed off the turn text like the await gates. Each becomes a view FrameworkEvent that renders as a first-class Views tab in the right rail with a sticky top-nav; re-showing the same title updates that view in place. Complements the choice-gate rail. Closes #441 --- .changeset/dashboard-agent-views-441.md | 5 ++ .../components/RightRail.tsx | 38 +++++++++----- .../components/ViewsRail.tsx | 45 ++++++++++++++++ .../framework-dashboard/lib/live-state.ts | 20 ++++++++ .../framework-dashboard/pages/index/+Page.tsx | 5 +- packages/framework/src/events.ts | 9 ++++ packages/framework/src/prompt-run.ts | 9 +++- packages/framework/src/run.ts | 8 ++- packages/framework/src/turn-gate.test.ts | 30 ++++++++++- packages/framework/src/turn-gate.ts | 51 +++++++++++++++++++ 10 files changed, 203 insertions(+), 17 deletions(-) create mode 100644 .changeset/dashboard-agent-views-441.md create mode 100644 packages/framework-dashboard/components/ViewsRail.tsx diff --git a/.changeset/dashboard-agent-views-441.md b/.changeset/dashboard-agent-views-441.md new file mode 100644 index 0000000..2ecd045 --- /dev/null +++ b/.changeset/dashboard-agent-views-441.md @@ -0,0 +1,5 @@ +--- +"@gemstack/framework": minor +--- + +Let the agent push ad-hoc markdown views into the dashboard right rail. A `show-markdown` block in a turn (non-blocking, unlike a choice gate) becomes a `view` event that renders as a first-class Views tab in the right rail, with a sticky top-nav to jump between views. Re-showing the same title updates that view in place. diff --git a/packages/framework-dashboard/components/RightRail.tsx b/packages/framework-dashboard/components/RightRail.tsx index d84b033..f3814c6 100644 --- a/packages/framework-dashboard/components/RightRail.tsx +++ b/packages/framework-dashboard/components/RightRail.tsx @@ -3,30 +3,42 @@ import type { ChoiceRequest } from '@gemstack/framework' import { DocsPanel } from './DocsPanel.js' import { ProjectLogPanel } from './ProjectLogPanel.js' import { ChoicesRail } from './ChoicesRail.js' +import { ViewsRail } from './ViewsRail.js' +import type { AgentView } from '../lib/live-state.js' import { Badge } from './ui/badge.js' import { Button } from './ui/button.js' import { cn } from '../lib/utils.js' -type Tab = 'choices' | 'docs' | 'log' +type Tab = 'choices' | 'views' | 'docs' | 'log' // The right sidebar (#314 third rail): the interactive choice gates the run parks on -// (#440), the surfaced docs (PLAN/TODO), and the committed project log. Docs/log are -// Telefunc-backed reads of the selected project; the choices come from the live event -// stream, passed down from the shell. The rail jumps to Choices whenever a gate opens so -// the decision is in view. -export function RightRail({ projectId, choices }: { projectId: string | null; choices: ChoiceRequest[] }) { +// (#440), the ad-hoc markdown views the agent pushes (#441), the surfaced docs (PLAN/TODO), +// and the committed project log. Choices/views come from the live event stream, passed +// down from the shell; docs/log are Telefunc-backed reads of the selected project. The rail +// jumps to whatever the run most wants seen: a choice gate first, else a fresh view. +export function RightRail({ + projectId, + choices, + views, +}: { + projectId: string | null + choices: ChoiceRequest[] + views: AgentView[] +}) { const [tab, setTab] = useState('docs') const hasChoices = choices.length > 0 + const hasViews = views.length > 0 - // A gate opening pulls the rail to Choices; when the last one clears, fall back to Docs. + // Pull the rail to the most urgent surface: a choice gate over a view over the docs. useEffect(() => { - setTab(hasChoices ? 'choices' : 'docs') - }, [hasChoices]) + setTab(hasChoices ? 'choices' : hasViews ? 'views' : 'docs') + }, [hasChoices, hasViews]) if (!projectId) return null - const tabs: Tab[] = hasChoices ? ['choices', 'docs', 'log'] : ['docs', 'log'] - const label = (t: Tab) => (t === 'choices' ? 'Choices' : t === 'docs' ? 'Docs' : 'Project log') + const tabs: Tab[] = [...(hasChoices ? ['choices' as const] : []), ...(hasViews ? ['views' as const] : []), 'docs', 'log'] + const label = (t: Tab) => (t === 'choices' ? 'Choices' : t === 'views' ? 'Views' : t === 'docs' ? 'Docs' : 'Project log') + const count = (t: Tab) => (t === 'choices' ? choices.length : t === 'views' ? views.length : 0) return (