Skip to content

feat(frontend): observatory workspace skin - #4915

Open
LittleChenLiya wants to merge 51 commits into
bytedance:mainfrom
LittleChenLiya:feat/observatory-skin
Open

feat(frontend): observatory workspace skin#4915
LittleChenLiya wants to merge 51 commits into
bytedance:mainfrom
LittleChenLiya:feat/observatory-skin

Conversation

@LittleChenLiya

@LittleChenLiya LittleChenLiya commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

概述

在 PR #4914(经典皮肤 + 前端重构)之上,叠加观星台专属皮肤内容。观星台是 deerflow 的第二个工作区皮肤,提供深蓝星空主题与全套动效。

改动内容

换肤入口与代码分割

  • observatory-lazy.tsx:观星台专属组件的 next/dynamic 懒加载入口(ssr: false),在 workspace-content.tsx 挂载
    • 经典皮肤用户不下载任何观星台 JS chunk(PointerPlay / CornerConstellations / IdleMeteors 等)
  • core/skins/types.ts / storage.tsSKIN_IDS 扩展至 ["classic", "observatory"],新增 hasPlayedObservatoryOpening / markObservatoryOpeningPlayed(开场动画记忆)

动效(观星台皮肤专属)

  • 页面淡入淡出page-enter.tsx 在观星台下为路由切换注入 obs-page-enter 过渡动画;经典皮肤直接透传(无动画)
  • 昼夜更替动画theme-transition.tsapplyObservatoryTheme 在切换明/暗主题时触发地平线动画(playObservatoryHorizonto-night / to-dawn),主题切换延迟 900ms 执行 + 2s 淡出(obs-theme-fading
  • 开场动画observatory-opening.tsx 首次进入观星台时的星空开场(localStorage 记忆,仅播一次)
  • 工作区引入workspace-intro.tsx / workspace-intro-event.ts 进入工作区的引入过渡
  • 空闲流星idle-meteors.tsx 页面闲置45秒后,背景流星划过
  • 四角星座corner-constellations.tsx 四角星座连线
  • 鼠标星点pointer-play.tsx 鼠标移动时的交互星点
  • 设置面板装饰settings-ornament.tsx 设置面板星尘装饰
  • 星群标记asterism-mark.tsx 欢迎页星群标识

组件接入点

  • input-box.tsx:观星台下挂载 ObservatoryOpening,输入外壳套用 obs-input-shell
  • welcome.tsx:观星台欢迎语 + AsterismMark
  • message.tsx:观星台消息气泡变体(左侧描边、透明底)
  • appearance-settings-page.tsx:经典 + 观星台双卡片网格,选中观星台时触发地平线动画;settings-dialog.tsx 应用观星台类名

样式

  • globals.css:观星台专属 CSS 块([data-skin="observatory"] 选择器),含深蓝配色、obs-page-enter / obs-theme-fading / ws-intro 动效、布局微调
  • i18n:en-US.ts / zh-CN.ts / types.ts 新增 skins.observatory(含 toggleSky / nightLabel / dawnLabel

测试

  • storage.test.ts:双皮肤存储测试
  • theme-transition.test.ts:地平线动画 / 主题解析测试

截图

页面 说明
工作区(观星台) 深蓝星空背景 + 空闲流星动画 + 四角星座
页面切换 路由切换时的淡入淡出过渡
明暗主题切换 昼夜更替地平线动画
消息气泡 观星台消息样式
鼠标移动 交互星点尾迹
image

验证

  • pnpm typecheck 通过
  • pnpm lint 通过
  • pnpm format 通过
  • ✅ 代码分割:经典皮肤用户不加载观星台 chunk

依赖

@willem-bd willem-bd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full diff at a25bbfe (this PR currently carries the whole #4914 base rework plus the observatory layer, so I reviewed both). The observatory skin layer itself looks solid: lazy chunking via observatory-lazy.tsx, ssr: false everywhere heavy, consistent prefers-reduced-motion handling, SSR-safe storage access, an anti-FOUC boot script in layout.tsx, and real unit tests for storage/theme transitions. The findings below are mostly in the shared base-rework files: one correctness bug that breaks skill toggling for zh-CN users, an e2e suite that this diff breaks, a silent feature removal in the memory page, and a couple of smaller regressions in the scheduled-task schedule input.

[skills, filter],
() =>
skills
.filter((skill) => skill.category === filter)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: the localized display name is used as the skill identifier. localizeSkill overwrites skill.name with the Chinese display name (e.g. deep-research深度研究), but skill.name is then consumed as identity downstream: the Switch calls enableSkill({ skillName: skill.name, ... }) (line 244), which issues PUT /api/skills/{skillName}. In the zh-CN locale every toggle on a mapped skill will PUT to /api/skills/深度研究 and fail. It's also the React key.

Suggestion: keep the original name for identity and localize only at render time, e.g. have localizeSkill return { ...skill, displayName, description } (or look up SKILL_ZH[skill.name] inside the JSX for the title/description only). Side note: SKILL_ZH hardcodes zh copy in the component while this same PR adds proper i18n keys in locales/zh-CN.ts for everything else — if the map stays, it would be more consistent to move the strings into the locale files.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skill localization moved out of this PR to #4922, where the identifier bug is fixed (raw skill.name kept as identity, displayName used only for the title).

<Button
size="sm"
onClick={() => router.push("/workspace/scheduled-tasks/new")}
data-testid="scheduled-task-create-toggle"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rework breaks frontend/tests/e2e/scheduled-tasks.spec.ts, which isn't updated in this PR (and the e2e-tests workflow runs on frontend/** changes, so CI will be red). Concretely:

  • scheduled-task-create-form testid is gone — creation moved to the new /workspace/scheduled-tasks/new page; the spec's "user can create a scheduled task from the page" test fills the old inline form.
  • scheduled-task-detail testid is gone — detail is now a Dialog, and it only renders after clicking a card (detailOpen starts false, and selectedTask no longer auto-selects the first task). The pause/trigger/filter-fallback tests all assume the detail pane is visible on load.
  • Even the first test ("page is reachable from sidebar") asserts scheduled-task-runs contains "0 runs", but that testid now only exists inside the closed dialog.

The spec needs rewriting for the new flow (navigate to /new to create; click a card to open the dialog). Repo convention is that reworks ship with their tests — this probably belongs to the #4914 base layer, but it has to land somewhere before merge.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scheduled-tasks rework lives in the base layer #4914, where scheduled-tasks.spec.ts was rewritten for the card-grid/detail-dialog UI.

<h3 className="text-base font-medium">
{t.settings.memory.markdown.facts}
</h3>
{/* Facts list */}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The memory page no longer renders learned summaries at all — only facts survive. summariesToMarkdown / buildMemorySectionGroups / the summaries card were deleted, so users can no longer see work context / personal context / top-of-mind / history summaries anywhere in the UI (only via JSON export). This isn't mentioned in the PR description, and it leaves orphaned i18n keys behind (summaryReadOnly, filterAll/filterFacts/filterSummaries, and most of settings.memory.markdown.* — only markdown.table.* and markdown.facts are still referenced).

If dropping the summaries view is a deliberate product decision for the rework, please call it out in the description and clean up the dead keys; if not, the summaries block needs to be restored alongside the facts list.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The memory page rework is part of #4914 (base layer), not this observatory-only PR.

"America/Chicago",
"America/Los_Angeles",
// 只保留最常用的几个时区,避免选项过多难以选择。
const COMMON_TIMEZONES: Array<{ value: string; label: string }> = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: this replaces the previous full timezone list (Intl.supportedValuesOf("timeZone") with a FALLBACK_TIMEZONES fallback) with a fixed set of 10 zones plus the currently-selected one. A user in, say, Australia/Sydney or America/Denver who wants a task to run in a zone other than their browser-detected default can no longer select it. If the motivation is only the overwhelming dropdown length, a searchable combobox (or keeping supportedValuesOf with the common zones pinned to the top) preserves the old capability. Also, the zh labels are hardcoded here — the non-zh branch falls back to the raw IANA id — while the rest of this PR routes copy through locales/zh-CN.ts.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timezone dropdown belongs to #4914, which restores the full Intl.supportedValuesOf list behind a common/all toggle.

/>
</div>
<Input
type="time"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor UX trap from splitting the old datetime-local input: the time field is optional, and a date with an empty time still produces a non-empty run_at (via runAtLocal = "YYYY-MM-DD"), so the create button enables and zonedLocalToUtcIso silently schedules it at 00:00 in the selected timezone. The old input couldn't represent that state. Consider requiring the time (add onceTime to validDate) or defaulting it to a visible value so the user sees when the task will actually fire.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The one-time scheduling input lives in #4914 (base layer).

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedTask?.id]);

const statusFilters = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this rework orphans a fair bit of dead code worth sweeping up — @/core/scheduled-tasks/recipes is no longer imported anywhere, describeSchedule in cron.ts is now only referenced by its own unit test (the schedule preview line was removed from the form), and several i18n keys are unused after the redesign: scheduledTasks.recipes.*, filters.allStatuses/allTypes/cron/once/completed/failed, and workspace.visitGithub/reportIssue/about (the menu now uses the new github key).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead-code sweep for recipes/describeSchedule/i18n keys is tracked in #4914.

@LittleChenLiya
LittleChenLiya marked this pull request as ready for review August 21, 2026 05:37
@github-actions github-actions Bot added area:frontend Next.js frontend under frontend/ risk:medium Medium risk: regular code changes size/XL PR changes 700+ lines labels Aug 21, 2026
@LittleChenLiya
LittleChenLiya force-pushed the feat/observatory-skin branch 2 times, most recently from fb3cfe1 to d5cc199 Compare August 21, 2026 05:57

@willem-bd willem-bd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up review at head 65222e1, scoped to the observatory-layer files this PR adds on top of the #4914 base (the earlier findings on the shared base-rework files still stand). The layering itself is well done — clean lazy chunking, consistent prefers-reduced-motion handling, SSR-safe storage access, and unit tests for storage/theme transitions. But at this head the workspace does not render at all: SkinProvider is defined yet never mounted anywhere in the tree, so every useSkin() consumer (starting with PageEnter, which wraps all workspace children) throws during render. Plus one hydration-mismatch bug in AsterismMark and two smaller notes below.

<GatewayOfflineBanner gatewayUnavailable={gatewayUnavailable} />
<ModelLoadErrorBanner gatewayUnavailable={gatewayUnavailable} />
{children}
<PageEnter>{children}</PageEnter>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker: SkinProvider is never mounted, so every /workspace/* route throws during render. At this head, SkinProvider is defined in frontend/src/core/skins/context.tsx but a tree-wide grep shows no file renders it — yet PageEnter calls useSkin() unconditionally, and WorkspaceContent wraps all workspace children in <PageEnter> (here) and also renders ObservatoryLazy (line 45). useSkin() throws "useSkin must be used within SkinProvider" when the context is null, so the whole workspace crashes server- and client-side for every user, in both skins (same for InputBox, Welcome, and the settings appearance page, which all call useSkin()). Typecheck/lint can't catch this. Fix: mount <SkinProvider> above these consumers — e.g. wrap the tree in WorkspaceContent (around SidebarProvider) or in app/layout.tsx next to the boot script.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 35b57c2SkinProvider is now mounted in the workspace and showcase layouts.

title?: string;
}) {
const mark = useMemo(
() => MARKS[Math.floor(Math.random() * MARKS.length)] ?? MARKS[0],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hydration mismatch: Math.random() runs during render on both the server and the client. AsterismMark is statically imported by welcome.tsx and appearance-settings-page.tsx, so it is server-rendered; the server picks one MARKS entry and hydration picks another, which disagrees on viewBox, the path d, and the star circles — React 18 will log a hydration error and force a client re-render of the subtree (with a visible flip of the asterism). Since the component is pure decoration, either pick deterministically (e.g. derive the index from a stable input like the pathname passed as a prop) or choose the mark after mount (useEffect + useState) and render a fixed default during SSR.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 613f827AsterismMark no longer calls Math.random() during render (deterministic selection after mount), removing the hydration mismatch.

<div className="obs-welcome-kicker text-primary mb-2 flex items-center justify-center">
<AsterismMark className="h-7 w-16" />
</div>
<h1 className="obs-welcome-title font-display text-[2.6rem] leading-[1.15] font-bold tracking-tight">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: font-display is a no-op here — Tailwind v4 only generates font-{name} utilities from theme vars, and only --font-sans is defined in globals.css (no --font-display), so this class is never emitted. Either add --font-display to the theme if a display face was intended for the observatory title, or drop the class.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 613f827 — dropped the no-op font-display class.

}
frame = window.requestAnimationFrame(tick);
};
frame = window.requestAnimationFrame(tick);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perf note: this requestAnimationFrame(tick) runs unconditionally, and corner-constellations.tsx (line 101) uses the same pattern — so two rAF loops run for the entire session while the observatory skin is active, mapping over the star arrays on every frame even when everything has settled and no pointer has moved for minutes. Consider stopping the loop once moved is false and re-arming it from the pointermove listener (and on resize / skin change) so an idle observatory workspace costs ~0 CPU instead of two perpetual frame callbacks.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 613f827 — pointer-play now stops its rAF loop when idle and re-arms from pointermove.

@LittleChenLiya

Copy link
Copy Markdown
Collaborator Author

Thanks for the scoped follow-up. All four observatory findings are addressed:

  • SkinProvider never mounted (blocker): SkinProvider is now mounted in WorkspaceContent (wrapping SidebarProvider) and in the public showcase layout, so every useSkin() consumer — PageEnter, ObservatoryLazy, Welcome, InputBox, and the settings appearance page — renders without throwing in both skins.
  • AsterismMark hydration mismatch: the mark is now selected after mount via useEffect + useState (rendering a fixed default during SSR), so server and client no longer disagree on viewBox/path/stars.
  • font-display no-op: removed the class from the observatory welcome title.
  • pointer-play rAF loop: the loop now stops once the stars settle and re-arms from the pointermove listener, so an idle observatory workspace no longer burns two perpetual frame callbacks.

The base-rework items referenced in the earlier review were resolved in the #4914 base layer (StatusBadge dot, memory import validation, e2e specs, timezone list, queryError gating) and are now present in this branch after the rebase.

@willem-bd willem-bd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up review at head 613f827. The pushes since the last round resolved the three previously-flagged observatory-layer issues - SkinProvider is now mounted in both workspace and showcase layouts (35b57c2), AsterismMark picks its mark deterministically during render and randomizes only in an effect (613f827), and the FOUC boot script landed (6a0c38d). The base-rework findings from the first round were addressed on the #4914 side (timezone dropdown with full zone list + e2e fixes in 40bcb8e, memory/settings feedback in 640e8db) and are being tracked there. Three smaller residual notes below: the corner-constellations rAF loop is the one half of the earlier perf note that the idle rAF stop commit didn't cover, the PageEnter conditional wrapper remounts the whole workspace subtree, and the FOUC boot script duplicates the storage-key constant.

const box = svgRef.current?.getBoundingClientRect();
if (box) onStars?.(next.map((star) => toScreen(star, box)));
}
frame = window.requestAnimationFrame(tick);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Residual from the earlier rAF perf note: 613f827 ("idle rAF stop") gated pointer-play's re-arm on moved, but this loop still calls requestAnimationFrame(tick) unconditionally at the end of tick() - so while the observatory skin is active this runs a full map over HOMES + goalOf distance math every frame for the entire session, even after the stars have settled. Same fix applies: move the re-arm inside if (moved) (the initial frame = window.requestAnimationFrame(tick) arm can stay, or an occasional low-rate keepalive tick can be used if you want the loop to wake on the next pointermove - pointer-play re-arms from its pointermove listener via arm(), which this component could mirror using its existing localRef).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3dd922a — corner-constellations now stops its rAF loop when idle instead of running every frame.

}, [pathname, skin]);

if (skin !== "observatory") {
return children;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: switching the wrapper shape on skin (<div> in observatory vs. children directly in classic) unmounts and remounts the entire workspace subtree whenever skin changes. That happens not only when the user switches skins in settings - for observatory users it happens on every page load too, because SkinProvider starts in classic and flips to the stored skin in a useLayoutEffect after hydration, so the tree renders unwrapped first and then remounts inside the obs-page-enter div (losing any component-local state below, e.g. input drafts, and paying a double mount of the whole workspace). Rendering the wrapper <div> unconditionally and toggling only the class (obs-page-enter + is-on vs. a plain min-h-0 flex-1) keeps the element type stable across skin changes and costs classic users nothing measurable.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3dd922a — page-enter now renders an unconditional wrapper and toggles only the class, so the subtree no longer remounts on skin change.

Comment thread frontend/src/app/layout.tsx Outdated
dangerouslySetInnerHTML={{
__html: `(function () {
try {
var s = localStorage.getItem("deerflow.skin");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the boot script hardcodes "deerflow.skin" and "observatory", duplicating SKIN_STORAGE_KEY and the SkinId values from core/skins/types.ts. If either ever changes (or a third skin joins), this guard silently stops matching and the FOUC protection quietly regresses with no type/lint error to catch it. Interpolating the constants keeps a single source of truth: __html: \(function(){try{var s=localStorage.getItem(${JSON.stringify(SKIN_STORAGE_KEY)});if(s===${JSON.stringify("observatory")}){...}catch(e){}})();`` - it's still a static string at build time, so there's no cost to the pre-hydration path.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3dd922a — the boot script now interpolates SKIN_STORAGE_KEY and the observatory value from the shared constants.

@willem-bd willem-bd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up pass at 613f827 (head unchanged since the last round). One new finding this round: the observatory skin clips document-level scrolling, which breaks pages that rely on viewport scroll — details inline on the sidebar-inset rule. The three residual notes from the last round (unconditional rAF in corner-constellations.tsx, subtree remount on skin flip in page-enter.tsx, hardcoded skin constants in the layout.tsx boot script) still stand as posted; the base-rework findings remain tracked on #4914.

Comment thread frontend/src/styles/globals.css Outdated
animation: obs-rise 0.7s ease both;
}
[data-skin="observatory"] [data-slot="sidebar-inset"] {
overflow: hidden !important;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: observatory disables document scrolling, so pages without their own scroll container lose everything below the fold. With the observatory skin active, three layers clip at once: this overflow: hidden !important on [data-slot="sidebar-inset"], [data-skin="observatory"] .obs-page-enter { overflow: hidden; ... } (line 831 — it stays hidden after the enter animation, only opacity is animated), and html[data-skin="observatory"] body.obs-lock-scroll { overflow: hidden } (line 1008), which ObservatoryLazy (observatory-lazy.tsx:23) applies for the entire session, not just during the opening animation. Nothing in the workspace can then scroll via the document.

Pages that own a scroller are fine (chat thread, /workspace/scheduled-tasks/new has overflow-y-auto on its main), but /workspace/scheduled-tasks has no internal scroll container — neither here nor on main (WorkspaceContainer/WorkspaceBody don't scroll; the card grid relies on viewport scroll, which is exactly how it works under the classic skin today). With more tasks than fit the viewport, the grid is simply cut off with no scrollbar and no way to reach the rest. Even a single screen of content loses the bottom sliver, because WorkspaceContainer is h-screen inside the flex-1 min-h-0 page-enter box that is already offset by the banner height.

Two possible fixes: give the wrapper a real scroller once the enter animation finishes (e.g. overflow-y: auto on .obs-page-enter.is-on after the animation, or an obs-scroll region inside), or scope the !important clip / body lock to the opening and theme-transition windows only. Worth checking the other non-chat workspace routes for the same pattern before merge.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3dd922a — removed the redundant overflow: hidden on .obs-page-enter and scoped the body lock to the opening window, restoring document scroll.

@willem-bd willem-bd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up at 3dd922a — all four findings from the last round verified fixed: the boot script now interpolates SKIN_STORAGE_KEY from the shared constants (types.ts is a pure constants module, so importing it into the server layout is fine), corner-constellations stops its rAF once the stars settle and re-arms from pointermove (onStars is a stable useCallback in observatory-overlays, so no effect churn), page-enter renders an unconditional wrapper and toggles only the class (no subtree remount on skin flip), and document scroll is restored — the sidebar-inset !important clip and .obs-page-enter overflow are gone, and the body lock is now gated on html.obs-is-opening, which observatory-opening.tsx only sets during the 2.8s first-visit opening and cleans up reliably (the remaining overflow: hidden rules sit on fixed overlay layers only). One small residual of the idle-stop change noted inline; the base-rework items stay tracked on #4914/#4922.

if (box) onStars?.(next.map((star) => toScreen(star, box)));
frame = window.requestAnimationFrame(tick);
} else {
frame = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small residual of the idle-stop change: while the loop is stopped here, the screen-space positions pushed via onStars go stale after a window resize or sidebar open/close. The settle branch never re-emits, and re-arming from pointermove only recomputes when a star actually displaces (pointer within 70 viewBox units of a home), so the corner link line in PointerPlay — extraStars are viewport coords drawn in the fixed obs-pointer-layer — can anchor to outdated coordinates until the pointer pushes a corner star. PointerPlay handles this itself via its resize listener + ResizeObserver (pointer-play.tsx:190); this component could mirror it: window.addEventListener("resize", arm) (with cleanup) plus re-emitting onStars with the fresh svgRef box in this settle branch. Cosmetic only, fine to fold into a follow-up.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 956dfad: corner-constellations now mirrors PointerPlay — a window resize listener (with cleanup) re-emits onStars with the fresh svgRef bounding box and re-arms the rAF loop, so the corner link line cannot anchor to stale coordinates after a resize or sidebar toggle.

@willem-bd willem-bd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up at 956dfad. The head commit (re-anchor corner stars on window resize) resolves the last residual cleanly — since .obs-corner-sky is a fixed full-viewport container, window resize is the only thing that moves its box, and the new handler both re-emits fresh screen coords and re-arms the loop; the settle branch now also emits an initial projection on mount, so PointerPlay gets the corner stars before the first pointer interaction. CI is green including e2e. Two small new findings below: two observatory class names that never had any CSS behind them, and the boot script painting the observatory palette onto routes that don't mount SkinProvider. Also a docs note, no code change needed: the PR description says selecting the observatory card triggers the horizon animation, but at this head the horizon only plays on theme switches — SkinCard wires onSelect={setSkin} directly with no playObservatoryHorizon call.

<PromptInput
className={cn(
"bg-background/85 relative z-10 rounded-2xl backdrop-blur-sm transition-all duration-300 ease-out *:data-[slot='input-group']:rounded-2xl",
skin === "observatory" && "obs-input-shell",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: obs-input-shell has no CSS behind it — at this head no stylesheet in the repo defines a .obs-input-shell selector (it's not in globals.css, and it never was, even in the first observatory-layer commit af67d96), so this class currently does nothing even though the PR description lists it as the observatory input-shell treatment. Same for obs-skin-card--observatory in appearance-settings-page.tsx (line 261): .obs-skin-card is styled, but the --observatory modifier has no rule. Either add the intended rules or drop the dead class names so the skin hooks that exist match the ones that are styled.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 00961a9 - dropped the no-op obs-input-shell class in input-box.tsx and the obs-skin-card--observatory modifier in appearance-settings-page.tsx. The remaining skin hooks (.obs-skin-card / .obs-theme-card under [data-skin="observatory"]) are the ones that are actually styled.

try {
var s = localStorage.getItem(${JSON.stringify(SKIN_STORAGE_KEY)});
if (s === ${JSON.stringify("observatory")}) {
document.documentElement.setAttribute("data-skin", ${JSON.stringify("observatory")});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: because this boot script runs on every route while the [data-skin="observatory"] token block in globals.css (line 478) overrides --background/--primary/--border etc. at the document root, an observatory user also gets the observatory palette on routes that never mount SkinProvider — the auth pages ((auth)/login, (auth)/setup use bg-background/border-border/text-muted-foreground), the landing page header, and blog — where none of the observatory layout/motifs exist and there's no way to switch the skin back. If the skin is meant to be workspace+showcase only, consider scoping the token overrides (e.g. under the workspace/showcase shells) or clearing the attribute in those layouts; if it's meant to be app-wide, mounting SkinProvider there too would at least make it consistent.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 00961a9 - scoped the palette to workspace/showcase only. The root boot script now applies data-skin only when the pathname is under /workspace or /showcase (via a shared SKIN_SCOPED_PREFIXES constant), and a new SkinRouteGuard client component clears the attribute on client-side navigation to public routes, so auth/landing/blog/docs never inherit the observatory palette.

…ain them

Keep inbound thread_id for Back/Cancel, but send create success to the submitted context: global list for fresh_thread_per_run, or the chosen target thread for reuse_thread. Cover both branches in e2e.
Keep the redesigned memory facts list from this PR. Main's Streamdown sanitization on memory summaries does not apply because this branch no longer renders summaries via SafeStreamdown. Auto-merged chat-page (ThreadSubagentBatches), i18n, and mock-api branch coverage from bytedance#4983/bytedance#4998/bytedance#4987.
Keep the card+dialog scheduled-tasks UI from this PR. Port bytedance#5064 duplication into the create page via query params (title/prompt/context/schedule), restore schedule-preview via describeSchedule, and adapt the duplicate e2e to open the detail dialog then navigate to /new.
# Conflicts:
#	frontend/src/components/workspace/input-box.tsx
Honor an explicit context_mode when duplicating, and pass the source
task id plus sessionStorage instead of serializing the prompt into the
query string.
…llout

The describe("describeSchedule") block (once/daily en+zh, weekly en+zh,
weekly-no-weekdays fallback, hourly, monthly, custom) was accidentally
deleted during a rebase onto main while buildOnceRunAtLocal was extracted.
Restore it so the locale branches of describeSchedule stay covered.
…idle rAF thresholds, horizon timer cleanup, remove dead ws-intro
…, debug meter css, unused skin vars, dead i18n keys)
…om during render), idle rAF stop, drop no-op font-display
… workspace/showcase

Remove the no-op obs-input-shell and obs-skin-card--observatory classes that had no CSS behind them, and gate the root-layout boot script on workspace/showcase pathnames so the observatory palette no longer leaks to auth/landing/blog/docs. A SkinRouteGuard clears data-skin on client-side navigation to public routes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:frontend Next.js frontend under frontend/ risk:medium Medium risk: regular code changes size/XL PR changes 700+ lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants