Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/changelog/en.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
- Allow up to ten minutes to verify uploaded desktop release files so slower downloads do not abort publication after a successful upload.
- The website starts downloads automatically only on macOS; other devices show a manual download page with the configured platform installers instead of downloading a potentially incompatible file.
- Windows uploads to GitHub Releases now include only the installer, omitting separate checksum and update metadata files while retaining release validation and automatic updates through R2.
- Classify ungrouped tabs in the current window into folders with your configured AI model, or classify only the tabs below a chosen tab without disturbing existing groups.
- Reuse existing tab categories in each AI classification, create new categories when needed, and add, rename, or delete categories in Settings without closing their tabs.
- Closing tabs while AI classification is running no longer discards results for the remaining tabs, including when the closed tab was outside the selected range.
- AI classification can save results when older folder files contain duplicate references, without mistaking unchanged data for a conflicting edit.

### [1.0.110] - 2026-10-12

Expand Down
4 changes: 4 additions & 0 deletions docs/changelog/zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
- 桌面发布文件上传后的校验最多等待十分钟,避免下载较慢时,上传成功却因校验超时中断发布。
- 官网仅在 macOS 上自动开始下载;其他设备显示手动下载页面,可选择已配置的各平台安装包,避免自动下载不适用的文件。
- GitHub Release 的 Windows 附件只保留安装包,不再上传单独的校验文件和更新清单;发布校验及通过 R2 自动更新的流程继续保留。
- 使用已配置的 AI 模型将当前窗口中未分类标签页整理到文件夹,也可只分类指定标签页下方的页面,保留已有分组。
- AI 每次分类都会优先复用已有分类,并按需创建新分类;可在设置中新增、重命名或删除分类,删除分类不会关闭其中的标签页。
- AI 分类期间关闭标签页,不再丢弃其他标签页的分类结果;关闭分类范围之外的标签页也不会中断分类。
- 旧版文件夹数据含有重复标签页记录时,AI 分类也能正常保存,不再把未变化的数据误判为编辑冲突。

### [1.0.110] - 2026-10-12

Expand Down
1 change: 1 addition & 0 deletions docs/feature-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ sidebar, insets + rounds the content area, and re-homes the toolbar off-screen.
| ☐ | Pinned sites/tabs grid, stable backing identity, and dormant pinned items | `src/dao/.../sidebar/dao_pinned_tabs_grid.ts`, `dao_pinned_tab_model.{h,cc}`, `dao_pinned_tab_storage.{h,cc}`, `dao_tab_identity.{h,cc}`, `dao_sidebar_ui.cc`, `sessions/session_service.cc.patch` | `DaoPinnedTabModelTest.*`, `DaoPinnedTabStorageTest.*`, `DaoSidebarBrowserTest.*Pinned*`, `DaoSidebarBrowserTest.ActivatingPinnedItemReusesTabFromAnotherWindow`, `DaoSidebarBrowserTest.ClosingPinnedItemFromAnotherWindowMakesItDormantAndReopenable`, `DaoSidebarBrowserTest.UnpinningPinnedItemFromAnotherWindowMovesItHere`, `DaoTabBrowserTest.SidebarTabIdentity*`, `pinned_tabs_grid.test.ts` | Pin and activate without changing tab count; navigate; discard/replace WebContents; replace then navigate; restore the session both before and after command compaction and preserve the backing identity; activate the same Pin from another window and focus the existing tab without increasing either window's tab count; close it remotely and synchronize dormant state across windows; drag a remote Pin into the current ordinary tab list and move the backing tab into this window; block clicks and unpin drags until restore completes; close then reopen once and restart with the same backing identity; double-click; create two identical-URL tabs and preserve exact ownership; create a legacy identity conflict and verify active tab/tab count remain unchanged; reject partially invalid migration data without dropping Pins; verify failed atomic persistence preserves the previous file; drag/reorder does not corrupt state |
| ☐ | Active sidebar tab stays visible after tab switches | `dao_sidebar_app.ts`, `dao_tab_item.ts`, `dao_folder_item.ts`, sidebar WebUI tests | — | Scroll the tab list away from a visible tab, activate it by keyboard or another surface, and verify it smoothly enters the nearest viewport edge; activate a child of a collapsed folder and verify the folder expands, persists the expanded state, and scrolls the child into view |
| ☐ | Sidebar folder model, Unfolder/Delete Folder, and configurable stale-tab archiving | `src/dao/.../sidebar/dao_folder_model.ts`, `dao_sidebar_app.ts`, `dao_sidebar_ui.cc`, Settings Dao page patches | `DaoSidebarBrowserTest.StaleTabExpirationPrefDefaultsTo24Hours`, `DaoSidebarBrowserTest.DeleteFolderUsesDaoNativeDialog`, `DaoTabBrowserTest.CommandWCloseSkipsStaleFolderUnlessClosingStaleTab`, `sidebar_app.test.ts`, `folder_model.test.ts`, `dao_page_test.ts` | "You and Dao" accepts only integer hours from 1–720 and defaults to 24; invalid input is not saved; "Move Stale Tabs to stale" reads the current profile value, creates/updates the `stale` folder only for qualifying ordinary tabs, expands it, and persists folder membership. Closing an ordinary active tab skips archived tabs only while another ordinary tab remains; if all remaining tabs are in `stale`, or the closed tab was already there, preserve Chromium's normal selection and expand `stale` to the active tab. Every folder, including `stale`, exposes the same context menu: Unfolder removes the folder and releases its child tabs in place without closing tabs; Delete Folder opens the Dao native confirmation dialog. Cancel preserves tabs/folder; confirmation closes only current matched children and persists deletion only after all of them actually close; cancelling a page's beforeunload prompt keeps the folder and remaining tabs. Verify ordinary, stale, empty, missing, renamed, changed-membership, ordered-child, and cancelled-close cases. |
| ☐ | AI tab classification and category management | `dao_sidebar_ui.*`, `dao_agent_settings_handler.*`, sidebar/Agent `dao_tab_classification.ts`, `dao_folder_settings_handler.*`, `dao_folder_storage.*`, Settings Dao page patches | `tab_classification.test.ts`, `sidebar_app.test.ts`, Agent `dao_tab_classification.test.ts`, `folder_settings.test.ts`, `DaoFolderStorageTest.*`, `DaoAgentSettingsHandlerTest.ClassificationUsesOnlyActiveProviderKey`, `dao_page_test.ts` | Configure an AI model and check both menus; remove the active key and verify classification disappears. Incognito/Guest never classify. Use a visually reordered list: Below excludes the anchor and only groups loose HTTP(S) tabs below it in the current window. Keep existing folders, Pins, split groups, controlled tabs, and the active page unchanged. Confirm every request includes existing category IDs/names and only selected titles/URLs; reuse names and create new categories as needed. During a request close, navigate, move, pin, group or reorder tabs, rename/delete a destination, edit another window, and change AI settings: stale results cannot override edits or revive deleted folders. Close a tab above the Below range or one of several selected tabs: surviving selected tabs still classify, the closed tab stays absent, and manual reordering still discards stale results. Repeat with the closed active tab activating a child in a collapsed folder: automatic expansion must not cancel classification. Classify immediately after loading a legacy snapshot with repeated tab IDs: unchanged normalized data saves successfully, while unreadable files and newer edits remain protected. Invalid/partial/duplicate output, unavailable Agent, timeouts and failed writes preserve state, with localized native toast feedback. In You and Dao add/rename/delete categories, reject blank/duplicate/overlong names, and verify deletion releases all windows' children without closing them. Check cross-window live refresh, empty/read-error states, keyboard labels, English/Chinese, restart persistence, and unreadable storage. Test request limits and ensure oversized work does not partially apply. |
| ☐ | Sidebar utility controls: downloads, media controls, update button | `dao_download_button.ts`, `dao_download_hover_details.*`, `dao_tab_tooltip_view.*`, `dao_media_control.ts`, `dao_update_button.ts`, `dao_sidebar_ui.cc` | — | Start a download and verify a blue icon pops from the pointer, throws upward along a curved path into the actual download button, shrinks/fades, then pulses the button. Tiny downloads still animate; consecutive downloads retrigger feedback; only the active originating window animates. Collapsing the sidebar uses a localized Dao toast; reduced motion only highlights. Restored/transient downloads stay quiet. Active download rows retain progress/cancel behavior; a settled 400ms hover shows a native popup beyond the sidebar boundary with full filename, known transferred/total size and percentage, positive speed, and reliable remaining time. Long filenames wrap without truncation; hovering the lowest row flips the popup above its anchor and keeps all lines inside the browser window. Moving resets the delay; leaving, cancelling, or removing the download hides it. Unknown totals degrade to transferred size only; unavailable speed/time are omitted. After the latest download completes, verify its filename label remains without a timeout, long names truncate safely, clicking the label opens the file and dismisses it, dragging the label copies the actual file to the desktop or a folder and attaches it in applications that accept files without opening it (also check a custom save location, a cancelled drag, fast outward drags, and drags starting at the label edge); holding the left button across the hover boundary preserves the label until dragging starts or the button is released, and losing focus clears a pending dismissal, clicking the localized close button dismisses without opening, hovering opens the existing list, moving from the label into that list preserves it, and leaving their shared hover area dismisses it. Verify light/dark themes and confirm the recent-files popup still works. Tab media play/pause works; ready update state appears and `applyReadyUpdate` is invoked |
| ☐ | Folder persistence across multiple windows and restart | `dao_folder_storage.*`, `dao_sidebar_app.ts`, `dao_folder_model.ts`, `scripts/checks/folder-persistence.mjs` | `folder_model.test.ts`, `sidebar_app.test.ts`, `tab_list.test.ts` | Create distinct groups in two windows, including duplicate URLs; edit both from stale snapshots and verify neither overwrites the other. Reorder loose tabs, split tabs, folders, and folder children with hidden foreign references before/between them. Race unfolder against moving or inserting a child in both save orders; each stable tab ID must occur once, and stale moves must not resurrect closed tabs. Close a tab explicitly, transfer a tab, reload the sidebar, and restart with session restore; preserve the remaining groups and stable tab identities throughout, including newly created ordinary tabs before session command compaction. Partial restore and window teardown must not prune references. Verify corrupt/unreadable files are not overwritten and imported folders preserve concurrent edits. Migrate legacy window snapshots without losing folders or duplicate-URL identities; merely loading must not rewrite the file. Incognito folders survive sidebar reload in memory and never read or write the regular profile file. Foreign-window drops onto folders are rejected. |
| ☐ | Sidebar close/reorder motion | `src/dao/.../sidebar/dao_flip_motion.ts`, sidebar WebUI tests | — | Closing, moving, pinning, and foldering tabs animates surviving rows without duplicate placeholders or stale transforms |
Expand Down
31 changes: 31 additions & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,37 @@ An Arc-inspired vertical sidebar replaces Chromium's top tab strip — the singl
before closing the folder's currently matched child tabs. The folder is
deleted only after those tabs actually close; cancelling either confirmation
or a page's beforeunload prompt preserves the folder and remaining tabs.
- **AI tab classification** — Once an Agent model is configured, the sidebar
background and ordinary-tab context menus expose **Classify Tabs with AI**.
The tab menu also offers **Classify Tabs Below with AI**, using sidebar visual
order and excluding the clicked tab. Both actions group only loose HTTP(S)
tabs in the current window; pinned tabs, split panes, existing folder members,
and Agent/MCP-controlled tabs are preserved. Off-the-record and Guest windows
do not expose classification. Each request sends the chosen tabs' titles and
URLs plus the profile's existing folder names and IDs to the configured model,
without page content, chat history, or browser tools. Existing categories are
preferred; new folders are created only when needed, in the interface language.
Categories are the existing folders, with no separate category database or
pre-created empty folders. **You and Dao > Tab categories** lists all of them
and supports adding, renaming, and deleting categories. Deleting a category
there releases its tabs in place across windows without closing pages; the
sidebar's separate **Delete Folder** action retains its close-confirmation flow.
Names must be nonblank, unique without regard to case, and at most 100 Unicode
codepoints. Changes persist through the existing profile folder file and
notify both Settings and all same-profile sidebars.
Classification uses the resident Agent's model adapter without opening a chat,
permits one request per profile, and aborts after 60 seconds. Invalid, incomplete,
or duplicate assignments fail without changing folders. Closed, transferred,
navigated, pinned, or newly grouped tabs are excluded when results are applied;
manual edits or reordered snapshots can discard the result with retry feedback.
Closing a tab, including one outside the chosen range, does not discard the
classification of surviving tabs, even when the newly active tab automatically
expands its folder. The final atomic write compares normalized
snapshots so unchanged legacy data remains writable, and rejects newer folder
data rather than overwriting it. Classification does not activate or navigate
tabs, and uses Dao native toasts for progress, completion, empty scope, and
failure. Requests exceeding
1,000 tabs, 500 existing folders, or 512 KiB fail without partial classification.
- **Configurable stale-tab expiration** — The "You and Dao" Settings page
stores a profile-scoped integer from 1–720 hours (24 hours by default);
the sidebar's "Move Stale Tabs to stale" action reads that value when run,
Expand Down
106 changes: 106 additions & 0 deletions scripts/commands/__tests__/folder_settings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {readFileSync} from 'node:fs';
import vm from 'node:vm';

import ts from 'typescript';
import {describe, expect, it, vi} from 'vitest';

function createPage() {
const patch = readFileSync(
'src/patches/chrome/browser/resources/settings/dao_page/dao_page.ts.patch',
'utf8');
const source = patch.split('\n')
.filter(line => line.startsWith('+') && !line.startsWith('+++'))
.map(line => line.slice(1)).join('\n')
.replace(/^import[\s\S]*?;\n/gm, '')
.replace(/^export /gm, '');
const output = ts.transpileModule(source, {
compilerOptions: {target: ts.ScriptTarget.ES2022},
}).outputText;
const Page = vm.runInNewContext(output + '; SettingsDaoPageElement', {
PolymerElement: class {},
WebUiListenerMixin: (base: unknown) => base,
PrefsMixin: (base: unknown) => base,
SiteSettingsBrowserProxyImpl: {getInstance: () => ({})},
customElements: {define: () => {}},
loadTimeData: {getString: (key: string) => key},
});
const page = new Page();
page.daoTabCategories_ = [{id: 'work', name: 'Work'}];
page.daoTabCategoryName_ = '';
page.daoTabCategoryEditingId_ = '';
page.daoTabCategoriesBusy_ = false;
page.daoTabCategoriesLoaded_ = true;
page.daoTabCategoryError_ = '';
page.daoFolderBrowserProxy_ = {
getCategories: vi.fn().mockResolvedValue(page.daoTabCategories_),
editCategory: vi.fn().mockResolvedValue(page.daoTabCategories_),
};
return page;
}

describe('folder settings behavior', () => {
it('rejects blank and oversized names before sending a mutation', () => {
const page = createPage();
for (const name of ['', ' \t ', 'x'.repeat(101)]) {
page.daoTabCategoryName_ = name;
page.onDaoTabCategorySave_();
expect(page.daoTabCategoryError_).toBe('daoTabCategoriesEditError');
}
expect(page.daoFolderBrowserProxy_.editCategory).not.toHaveBeenCalled();
});

it('trims names and counts Unicode codepoints rather than UTF-16 units', () => {
const page = createPage();
page.daoTabCategoryName_ = ' ' + '🌍'.repeat(100) + ' ';
page.onDaoTabCategorySave_();
expect(page.daoFolderBrowserProxy_.editCategory).toHaveBeenCalledWith(
'add', '', '🌍'.repeat(100));
});

it('keeps the editor and previous categories after a failed save', async () => {
const page = createPage();
page.daoTabCategoryName_ = 'Research';
page.daoTabCategoryEditingId_ = 'work';
page.daoFolderBrowserProxy_.editCategory.mockRejectedValueOnce(new Error());
await page.editDaoTabCategory_('rename', 'work', 'Research');
expect(page.daoTabCategoryName_).toBe('Research');
expect(page.daoTabCategoryEditingId_).toBe('work');
expect(page.daoTabCategories_).toEqual([{id: 'work', name: 'Work'}]);
expect(page.daoTabCategoryError_).toBe('daoTabCategoriesEditError');
expect(page.daoTabCategoriesBusy_).toBe(false);
});

it('disables mutations after an unreadable load and permits retry', async () => {
const page = createPage();
page.daoFolderBrowserProxy_.getCategories.mockRejectedValueOnce(new Error());
await page.refreshDaoTabCategories_();
expect(page.daoTabCategoriesLoaded_).toBe(false);
await page.editDaoTabCategory_('remove', 'work', '');
expect(page.daoFolderBrowserProxy_.editCategory).not.toHaveBeenCalled();
await page.refreshDaoTabCategories_();
expect(page.daoTabCategoriesLoaded_).toBe(true);
expect(page.daoTabCategoryError_).toBe('');
});

it('ignores a read begun before a newer edit', async () => {
const page = createPage();
let resolveRead!: (value: unknown[]) => void;
page.daoFolderBrowserProxy_.getCategories.mockReturnValueOnce(
new Promise(resolve => resolveRead = resolve));
const loading = page.refreshDaoTabCategories_();
page.daoFolderBrowserProxy_.editCategory.mockResolvedValueOnce([
{id: 'work', name: 'Research'},
]);
await page.editDaoTabCategory_('rename', 'work', 'Research');
resolveRead([{id: 'work', name: 'Old'}]);
await loading;
expect(page.daoTabCategories_).toEqual([{id: 'work', name: 'Research'}]);
});

it('sends only the selected folder identity when removing a category', () => {
const page = createPage();
page.onDaoTabCategoryRemove_({currentTarget: {dataset: {id: 'work'}}});
expect(page.daoFolderBrowserProxy_.editCategory).toHaveBeenCalledWith(
'remove', 'work', '');
});
});
Loading
Loading