Skip to content

feat(frontend): extra features - chats settings, zh skill localization, zh about, scheduled-tasks loading - #4922

Draft
LittleChenLiya wants to merge 45 commits into
bytedance:mainfrom
LittleChenLiya:feat/frontend-extra-features
Draft

feat(frontend): extra features - chats settings, zh skill localization, zh about, scheduled-tasks loading#4922
LittleChenLiya wants to merge 45 commits into
bytedance:mainfrom
LittleChenLiya:feat/frontend-extra-features

Conversation

@LittleChenLiya

Copy link
Copy Markdown
Collaborator

概述

从 PR #4914 / #4915 中拆分出的非皮肤功能,独立成 PR 以便聚焦 review。

⚠️ 依赖:本分支基于 #4914(feat/frontend-ui-rework)构建,请先合并 #4914

包含功能

  • Chats 设置页:设置内对话管理页(列表 + 搜索 + 无限滚动 + 加载/错误/重试态)
  • 技能中文本地化:skill-settings-page 中内置技能的中文名称与描述(SKILL_ZH 映射)
  • 中文 About:about 设置页支持 zh-CN 文案(aboutMarkdownZhCN + locale 切换)
  • 定时任务加载态:scheduled-tasks 页刷新时先显示 loading、不再闪现空状态

说明

  • 这些功能与观星台皮肤无关,原本混在 PR1(feat/frontend-ui-rework)/ PR2(feat/observatory-skin)中,现拆出以缩小那两个 PR 的 diff。
  • 依赖 PR1 的设置面板框架(SettingsSection、settings-dialog 等)。

@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 marked this pull request as draft August 21, 2026 04:56
@LittleChenLiya
LittleChenLiya force-pushed the feat/frontend-extra-features branch 2 times, most recently from 65bd700 to 550aa16 Compare August 21, 2026 05:58

@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 at 550aa16 (re-reviewed after the five follow-up commits; the earlier timezone-list regression, optional once-time midnight default, weakened memory import validation, stale e2e spec, and dead code have all been addressed — thanks). Two remaining findings, one functional: the zh skill localization replaces skill.name, which is also the identifier the enable/disable API consumes, so toggling skills breaks in zh-CN. Also note this PR still carries the full globals.css palette rewrite and message-bubble restyle despite being scoped as the non-skin split, which will make it conflict-prone with the observatory-skin PR.

() =>
skills
.filter((skill) => skill.category === filter)
.map((skill) => localizeSkill(skill, locale)),

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: localizeSkill replaces skill.name with the Chinese display name (e.g. deep-research -> 深度研究), but name is the skill identifier downstream: the switch calls enableSkill({ skillName: skill.name, ... }) (line 244) and the list uses it as the React key (line 228). In zh-CN, toggling any built-in skill sends the localized display name to the backend, so the enable/disable request fails silently (useEnableSkill has no onError handling, and the switch only refreshes via invalidation on success). Keep the original name for identity and localize only for display, e.g. return { ...skill, displayName: zh.name, description: zh.description } from localizeSkill and render displayName in ItemTitle.

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 — localizeSkill now keeps skill.name as identity and localizes only displayName (7f8b1f3 + 78836eb).

<div className="text-muted-foreground flex h-40 items-center justify-center text-sm">
{t.common.loading}
</div>
) : filteredData.length === 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.

Nit: when loading fails, the queryError banner renders and then this branch renders too — data is empty (so hasTasks is false and isLoading is false), leaving "Failed to load scheduled tasks" directly above the "No scheduled tasks yet" empty state and its create button. Gate the empty states on !queryError so a load failure doesn't also advertise an empty 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.

Fixed — the empty states are now gated on !queryError, so a load failure no longer also shows the empty list.

@LittleChenLiya
LittleChenLiya force-pushed the feat/frontend-extra-features branch from 550aa16 to 7a24983 Compare August 21, 2026 10:00
@LittleChenLiya

Copy link
Copy Markdown
Collaborator Author

Thanks — addressed the two remaining findings:

  • localizeSkill identifier bug: skill.name is now kept as the raw identifier (used for the React key and the enableSkill({ skillName }) call), and the Chinese copy is applied to a separate displayName field used only for rendering. Toggling skills in zh-CN now PUTs to /api/skills/{real-id} correctly.
  • about-settings-page.tsx: switched locale === "zh-CN" to locale.startsWith("zh") to match the rest of the codebase.

On the scope note (the PR still carrying the globals.css palette rewrite and message-bubble restyle): I've kept them here since they're UI-layer changes that don't depend on the observatory skin, and splitting them further would create churn; happy to move them to a dedicated PR if you'd prefer. This PR is currently a draft pending the #4914 base merge.

@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.

Re-review at 7f8b1f3, scoped to this PR's increment over its base #4914 (chats settings page, zh skill localization, zh about content, plus the scheduled-tasks error/empty-state fix). Both previously reported findings are verified fixed at this head — localizeSkill now keeps skill.name for identity and adds displayName, and the empty states are gated on !queryError. Three new findings below, the first one functional: the chats page's load-more-to-search affordance is unreachable exactly when it's needed (search matching nothing in the loaded pages), plus a suggestion on the hardcoded zh skill descriptions and a copy nit.

<MessagesSquare className="text-muted-foreground/50 size-8" />
<div>
<div className="text-sm font-medium">
{isSearching ? t.chats.noMatches : t.chats.emptyTitle}

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.

Functional gap: search only covers already-loaded pages, and the "Load more to search older conversations" button (lines 157-170) is rendered only inside the filteredThreads.length > 0 branch. When a query matches nothing in the loaded pages — exactly the case where older, not-yet-loaded conversations might match — this empty branch shows "No matching conversations" with no way to load more, making those conversations unreachable via search. Render the load-more row in this branch too when hasNextPage && isSearching, or hoist it out of the filteredThreads.length > 0 conditional.

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 78836eb — the load-more button is now rendered in the empty search branch when hasNextPage && isSearching.

<div className="text-muted-foreground mt-0.5 flex items-center gap-2 text-xs">
<span>
{thread.updated_at
? formatTimeAgo(thread.updated_at)

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: when updated_at is missing, the meta row renders t.pages.untitled ("Untitled") in the timestamp slot — a title string where a relative time is expected. "—" (or hiding the span) reads better.

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 78836eb — a missing updated_at now renders an em dash instead of Untitled.

if (!locale.startsWith("zh")) return skill;
const zh = SKILL_ZH[skill.name];
if (!zh) return skill;
return { ...skill, displayName: zh.name, description: zh.description };

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: localizeSkill hard-overrides description with the frozen copy in SKILL_ZH, so zh-CN users always see this hardcoded text even after the backend's skill descriptions change, while en-US users see the live description. It would also replace the description of any non-built-in skill whose name happens to collide with a map key. Consider localizing only the display name here and letting the description come from the backend (or driving both from skill metadata) so the map can't drift.

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 78836ebSKILL_ZH is now a name-only map and localizeSkill localizes only displayName; the description always comes from the backend.

@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.

Re-review at 78836eb. All previously flagged items are verified fixed at this head: localizeSkill keeps skill.name for identity and only overrides displayName, the scheduled-tasks empty states are gated on !queryError, the load-more-to-search button now renders in the empty-search branch, a missing updated_at renders an em dash, and SKILL_ZH no longer freezes the description copy. Three new findings below — the main one is that removing the sidebar Chats link leaves the existing /workspace/chats page unreachable.

</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>

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 removes the last navigation entry to /workspace/chats, but the page itself (frontend/src/app/workspace/chats/page.tsx, with its virtualized list and channel badges) is still registered at this head — I can't find any other link to it (the header, command palette, and /workspace redirect all target /workspace/chats/new or thread pages). It's now orphaned dead UI duplicating the new Chats settings page. Either delete the page (and its ChatsPage imports) or redirect it somewhere reachable, so the two surfaces can't drift apart.

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 f78c2f8: frontend/src/app/workspace/chats/page.tsx (and its ChatsPage imports) is deleted, so the Chats settings page is now the single surface for the conversation list.

</div>
) : filteredThreads.length > 0 ? (
<div className="overflow-hidden rounded-lg border">
{filteredThreads.map((thread) => {

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 renders every loaded thread as a plain DOM row, while the /workspace/chats page it replaces used VirtualThreadList for the same data. With the 50-thread pages auto-loading via the sentinel, a user who scrolls deep accumulates unbounded rows inside the dialog's ScrollArea. Rows are light so this is fine for the first few pages, but if this page is meant to fully replace the old one, consider capping auto-loading or reusing the virtualized 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.

Fixed in f78c2f8: chats-settings-page.tsx now reuses VirtualThreadList (the same virtualizer the old page used) inside the dialog ScrollArea, so deep scrolls do not accumulate unbounded DOM rows.

<div className="flex size-full flex-col">
<header className="flex shrink-0 items-center justify-between gap-3 border-b px-4 py-3">
<div className="flex items-center gap-3">
<Button

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 icon-only back button has no accessible name, so screen readers announce it as just "button". Add aria-label={t.common.back} (or the equivalent existing common key) to match the other icon buttons in the codebase.

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 f78c2f8: the icon-only back button now carries aria-label={t.common.close} (the same common key used by sibling icon buttons), so screen readers announce it.

@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.

Re-review at f78c2f8. All three findings from the last pass are verified fixed: /workspace/chats is deleted with no remaining in-app links to it (only thread-detail routes still reference the path prefix), the back button now has an accessible name via the existing t.common.close key, and the chats settings page now uses VirtualThreadList with a matching estimateSize (56 = min-h-14) and the correct scroll parent for the dialog's ScrollArea viewport. Two new findings below, both fallout from the page deletion: the e2e suite still navigates to the deleted route (CI-breaking, and it also loses the issue #3482 regression coverage since the replacement page has no testids), plus a small border seam nit.

</div>
) : filteredThreads.length > 0 ? (
<div className="overflow-hidden rounded-lg border">
<VirtualThreadList

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.

Deleting /workspace/chats orphans the e2e specs that drive it — neither file is in this PR's diff, and Playwright's testDir is ./tests/e2e, so CI will hit a 404 here: frontend/tests/e2e/thread-list-infinite-scroll.spec.ts:36 and :96 both page.goto("/workspace/chats") and additionally assert the chats-page-sentinel / chats-page-load-more testids that only existed on the deleted page, and frontend/tests/e2e/thread-history.spec.ts:751 and :789 do the same for the "chats list page shows all threads" and "IM channel threads show their source" tests. Since this settings page is the replacement surface, repointing those specs here would also keep the issue #3482 regression coverage (sentinel-driven infinite scroll, no auto-pagination while searching) — the new page currently has no testids, so that coverage silently disappears. Alternatively, keep a redirect at /workspace/chats if you'd rather leave the specs untouched.

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 9fcc2ff — both e2e specs were repointed to the replacement surface: they now goto("/workspace/settings?settings=chats") and target the new chats-settings-page / chats-settings-sentinel / chats-settings-load-more testids added to ChatsSettingsPage, preserving the issue #3482 sentinel-driven coverage.

<Link
key={thread.thread_id}
href={pathOfThread(thread)}
className="hover:bg-secondary/50 flex min-h-14 items-center gap-3 border-b px-4 transition-colors"

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 pre-virtualizer rows ended with last:border-b-0; without it every row keeps a bottom border, so the final row's border sits directly on the container's own bottom border (a 1px double seam at the bottom of the card). last: can't work reliably once the list virtualizes (the last rendered element is only the last item when scrolled to the bottom), so if the seam is noticeable the container border is the side to adjust rather than the rows.

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 9fcc2ff — with the list now virtualized, last: is unreliable, so the container's own bottom border was dropped instead; the final row's border-b now sits flush against the card edge with no double seam.

@LittleChenLiya
LittleChenLiya force-pushed the feat/frontend-extra-features branch 2 times, most recently from 798f5ea to eed2c43 Compare August 22, 2026 10:01

@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.

Re-review at eed2c43. The previous findings are verified fixed at this head: the e2e specs are repointed off the deleted route, the back button carries aria-label={t.common.close}, and the list container now uses border-x border-t so there is no double bottom seam (the dialog's ScrollArea viewport also correctly serves as the virtual list's scroll parent via closest('[data-slot="scroll-area-viewport"]')). One new finding, unfortunately CI-breaking in the same way as the one it was meant to fix: the repointed specs navigate to /workspace/settings?settings=chats, but no such route exists — the deep link only mounts via WorkspaceContent on real workspace routes — so all four repointed tests will hit Next's 404 page and time out. Anchored on both spec files below; pointing them at an existing workspace route with the query param (e.g. /workspace/chats/new?settings=chats) or adding a real /workspace/settings route resolves it.

mockLangGraphAPI(page, { threads: THREADS });

await page.goto("/workspace/chats");
await page.goto("/workspace/settings?settings=chats");

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.

CI-breaking: page.goto("/workspace/settings?settings=chats") targets a route that does not exist. frontend/src/app/workspace/ only contains page.tsx (which redirects to /workspace/chats/new), agents/, chats/[thread_id]/, and scheduled-tasks/ — there is no settings/ segment and no rewrite for it in next.config.js. Next.js serves the 404 page, so WorkspaceContent (and with it WorkspaceSettingsDeepLink, the only thing that turns ?settings= into an open dialog) never mounts; both tests in this file then time out waiting for chats-settings-page, i.e. the same failure mode the repoint was meant to remove. Navigate to an existing workspace route that mounts the deep link instead, e.g. /workspace/chats/new?settings=chats (line 96 needs the same change; the IM-channel test in thread-history.spec.ts already proves /workspace/chats/new works under mockLangGraphAPI), or add a real /workspace/settings page so the URL exists.

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 6d20129. Both chats-settings gotos in this spec now navigate to /workspace/chats/new?settings=chats so WorkspaceSettingsDeepLink can mount on an existing workspace route.

mockLangGraphAPI(page, { threads: THREADS });

await page.goto("/workspace/chats");
await page.goto("/workspace/settings?settings=chats");

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.

Same issue as thread-list-infinite-scroll.spec.ts: both goto("/workspace/settings?settings=chats") calls here (lines 751 and 789) hit a route that does not exist under frontend/src/app/workspace/, so these two tests 404 before the settings dialog can open and time out on chats-settings-page. Same fix — navigate to an existing workspace route carrying the query, e.g. /workspace/chats/new?settings=chats (the /workspace/chats/new goto at line 769 above already works under this mock setup).

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 6d20129. Both gotos here now use /workspace/chats/new?settings=chats instead of the non-existent /workspace/settings path.

…in reuse-thread e2e

The dialog overlay intercepts the create-toggle click; press Escape and await dialog dismissal first.
@LittleChenLiya
LittleChenLiya force-pushed the feat/frontend-extra-features branch from dbe679f to bd995ff Compare August 24, 2026 11:19
… cover one-time e2e

Round-trip inbound thread_id on Back/Cancel/success from /workspace/scheduled-tasks/new. Give the icon-only back button a localized aria-label. Add e2e for the once schedule year/month/day/time path and for returning to the filtered list.
@LittleChenLiya
LittleChenLiya force-pushed the feat/frontend-extra-features branch from bd995ff to 6d20129 Compare August 24, 2026 13:47
…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.
@LittleChenLiya
LittleChenLiya force-pushed the feat/frontend-extra-features branch from 6d20129 to 44fbf0d Compare August 24, 2026 14:26
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.
@LittleChenLiya
LittleChenLiya force-pushed the feat/frontend-extra-features branch from 44fbf0d to 7644a11 Compare August 25, 2026 02:49
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.
@LittleChenLiya
LittleChenLiya force-pushed the feat/frontend-extra-features branch from 7644a11 to 4ea6068 Compare August 28, 2026 05:42
# Conflicts:
#	frontend/src/components/workspace/input-box.tsx
@LittleChenLiya
LittleChenLiya force-pushed the feat/frontend-extra-features branch from 4ea6068 to 46b3551 Compare August 30, 2026 05:07
Honor an explicit context_mode when duplicating, and pass the source
task id plus sessionStorage instead of serializing the prompt into the
query string.
@LittleChenLiya
LittleChenLiya force-pushed the feat/frontend-extra-features branch from 46b3551 to f9e2029 Compare August 30, 2026 07:25
@LittleChenLiya
LittleChenLiya force-pushed the feat/frontend-extra-features branch from f9e2029 to 5de2735 Compare August 30, 2026 07:32

@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.

Re-review at 5de2735 (after the rebase onto origin/main and the observatory-skin layer being merged into this branch). All previously flagged items are verified fixed at this head: both chats-list e2e specs now navigate to /workspace/chats/new?settings=chats, which is a real route — /workspace/chats/[thread_id] matches new, and the workspace layout mounts WorkspaceContentSettingsDialogHost + WorkspaceSettingsDeepLink, so the dialog does open on the chats section (testids chats-settings-page / chats-settings-sentinel / chats-settings-load-more all exist). The earlier fixes also survived the rebase: localizeSkill keeps skill.name as identity and only overrides displayName, the scheduled-tasks empty states are still gated on !queryError, the chats list still virtualizes via VirtualThreadList, and no references remain to the deleted /workspace/chats list route. The rebase itself is clean — no conflict markers in any changed file, the restored isLoading definition is in place, and en-US/zh-CN key sets are identical. Two new findings below: a test-coverage regression (the whole describeSchedule block was dropped while the function is still rendered in the UI) and a scope note — this head now carries every commit of #4915 on top of #4914's head, so this "non-skin extras" PR is a strict superset of both sibling PRs.

weekdays: [],
dayOfMonth: 1,
} as CronParts;
describe("buildOnceRunAtLocal", () => {

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.

Coverage regression: this diff deletes the entire describe("describeSchedule") block (the once/daily/weekly en+zh, weekly-no-weekdays fallback, hourly, monthly, and custom tests) and drops describeSchedule from the imports — but the function is still exported from frontend/src/core/scheduled-tasks/cron.ts and still rendered in the UI (scheduled-task-schedule-input.tsx renders describeSchedule(...) in the schedule-preview row). Its locale branches (每天 09:00, 每周 周一、周二 …, 每小时第 N 分钟) are exactly the kind of string formatting that regresses silently without tests. Looks like rebase fallout rather than an intentional removal — please restore the block (only the buildOnceRunAtLocal suite is new here).

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 - the full describe(describeSchedule) block (once/daily/weekly en+zh, weekly-no-weekdays fallback, hourly, monthly, custom) was restored in 78dd7a5 after being dropped during the rebase onto main; the pushed branch head now includes it.

Comment thread frontend/src/core/skins/index.ts Outdated
@@ -0,0 +1,18 @@
export {

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.

Scope note: this head now contains the complete observatory skin layer — every commit of #4915, up to and including 3f8d266d, which is #4915's current head — layered on top of #4914's head (05a1fe0e). So this PR, described as the non-skin extras split out to shrink the other two, is now a strict superset of both siblings: merging any one of #4914 / #4915 / #4922 lands the other two's content and leaves them to merge empty or conflict. Either drop the observatory commits back onto #4915 (this file, core/skins/*, components/workspace/skins/*, and the obs-* globals.css block are the boundary), or update this PR's description and close the superseded PRs so it's clear which one is meant to be reviewed.

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 - the branch was rebased onto the current #4914 tip (78dd7a5) with the observatory skin commits stripped; feat/frontend-extra-features now contains only the six extra-features commits and no #4915 skin code.

…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.
…zation, zh about, scheduled-tasks loading state
…ing updated_at, allow load-more on empty search
The /workspace/chats route is gone (single surface is now the Chats settings page). Add data-testids to ChatsSettingsPage (page/sentinel/load-more), allow ?settings=chats deep link, and update the two e2e specs that drove the deleted page so issue bytedance#3482 coverage is preserved. Also drop the list container bottom border so the final row's border-b no longer forms a double seam.
/workspace/settings does not exist, so chats-list e2e was 404ing before WorkspaceSettingsDeepLink could mount. Navigate to /workspace/chats/new?settings=chats instead.
@LittleChenLiya
LittleChenLiya force-pushed the feat/frontend-extra-features branch from 5de2735 to ff0e471 Compare August 30, 2026 08:23

@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.

Re-review at ff0e471. This head is content-identical to 5de2735 for every file this PR owns (checked byte-for-byte across all nine of the PR's own files), so the rebase is clean and nothing regressed: it now sits on 78dd7a5, which puts the restored describe("describeSchedule") block in the ancestry, and the observatory-skin commits are stripped, so this is once again the non-skin extras PR. Both findings from the last pass are resolved at this head — en-US/zh-CN key sets are still identical, the e2e chats-list specs still goto /workspace/chats/new?settings=chats, localizeSkill still keeps skill.name as identity and only overrides displayName, and the scheduled-tasks empty states are still gated on !queryError. Two new functional findings in ChatsSettingsPage (the dialog stays open after clicking a conversation; dropping buildThreadListModel loses cross-page dedup and pinned-first ordering) plus one orphaned-i18n-key nit.

renderItem={(thread) => {
const channelSource = channelSourceOfThread(thread);
return (
<Link

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.

Functional bug: the rows navigate with a plain <Link> and nothing closes the settings dialog, so clicking a conversation routes to /workspace/chats/[thread_id] with the dialog still modal on top of it.

SettingsDialogHost is mounted once at the workspace root (src/app/workspace/workspace-content.tsx:44, reached through app/workspace/layout.tsx), so it survives client-side navigation, and the store in settings-dialog-store.ts has no route listener — the only close path is onOpenChange, i.e. the user dismissing it by hand. Since opening a conversation is this page's primary action, the user lands on the thread with a full-screen modal still covering it.

The sibling flows in this same PR close first: SkillSettingsPage.handleCreateSkill calls onClose?.() before router.push, and duplicateSelectedTask on the scheduled-tasks page calls setDetailOpen(false) before router.push. Suggest wiring the same here — pass onClose={() => props.onOpenChange?.(false)} at the activeSection === "chats" render site in settings-dialog.tsx (exactly as SkillSettingsPage already receives it) and call it from the row's onClick.

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 66c8db5 — ChatsSettingsPage now accepts an onClose prop, wired to props.onOpenChange?.(false) at the activeSection === "chats" render site in settings-dialog.tsx (mirroring SkillSettingsPage), and the thread row's Link calls onClose?.() before navigating, so the modal is dismissed before the thread route opens.

refetch,
} = useInfiniteThreads();
const threads = useMemo(
() => infiniteThreads?.pages.flat() ?? [],

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 reimplements what buildThreadListModel already does, and drops two behaviours the deleted /workspace/chats page got from it:

(a) No cross-page dedup. useInfiniteThreads paginates by a running offset (getInfiniteThreadsNextPageParam sums the allPages lengths) over a list ordered updated_at desc, so any thread created or updated between two page fetches shifts the window and repeats at the next page boundary. pages.flat() keeps the duplicate, and VirtualThreadList sets getItemKey: (index) => items[index]?.thread_id ?? index, so a repeat produces duplicate React keys and the conversation rendered twice in the list. buildThreadListModel's byId map exists precisely to collapse that.

(b) Pinning is ignored. The model returns sortPinnedThreads(threads), so pinned conversations float to the top; this sort is updated_at only, so a pinned thread with an older updated_at sinks below recent ones — inconsistent with recent-chat-list.tsx, which still goes through the model.

Suggest const model = useMemo(() => buildThreadListModel(infiniteThreads?.pages ?? []), [infiniteThreads]) and filtering model.threads, keeping the search filter and this sort only as a tie-break within the unpinned group.

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 66c8db5 — the page now uses buildThreadListModel(infiniteThreads?.pages ?? []): the byId map collapses cross-page duplicates (no duplicate React keys), and the model's pinned-first ordering is preserved. The updated_at sort is kept only as a tie-break within the unpinned group, so pinned conversations stay on top.

@@ -628,11 +629,17 @@ export const enUS: Translations = {

// Chats
chats: {

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: with /workspace/chats deleted in this PR, pages.chats is now unreferenced in both locales (en-US.ts:712, zh-CN.ts:683) — its only consumer was the deleted page's document.title effect. sidebar.chats was correctly removed in the same change, so this one looks like it slipped through; the Translations type will keep compiling either way, so nothing flags it.

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 66c8db5 — the unreferenced pages.chats key is removed from en-US.ts, zh-CN.ts, and the Translations type (types.ts).

- close the settings dialog when navigating to a thread from the chats list
- use buildThreadListModel so cross-page duplicates collapse and pinned
  threads stay on top; keep updated_at sort as a tie-break within the
  unpinned group
- remove the now-orphaned pages.chats i18n key (en-US, zh-CN, types)

@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 at 5d6074c. The increment over ff0e471 (chats-settings onClose wiring, buildThreadListModel reuse with pinned-first ordering, and the pages.chats key removal from en-US/zh-CN/types) is all verified in place and correct. Three new findings below, all in code outside that increment: the memory settings page no longer renders or searches stored summaries, the isMemorySummaryEmpty -> hasAnyMemorySummary swap drops a trim, and the duplicate-create page's schedule-input remount key changes on every schedule edit.

<div>
{filteredFacts.length === 0 ? (
<div className="text-muted-foreground text-sm">
{normalizedQuery ? noMatches : noFacts}

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.

Functional regression: normalizedQuery is now matched against memory.facts only, but the input above is still labelled t.settings.memory.searchPlaceholder ("Search memory") and this branch reports t.settings.memory.noMatches ("No matching memory found."). The stored summaries (workContext / personalContext / topOfMind / recentMonths / earlierContext / longTermBackground) are no longer rendered or searched anywhere — hasAnyMemorySummary is now their only consumer. On main the page rendered them via buildMemorySectionGroups + summariesToMarkdown inside SafeStreamdown and included them in the search through filteredSectionGroups, so a query that matches only a summary now produces a false "no matching memory found", and the summaries themselves are unreachable in the UI short of an export.

If dropping the summaries view is intentional, please scope the search copy to facts (e.g. "Search facts" / "No matching facts") and call the removal out in the PR description — it is well outside the scope this PR advertises. Otherwise the read-only summaries block needs restoring.

) : (
<div className="space-y-4">
{isMemorySummaryEmpty(memory) && memory.facts.length === 0 ? (
{memory.facts.length === 0 && !hasAnyMemorySummary(memory) ? (

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.

Behaviour change versus the isMemorySummaryEmpty this replaces: the old helper trimmed before comparing (summary.trim() === ""), while hasAnyMemorySummary treats a whitespace-only summary as content. A memory whose six summaries are all blank strings now skips the memoryFullyEmpty empty state and just shows "No saved facts yet." with no explanation of where the rest of the memory went.

Keeping the old semantics is a one-liner:

function hasAnyMemorySummary(memory: UserMemory): boolean {
  return [
    memory.user.workContext.summary,
    memory.user.personalContext.summary,
    memory.user.topOfMind.summary,
    memory.history.recentMonths.summary,
    memory.history.earlierContext.summary,
    memory.history.longTermBackground.summary,
  ].some((summary) => summary.trim() !== "");
}

key={
sourceTaskId
? `${sourceTaskId}:${createSchedule.schedule_type}:${JSON.stringify(createSchedule.schedule_spec)}`
: "new"

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 remount key changes on every schedule_spec edit, so the one-time date fields lose focus mid-edit in the duplicate flow. ScheduledTaskScheduleInput emits onChange from its effect on every internal change (including onceYear/onceMonth/onceDay/onceTime), and the parent stores that straight into createSchedule. Once year/month/day/time form a valid run_at, every subsequent keystroke produces a new schedule_spec JSON -> a new key -> React unmounts and remounts the input, replacing the DOM node the user is typing into. Editing a valid ...-15T09:30 to ...-15T09:31, or clearing the day (which flips the spec back to {}), drops focus after a single keystroke.

Note this only bites when sourceTaskId is set — the plain create path uses the constant "new" key and is unaffected.

initial is only read inside useState initializers, which is why the key hack exists. A cheaper form is to key on the draft identity plus an explicit nonce that applyDraft bumps once, so the remount happens exactly when a draft lands instead of on every field edit.

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