OR-280 Nudge new installs to run the demo experiment (#338)

* Nudge new installs to run the demo experiment

Analytics for Sep 9-12 installs showed the largest funnel leak between
"onboarding complete" and "first message sent" (43.8%). The demo welcome
steered users to read the recorded sessions rather than send anything.

- Rename the welcome's primary button to "Run the demo experiment" and hand
  focus to the composer, which already holds the run-experiment prompt.
- Keep that prefill on offer until the user has sent anything in the demo,
  detected by a recorded session's active leaf moving off its seeded message
  or a new session appearing, instead of only until the welcome closes.
- Show a dismissible hint above the composer explaining the 200-step probe,
  with a ring on the send button while the prefilled prompt is untouched.
- Open only the experiments tab on the first demo visit.
- Default the right pane to a quarter of the window (max 480px) on installs
  with no saved layout, and share that default with the projects home so it
  is not overwritten before the demo opens.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Refine the demo nudge and add a running-experiment hint

- Relabel the welcome button "Try the demo experiment": the click only
  focuses the prefilled prompt, so "Run" over-promised.
- Style the composer hint as a neutral surface bubble instead of a tint.
- When a demo run starts, show a second dismissible hint above the composer
  with a Logs button that opens that run's logs; it clears when the run
  leaves the running state.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Address review on the running-experiment hint

- Seed the running-run hint from the runs baseline so it survives a reload
  or a return to the demo project mid-run.
- Announce the hint to assistive tech via a polite live region and return
  focus to the composer on dismiss, matching the prompt hint.
- Share the hint bubble classes, drop the redundant Logs tooltip key, and
  use the idiomatic Arabic phrasing for the tap instruction.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Myles Anderson
2026-09-15 12:19:34 -07:00
committed by GitHub
co-authored by Claude Fable 5.1
parent 470ab76cfa
commit b38c8136e6
17 changed files with 391 additions and 274 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -49,8 +49,8 @@
html { background: #ffffff; }
html[data-theme="dark"] { background: #0e0c0c; }
</style>
<script type="module" crossorigin src="/assets/index-mNCWAddR.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BJLMzvdf.css">
<script type="module" crossorigin src="/assets/index-C4inFLcC.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CdfPFYKN.css">
</head>
<body>
<div
+5 -1
View File
@@ -69,7 +69,11 @@
"chat_panel_data_sources": "مصادر البيانات",
"chat_panel_delegated_a_task_to_a_new_agent": "فوّض مهمة إلى وكيل جديد",
"chat_panel_delete": "حذف",
"chat_panel_demo_hint_body": "يشغّل هذا الطلب اختبار تدريب صغيرًا من 200 خطوة على جهازك. اضغط Enter للبدء.",
"chat_panel_demo_run_hint_after": "لمتابعتها.",
"chat_panel_demo_run_hint_before": "التجربة قيد التشغيل الآن. اضغط على",
"chat_panel_deny": "رفض",
"chat_panel_dismiss_demo_hint": "إغلاق",
"chat_panel_edit_and_re_send": "تعديل وإعادة الإرسال",
"chat_panel_edit_message": "تعديل الرسالة",
"chat_panel_edited_a_file": "عدّل ملفًا",
@@ -474,6 +478,7 @@
"tour_demo_project": "مشروع توضيحي",
"tour_look_through_the_agent_conversations_experiments_runs_and": "تصفّح محادثات الوكيل والتجارب وعمليات التشغيل والمُخرَجات لترى كيف يتشكّل مشروع على OpenResearch.",
"tour_nanochat": "nanochat",
"tour_run_demo_experiment": "جرّب التجربة التوضيحية",
"tour_this_is_a_demo_project_showing_how_open": "هذا مشروع توضيحي يبيّن كيف يعمل OpenResearch. يستخدم هذا العرض مستودع",
"tour_welcome_to_open_research": "مرحبًا بك في OpenResearch",
"tree_view_ask_in_this_task_to_create_one_or": "اطلب في هذه المهمة إنشاء واحدة، أو بدّل إلى «المشروع بالكامل» لرؤية جميع التجارب.",
@@ -751,7 +756,6 @@
"artifacts_too_large_to_preview": "الملف كبير جدًا للمعاينة المضمّنة.",
"artifacts_open_raw": "فتح الملف الخام",
"artifacts_copy_path": "نسخ المسار",
"tour_explore_demo": "استكشاف العرض التوضيحي",
"experiment_latest_run": "أحدث تشغيل",
"experiment_runs": "عمليات التشغيل",
"experiments_none_yet": "لا توجد تجارب بعد.",
+5 -1
View File
@@ -69,7 +69,11 @@
"chat_panel_data_sources": "Data sources",
"chat_panel_delegated_a_task_to_a_new_agent": "Delegated a task to a new agent",
"chat_panel_delete": "Delete",
"chat_panel_demo_hint_body": "This prompt runs a small 200-step training probe on your machine. Hit enter to get started.",
"chat_panel_demo_run_hint_after": "to monitor it.",
"chat_panel_demo_run_hint_before": "The experiment is now running. Tap",
"chat_panel_deny": "Deny",
"chat_panel_dismiss_demo_hint": "Dismiss",
"chat_panel_edit_and_re_send": "Edit and re-send",
"chat_panel_edit_message": "Edit message",
"chat_panel_edited_a_file": "Edited a file",
@@ -474,6 +478,7 @@
"tour_demo_project": "Demo project",
"tour_look_through_the_agent_conversations_experiments_runs_and": "Look through the agent conversations, experiments, runs, and artifacts to see how a project on OpenResearch comes together.",
"tour_nanochat": "nanochat",
"tour_run_demo_experiment": "Try the demo experiment",
"tour_this_is_a_demo_project_showing_how_open": "This is a demo project showing how OpenResearch works. This demo uses Andrej Karpathy's",
"tour_welcome_to_open_research": "Welcome to OpenResearch",
"tree_view_ask_in_this_task_to_create_one_or": "Ask in this task to create one, or switch to Entire project to see all experiments.",
@@ -751,7 +756,6 @@
"artifacts_too_large_to_preview": "File too large to preview inline.",
"artifacts_open_raw": "Open raw",
"artifacts_copy_path": "Copy path",
"tour_explore_demo": "Explore the demo",
"experiment_latest_run": "Latest run",
"experiment_runs": "Runs",
"experiments_none_yet": "No experiments yet.",
+5 -1
View File
@@ -69,7 +69,11 @@
"chat_panel_data_sources": "منابع داده",
"chat_panel_delegated_a_task_to_a_new_agent": "وظیفه به عامل جدید واگذار شد",
"chat_panel_delete": "حذف",
"chat_panel_demo_hint_body": "این پیام یک کاوش آموزشی کوچک ۲۰۰ مرحله‌ای روی دستگاه شما اجرا می‌کند. برای شروع Enter را بزنید.",
"chat_panel_demo_run_hint_after": "بزنید.",
"chat_panel_demo_run_hint_before": "آزمایش اکنون در حال اجراست. برای پیگیری روی",
"chat_panel_deny": "رد کردن",
"chat_panel_dismiss_demo_hint": "بستن",
"chat_panel_edit_and_re_send": "ویرایش و ارسال دوباره",
"chat_panel_edit_message": "ویرایش پیام",
"chat_panel_edited_a_file": "فایل ویرایش شد",
@@ -470,6 +474,7 @@
"tour_demo_project": "پروژهٔ نمایشی",
"tour_look_through_the_agent_conversations_experiments_runs_and": "گفتگوهای عامل، آزمایش‌ها، اجراها و خروجی‌ها را ببینید تا با شکل‌گیری یک پروژه در OpenResearch آشنا شوید.",
"tour_nanochat": "nanochat",
"tour_run_demo_experiment": "آزمایش نمایشی را امتحان کنید",
"tour_this_is_a_demo_project_showing_how_open": "این پروژهٔ نمایشی نحوهٔ کار OpenResearch را نشان می‌دهد. این نسخهٔ نمایشی از",
"tour_welcome_to_open_research": "به OpenResearch خوش آمدید",
"tree_view_ask_in_this_task_to_create_one_or": "در این وظیفه بخواهید یکی ساخته شود، یا برای دیدن همهٔ آزمایش‌ها به «کل پروژه» بروید.",
@@ -745,7 +750,6 @@
"artifacts_too_large_to_preview": "فایل برای پیش‌نمایش درون‌صفحه‌ای بیش از حد بزرگ است.",
"artifacts_open_raw": "باز کردن فایل خام",
"artifacts_copy_path": "کپی مسیر",
"tour_explore_demo": "دیدن پروژهٔ نمایشی",
"experiment_latest_run": "آخرین اجرا",
"experiment_runs": "اجراها",
"experiments_none_yet": "هنوز آزمایشی وجود ندارد.",
+5 -1
View File
@@ -69,7 +69,11 @@
"chat_panel_data_sources": "数据源",
"chat_panel_delegated_a_task_to_a_new_agent": "已将任务委派给新智能体",
"chat_panel_delete": "删除",
"chat_panel_demo_hint_body": "这条提示会在你的机器上运行一个小型 200 步训练探测。按 Enter 开始。",
"chat_panel_demo_run_hint_after": "以查看进度。",
"chat_panel_demo_run_hint_before": "实验正在运行。点按",
"chat_panel_deny": "拒绝",
"chat_panel_dismiss_demo_hint": "关闭",
"chat_panel_edit_and_re_send": "编辑并重新发送",
"chat_panel_edit_message": "编辑消息",
"chat_panel_edited_a_file": "已编辑文件",
@@ -470,6 +474,7 @@
"tour_demo_project": "演示项目",
"tour_look_through_the_agent_conversations_experiments_runs_and": "浏览智能体对话、实验、运行和产物,了解 OpenResearch 项目是如何形成的。",
"tour_nanochat": "nanochat",
"tour_run_demo_experiment": "试试演示实验",
"tour_this_is_a_demo_project_showing_how_open": "这是一个展示 OpenResearch 工作方式的演示项目。本演示使用 Andrej Karpathy 的",
"tour_welcome_to_open_research": "欢迎使用 OpenResearch",
"tree_view_ask_in_this_task_to_create_one_or": "在此任务中请求创建实验,或切换到“整个项目”查看所有实验。",
@@ -745,7 +750,6 @@
"artifacts_too_large_to_preview": "文件太大,无法内嵌预览。",
"artifacts_open_raw": "打开原始文件",
"artifacts_copy_path": "复制路径",
"tour_explore_demo": "探索演示项目",
"experiment_latest_run": "最新运行",
"experiment_runs": "运行",
"experiments_none_yet": "还没有实验。",
+20 -27
View File
@@ -28,6 +28,7 @@ import {
type TaskWorkspace,
} from "./workspaceState";
import { getRememberedGlobalWorkspace, globalWorkspaceWriter } from "./workspacePersistence";
import { PANEL_MIN_WIDTH, initialPanelWidth, panelMaxWidth } from "./panelLayout";
import {
type ExpViewDef,
sameExpTab,
@@ -82,7 +83,6 @@ import {
cancelRun,
DEMO_MAIN_SESSION_ID,
DEMO_OVERVIEW_ARTIFACT,
DEMO_RUN_EXPERIMENT_PROMPT,
captureUiEvent,
isDemoProjectId,
type FirstAction,
@@ -221,32 +221,14 @@ function fileBranchLabel(tab: FileViewDef, baselineBranch?: string): string | un
type ExperimentsView = "tree" | "table";
/** Floating panel sizing: keep both the panel and the chat column usable. */
const PANEL_MIN_WIDTH = 360;
const PANEL_MARGIN = 10;
const WORKSPACE_CARD_MIN_WIDTH = 1448; // 1420px content plus the body’s 14px gutters.
// Space the rest of the layout needs beside the panel: the 272px rail, the
// chat column's minimum, and the gutters/margins between the three columns
// (app-body padding 14×2, rail inner margin 14, end-pane inner margin 14).
const RAIL_WIDTH = 272;
const CHAT_MIN_SPACE = 380;
const LAYOUT_CHROME = RAIL_WIDTH + 14 * 4;
// Once a drag pushes the panel past its usable max by this much, it snaps to
// fullscreen — a bit of resistance you have to overcome deliberately.
const FULLSCREEN_SNAP_SLOP = 80;
// Inward drag needed before snapping back to the last non-fullscreen width.
const FULLSCREEN_RESTORE_DRAG = 48;
/** The widest the floating panel can be while leaving the rail + chat usable. */
function panelMaxWidth(): number {
return Math.max(PANEL_MIN_WIDTH, window.innerWidth - LAYOUT_CHROME - CHAT_MIN_SPACE);
}
function initialPanelWidth(): number {
const max = panelMaxWidth();
return Math.max(PANEL_MIN_WIDTH, Math.min(760, max, Math.round(window.innerWidth * 0.4)));
}
function upsert<T extends { id: string }>(list: T[], item: T): T[] {
const i = list.findIndex((x) => x.id === item.id);
if (i < 0) return [...list, item];
@@ -651,7 +633,7 @@ export default function App({ runtime, projectId, pane }: { runtime: RuntimeInfo
}, [setContentTabOrder, setPreviewTab]);
const { ready: workspaceReady, loaded: workspaceLoaded, error: workspaceError, retry: retryWorkspace, capture: captureWorkspace, workspace: workspaceRef } = useProjectWorkspace({
projectId: uiState && sessions !== null && (destination?.kind !== "task" || !activeSessionId || sessions.includes(activeSessionId)) ? projectId : null, taskKey: activeSessionId ?? "new", location: location.href, pane,
isTask: destination?.kind === "task", demoOverview: uiState?.tourCompleted === false, state: rightPaneState,
isTask: destination?.kind === "task", firstDemoOpen: uiState?.tourCompleted === false, state: rightPaneState,
apply: applyWorkspace, getScroll: getFileScroll,
sourceModes: sourceModesRef.current, revision: metadataRevision,
});
@@ -697,6 +679,8 @@ export default function App({ runtime, projectId, pane }: { runtime: RuntimeInfo
}, [location.href, uiState, workspaceReady, railOpen, panelWidth, view]);
const onboarded = uiState?.onboardingCompleted ?? false;
const [demoWelcomeOpen, setDemoWelcomeOpen] = useState(false);
const [demoRunningRunId, setDemoRunningRunId] = useState<string | null>(null);
const [composerFocusNonce, setComposerFocusNonce] = useState(0);
const openDemoWelcome = useCallback(() => setDemoWelcomeOpen(true), []);
const closeDemoWelcome = useCallback(async (choice: "explore_demo" | "create_project" | "dismiss") => {
const firstClose = tourCompletedRef.current === false;
@@ -704,6 +688,7 @@ export default function App({ runtime, projectId, pane }: { runtime: RuntimeInfo
if (firstClose) captureUiEvent({ name: "demo_welcome_choice", choice });
setUiState((current) => current && { ...current, tourCompleted: saved.tourCompleted });
setDemoWelcomeOpen(false);
if (choice === "explore_demo") setComposerFocusNonce((n) => n + 1);
}, []);
const createProjectFromDemoWelcome = useCallback(async () => {
await closeDemoWelcome("create_project");
@@ -841,6 +826,13 @@ export default function App({ runtime, projectId, pane }: { runtime: RuntimeInfo
}
runsBaselineReadyRef.current = true;
setRunDataReady(true);
if (isDemoProjectId(baselineProjectId)) {
setDemoRunningRunId((current) =>
loadedRuns.some((run) => run.id === current && run.status === "running")
? current
: loadedRuns.find((run) => run.status === "running")?.id ?? null,
);
}
if (shouldAutoOpen) openExperimentsTab(true);
})
.catch(() => {
@@ -887,6 +879,12 @@ export default function App({ runtime, projectId, pane }: { runtime: RuntimeInfo
if (previous && previous.updatedAt > run.updatedAt) return;
observedRunsRef.current.set(run.id, run);
liveRunIdsRef.current.add(run.id);
if (isDemoProjectId(run.projectId)) {
setDemoRunningRunId((current) => {
if (run.status === "running") return current ?? run.id;
return current === run.id ? null : current;
});
}
if (run.status !== "running" || previous?.status === "running") return;
const baselineRun = baselineRunsRef.current.get(run.id);
const newSinceBaseline =
@@ -1586,13 +1584,8 @@ export default function App({ runtime, projectId, pane }: { runtime: RuntimeInfo
experimentName={experimentName}
onOpenPlan={openPlanTab}
onOpenSubagent={openSubagentTab}
composerPrefill={
activeProject &&
isDemoProjectId(activeProject.id) &&
uiState?.tourCompleted === false
? DEMO_RUN_EXPERIMENT_PROMPT
: null
}
composerFocusNonce={composerFocusNonce}
demoRunningRunId={demoRunningRunId}
runtime={runtime}
onOpenDemoWelcome={
activeProject && isDemoProjectId(activeProject.id) ? openDemoWelcome : undefined
+7
View File
@@ -24,6 +24,12 @@ export const DEMO_EXPERIMENT_LABELS: Record<string, string> = {
[DEMO_LITERATURE_SESSION_ID]: "literature",
};
export const DEMO_OVERVIEW_ARTIFACT = "cpu-apple-silicon-pipeline-results.md";
/** Leaf message each recorded demo session is seeded with; a send moves it. */
export const DEMO_SEEDED_LEAF_IDS: Record<string, string> = {
[DEMO_MAIN_SESSION_ID]: "msg_demo_nanochat_assistant_v1",
[DEMO_FIGURE_SESSION_ID]: "msg_demo_nanochat_figures_assistant_v1",
[DEMO_LITERATURE_SESSION_ID]: "msg_demo_nanochat_literature_assistant_v1",
};
export const DEMO_RUN_EXPERIMENT_PROMPT =
"Run the Muon matrix LR 2× probe experiment. When it finishes, compare its step-100 and step-200 val_bpb against the baseline and tell me whether doubling the matrix learning rate helps early training.";
@@ -1835,6 +1841,7 @@ export interface ChatSession {
createdAt: number;
updatedAt: number;
busy: boolean;
activeLeafId: string | null;
contextUsage?: ContextUsage;
}
+95 -6
View File
@@ -51,6 +51,7 @@ import {
Search,
SlidersHorizontal,
SquareTerminal,
Terminal,
ToggleRight,
TriangleAlert,
Users,
@@ -79,6 +80,8 @@ import {
captureUiEvent,
DEMO_EXPERIMENT_LABELS,
DEMO_PROJECT_ID,
DEMO_RUN_EXPERIMENT_PROMPT,
DEMO_SEEDED_LEAF_IDS,
forkChatTurn,
fmtNumber,
createRemoteSession,
@@ -4222,7 +4225,8 @@ export function ChatPanel({
onOpenSubagent,
runtime,
onOpenDemoWelcome,
composerPrefill = null,
composerFocusNonce = 0,
demoRunningRunId = null,
activeSessionId,
onActiveSessionChange,
preferredAgent,
@@ -4278,7 +4282,10 @@ export function ChatPanel({
runtime: RuntimeInfo;
/** Reopen the demo welcome modal from the chat header. */
onOpenDemoWelcome?: () => void;
composerPrefill?: string | null;
/** Increments when the demo welcome hands focus to the composer. */
composerFocusNonce?: number;
/** Demo run currently executing, for the monitor-it hint above the composer. */
demoRunningRunId?: string | null;
activeSessionId: string | null;
onActiveSessionChange: (sessionId: string | null, options?: { replace?: boolean }) => void;
/** Database-backed selection used to seed new chat sessions. */
@@ -4319,6 +4326,8 @@ export function ChatPanel({
const [unreadSessionIds, setUnreadSessionIds] = useState<ReadonlySet<string>>(new Set());
const [sessionFilter, setSessionFilter] = useState<SessionFilter>("active");
const [draft, setDraft] = useState("");
const [demoHintDismissed, setDemoHintDismissed] = useState(false);
const [demoRunHintDismissed, setDemoRunHintDismissed] = useState(false);
const [annotations, setAnnotations] = useState<ComposerAnnotation[]>([]);
const annotationId = useRef(0);
const composerScopeRef = useRef({ projectId, activeId, mainView });
@@ -4793,6 +4802,8 @@ export function ChatPanel({
: new Set(),
);
setDraft("");
setDemoHintDismissed(false);
setDemoRunHintDismissed(false);
setAttachments([]);
setTitleReveals(new Map());
seenTitles.current = new Map();
@@ -5102,14 +5113,36 @@ export function ChatPanel({
setComposerCursor(prompt.length);
});
};
// Seeds the draft while a prefill is offered without taking focus, which may
// belong to the demo welcome dialog; clearing the prefill later leaves the draft.
// On offer until the user has sent anything in the demo: a send either adds
// a session or moves a recorded session's leaf off its seeded message.
const composerPrefill =
projectId === DEMO_PROJECT_ID &&
sessions.length > 0 &&
sessions.every((session) => DEMO_SEEDED_LEAF_IDS[session.id] === session.activeLeafId)
? DEMO_RUN_EXPERIMENT_PROMPT
: null;
// Seeds without taking focus; focus may still belong to the welcome dialog.
useEffect(() => {
if (!composerPrefill) return;
setDraft(composerPrefill);
setDraft((current) => current || composerPrefill);
setSkillMenuDismissed(false);
setComposerCursor(composerPrefill.length);
}, [composerPrefill]);
useEffect(() => {
if (composerFocusNonce === 0) return;
// The welcome dialog restores its previous focus on unmount; run after that.
const frame = window.requestAnimationFrame(() => {
const el = composerRef.current;
if (!el) return;
el.focus();
el.setSelectionRange(el.value.length, el.value.length);
el.scrollIntoView({ block: "nearest" });
});
return () => window.cancelAnimationFrame(frame);
}, [composerFocusNonce]);
const demoHintId = useId();
const demoHintVisible =
projectId === DEMO_PROJECT_ID && draft === DEMO_RUN_EXPERIMENT_PROMPT && !demoHintDismissed;
const updateTranscriptBottom = useCallback((el: HTMLDivElement) => {
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 60;
stickToBottom.current = atBottom;
@@ -6213,6 +6246,59 @@ export function ChatPanel({
))}
</div>
)}
{demoHintVisible && (
<div
id={demoHintId}
role="note"
className={`composer-demo-hint ${COMPOSER_HINT_CLASS}`}
>
<FlaskConical size={16} className="shrink-0 text-primary" />
<span className="flex-1" dir="auto">{m.chat_panel_demo_hint_body()}</span>
<IconButton
size="small"
aria-label={m.chat_panel_dismiss_demo_hint()}
title={m.chat_panel_dismiss_demo_hint()}
onClick={() => {
setDemoHintDismissed(true);
composerRef.current?.focus();
}}
>
<X size={14} />
</IconButton>
</div>
)}
{demoRunningRunId && !demoRunHintDismissed && onOpenRun && (
<div
role="note"
className={`composer-demo-run-hint ${COMPOSER_HINT_CLASS}`}
>
<FlaskConical size={16} className="shrink-0 text-primary" />
<span className="flex flex-1 flex-wrap items-center gap-x-1.5 gap-y-1" dir="auto">
<span>{m.chat_panel_demo_run_hint_before()}</span>
<Button size="small" onClick={() => onOpenRun(demoRunningRunId, "keepOpen")}>
<Terminal size={14} />
{m.experiments_table_logs()}
</Button>
<span>{m.chat_panel_demo_run_hint_after()}</span>
</span>
<IconButton
size="small"
aria-label={m.chat_panel_dismiss_demo_hint()}
title={m.chat_panel_dismiss_demo_hint()}
onClick={() => {
setDemoRunHintDismissed(true);
composerRef.current?.focus();
}}
>
<X size={14} />
</IconButton>
</div>
)}
<span className="sr-only" role="status" aria-live="polite">
{demoRunningRunId
? `${m.chat_panel_demo_run_hint_before()} ${m.experiments_table_logs()} ${m.chat_panel_demo_run_hint_after()}`
: ""}
</span>
<div className={`composer-box relative flex flex-col border ${bashActive ? "border-accent-amber" : "border-border"} rounded-lg bg-background shadow-elevated`} data-onboarding="composer">
{activeHarness && !activeHarness.agentReady && (
<div className="composer-harness-warning py-2 px-3 text-subtext text-sm leading-normal border-b border-b-border-variant [&_strong]:text-accent-amber [&_strong]:font-medium [&_code]:font-mono [&_code]:text-text">
@@ -6282,6 +6368,7 @@ export function ChatPanel({
<textarea
dir="auto"
ref={composerRef}
aria-describedby={demoHintVisible ? demoHintId : undefined}
// Native prose stays visible; the aligned mirror paints only skill tokens.
className="relative z-1 bg-transparent"
value={draft}
@@ -6491,7 +6578,7 @@ export function ChatPanel({
</IconButton>
) : (
<IconButton
className="send-btn"
className={`send-btn ${demoHintVisible && activeHarness?.agentReady ? "ring-2 ring-primary ring-offset-2 ring-offset-background" : ""}`}
variant="primary"
title={bashActive ? m.chat_panel_run() : m.chat_panel_send()}
aria-label={bashActive ? m.chat_panel_run() : m.chat_panel_send()}
@@ -6514,6 +6601,8 @@ export function ChatPanel({
);
}
const COMPOSER_HINT_CLASS =
"flex items-center gap-2.5 mb-2.5 py-2 ps-3.5 pe-2 rounded-lg border border-border bg-surface text-text text-sm leading-normal";
const EMPTY_SKILLS: SkillInfo[] = [];
const EMPTY_HARNESSES: Harness[] = [];
+1 -1
View File
@@ -138,7 +138,7 @@ export function DemoWelcomeModal({
onClick={() => run(() => onClose("explore_demo"))}
disabled={saving}
>
{saving ? m.common_saving() : m.tour_explore_demo()}
{saving ? m.common_saving() : m.tour_run_demo_experiment()}
</Button>
</div>
</div>
+17
View File
@@ -0,0 +1,17 @@
// Floating panel sizing: keep both the panel and the chat column usable.
export const PANEL_MIN_WIDTH = 360;
const RAIL_WIDTH = 272;
const CHAT_MIN_SPACE = 380;
// Four 14px gutters: app-body padding ×2, rail inner margin, end-pane inner margin.
const LAYOUT_CHROME = RAIL_WIDTH + 14 * 4;
/** The widest the floating panel can be while leaving the rail + chat usable. */
export function panelMaxWidth(): number {
return Math.max(PANEL_MIN_WIDTH, window.innerWidth - LAYOUT_CHROME - CHAT_MIN_SPACE);
}
/** Default when no layout is saved. */
export function initialPanelWidth(): number {
const max = panelMaxWidth();
return Math.max(PANEL_MIN_WIDTH, Math.min(480, max, Math.round(window.innerWidth * 0.25)));
}
+2 -1
View File
@@ -9,6 +9,7 @@ import { useRuntime } from "./RemoteRuntime";
import { clearReadDemoSessions } from "./demoSessionState";
import { globalResumeLocation, projectResumeLocation } from "./routeResume";
import { getRememberedGlobalWorkspace, globalWorkspaceWriter } from "./workspacePersistence";
import { initialPanelWidth } from "./panelLayout";
import { m } from "./paraglide/messages.js";
import { Onboarding } from "./components/Onboarding";
import { ProjectsHome } from "./components/ProjectsHome";
@@ -79,7 +80,7 @@ export function ProjectsPage() {
document.title = "OpenResearch";
if (!state) return;
globalWorkspaceWriter.queue({
...(getRememberedGlobalWorkspace() ?? state.workspace ?? { railOpen: true, panelWidth: 760, experimentsView: "table" }),
...(getRememberedGlobalWorkspace() ?? state.workspace ?? { railOpen: true, panelWidth: initialPanelWidth(), experimentsView: "table" }),
lastLocation: "/projects",
});
}, [state]);
+4 -4
View File
@@ -103,7 +103,7 @@ interface Props {
location: string;
pane: Pane | undefined;
isTask: boolean;
demoOverview: boolean;
firstDemoOpen: boolean;
state: RightPaneSessionState;
apply: (state: RightPaneSessionState, saved: TaskWorkspace | undefined, restored: boolean) => void;
getScroll: () => TaskWorkspace["scroll"];
@@ -139,7 +139,7 @@ export function useProjectWorkspace(props: Props): {
capture: () => void;
workspace: MutableRefObject<ProjectWorkspace>;
} {
const { projectId, taskKey, location, pane, isTask, demoOverview, state, apply, getScroll, sourceModes, revision } = props;
const { projectId, taskKey, location, pane, isTask, firstDemoOpen, state, apply, getScroll, sourceModes, revision } = props;
const workspace = useRef<ProjectWorkspace>(emptyProjectWorkspace());
const saveError = useSyncExternalStore(subscribeErrors, () => projectId ? saveErrors.get(projectId) ?? null : null);
const [readError, setReadError] = useState<string | null>(null);
@@ -193,7 +193,7 @@ export function useProjectWorkspace(props: Props): {
appliedScope.current = scope;
appliedPane.current = paneKey;
if (isTask) {
const saved = getTaskWorkspace(document, taskKey) ?? (isDemoProjectId(projectId) ? defaultTaskWorkspace(taskKey, demoOverview) : undefined);
const saved = getTaskWorkspace(document, taskKey) ?? (isDemoProjectId(projectId) ? defaultTaskWorkspace(taskKey, firstDemoOpen) : undefined);
apply(restoreWorkspace(saved, pane), saved, true);
}
setRenderedScope(scope);
@@ -213,7 +213,7 @@ export function useProjectWorkspace(props: Props): {
committed.current = { projectId, taskKey, scope, location, pane, state: resolvedState };
} else save(projectId, location);
workspace.current = projectCache.get(projectId) ?? document;
}, [projectId, taskKey, location, pane, paneKey, isTask, demoOverview, state, apply, getScroll, sourceModes, revision, loadedProject, scope, renderedScope, capture]);
}, [projectId, taskKey, location, pane, paneKey, isTask, firstDemoOpen, state, apply, getScroll, sourceModes, revision, loadedProject, scope, renderedScope, capture]);
useEffect(() => {
const visit = epoch;
+8 -14
View File
@@ -1,7 +1,7 @@
import type { ExperimentView } from "./components/DetailDrawer";
import type { CodeView } from "./components/CodeTab";
import type { WorktreeView } from "./components/WorktreeTab";
import { DEMO_MAIN_SESSION_ID, DEMO_FIGURE_SESSION_ID, DEMO_LITERATURE_SESSION_ID, DEMO_OVERVIEW_ARTIFACT } from "./api";
import { DEMO_MAIN_SESSION_ID, DEMO_FIGURE_SESSION_ID, DEMO_LITERATURE_SESSION_ID } from "./api";
import type { Pane, TaskWorkspace } from "./workspaceState";
export function tabPane(tab: RightTab, runId?: string | null): Pane {
@@ -240,7 +240,7 @@ export interface RightPaneSessionState {
export function initialRightPaneSessionState(
sessionId?: string,
openDemoOverview = false,
firstDemoOpen = false,
): RightPaneSessionState {
const initial: RightPaneSessionState = {
rightTab: "experiments",
@@ -263,21 +263,15 @@ export function initialRightPaneSessionState(
panelMax: false,
treeViewport: null,
};
if (sessionId === DEMO_MAIN_SESSION_ID && openDemoOverview) {
const demoOverviewTab: FileViewDef = {
path: DEMO_OVERVIEW_ARTIFACT,
source: "artifacts",
};
// First demo open leads with the experiments tab so the idle follow-ups
// are visible next to the prefilled prompt that runs one of them.
if (sessionId === DEMO_MAIN_SESSION_ID && firstDemoOpen) {
// First demo open shows only the experiments tab so the idle follow-ups
// sit next to the prefilled prompt that runs one of them.
const experimentsTab: RightTab = "experiments";
return {
...initial,
rightTab: experimentsTab,
tabHistory: [demoOverviewTab, experimentsTab],
tabHistory: [experimentsTab],
experimentsTabOpen: true,
fileTabs: [demoOverviewTab],
contentTabOrder: [rightTabKey(demoOverviewTab)],
panelOpen: true,
};
}
@@ -313,8 +307,8 @@ export function initialRightPaneSessionState(
return initial;
}
export function defaultTaskWorkspace(sessionId: string | undefined, openDemoOverview: boolean): TaskWorkspace | undefined {
const state = initialRightPaneSessionState(sessionId, openDemoOverview);
export function defaultTaskWorkspace(sessionId: string | undefined, firstDemoOpen: boolean): TaskWorkspace | undefined {
const state = initialRightPaneSessionState(sessionId, firstDemoOpen);
return state.panelOpen ? rememberWorkspace(state, {}, {}) : undefined;
}
+2 -2
View File
@@ -18,7 +18,7 @@ function load(name, dependencies) {
}, exports, { addEventListener() {}, removeEventListener() {} });
return exports;
}
const demo = { DEMO_MAIN_SESSION_ID: "demo-main", DEMO_FIGURE_SESSION_ID: "demo-figures", DEMO_LITERATURE_SESSION_ID: "demo-literature", DEMO_OVERVIEW_ARTIFACT: "overview.md" };
const demo = { DEMO_MAIN_SESSION_ID: "demo-main", DEMO_FIGURE_SESSION_ID: "demo-figures", DEMO_LITERATURE_SESSION_ID: "demo-literature" };
const tabs = load("workspaceTabs", { "./api": demo });
const clean = (value) => JSON.parse(JSON.stringify(value));
const deferred = () => {
@@ -60,7 +60,7 @@ function host(api) {
const hook = load("useProjectWorkspace", { react, "./api": { ...api, isDemoProjectId: () => false }, "./workspaceState": workspace, "./workspaceTabs": tabs });
const readiness = [];
let state = tabs.initialRightPaneSessionState();
let props = { projectId: "p", taskKey: "one", isTask: true, demoOverview: false, location: "/projects/p/tasks/one", pane: undefined, sourceModes: {}, revision: 0 };
let props = { projectId: "p", taskKey: "one", isTask: true, firstDemoOpen: false, location: "/projects/p/tasks/one", pane: undefined, sourceModes: {}, revision: 0 };
const scroll = {};
const getScroll = () => scroll;
const apply = (next, saved, restored) => {