Workspaces - phase 1 (#36059)

* Introduce a workspace service for serializing open windows/tabs.

---------

Co-authored-by: Brian Johnson <34129+bridiver@users.noreply.github.com>
This commit is contained in:
Brian Clifton
2026-05-20 09:14:00 -07:00
committed by GitHub
co-authored by Brian Johnson
parent d44a3bff68
commit b349cfc012
25 changed files with 1260 additions and 0 deletions
+4
View File
@@ -154,6 +154,10 @@
#define IDC_TOGGLE_FOCUS_MODE 56457
#define IDC_NEW_TEMPORARY_CONTAINER 56458
// Workspaces
#define IDC_SAVE_WORKSPACE 56459
#define IDC_OPEN_WORKSPACE 56460
#define IDC_BRAVE_COMMANDS_LAST 57000
#endif // BRAVE_APP_BRAVE_COMMAND_IDS_H_
+18
View File
@@ -253,6 +253,24 @@
Email Aliases
</message>
<!-- Workspaces -->
<if expr="use_titlecase">
<message name="IDS_SAVE_WORKSPACE" desc="App menu item to save all open tabs and tab groups as a named workspace">
Save Tabs as Workspace…
</message>
<message name="IDS_OPEN_WORKSPACE" desc="App menu item to open a previously saved workspace">
Open Workspace…
</message>
</if>
<if expr="not use_titlecase">
<message name="IDS_SAVE_WORKSPACE" desc="App menu item to save all open tabs and tab groups as a named workspace">
Save tabs as workspace…
</message>
<message name="IDS_OPEN_WORKSPACE" desc="App menu item to open a previously saved workspace">
Open workspace…
</message>
</if>
<!-- Widevine -->
<message name="IDS_WIDEVINE_PERMISSION_REQUEST_TEXT_FRAGMENT" desc="Text fragment for Widevine permission request. 'Widevine' is the name of a plugin and should not be translated.">
Install and run Google Widevine
+2
View File
@@ -243,6 +243,8 @@ EXISTING_TRANSLATIONS = {
"IDC_SHOW_BRAVE_WALLET": "IDS_SHOW_BRAVE_WALLET",
"IDC_SHOW_BRAVE_WEBCOMPAT_REPORTER": "IDS_SHOW_BRAVE_WEBCOMPAT_REPORTER",
"IDC_SHOW_EMAIL_ALIASES": "IDS_SHOW_EMAIL_ALIASES",
"IDC_SAVE_WORKSPACE": "IDS_SAVE_WORKSPACE",
"IDC_OPEN_WORKSPACE": "IDS_OPEN_WORKSPACE",
"IDC_WINDOW_CLOSE_TABS_TO_LEFT": "IDS_TAB_CXMENU_CLOSETABSTOLEFT",
"IDC_DEV_TOOLS_DEVICES": "IDS_DEV_TOOLS_DEVICES", # &amp;Inspect devices
"IDC_DEV_TOOLS": "IDS_DEV_TOOLS", # &amp;Developer tools
+15
View File
@@ -81,6 +81,7 @@
#include "chrome/browser/flags/android/chrome_feature_list.h"
#else
#include "brave/browser/ui/views/tabs/switches.h"
#include "brave/browser/workspaces/features.h"
#include "brave/components/commander/common/features.h"
#include "brave/components/commands/common/features.h"
#endif
@@ -795,6 +796,19 @@ constexpr flags_ui::FeatureEntry::Choice kVerticalTabCollapseDelayChoices[] = {
FEATURE_VALUE_TYPE(email_aliases::features::kEmailAliases), \
}))
#if !BUILDFLAG(IS_ANDROID)
#define BRAVE_WORKSPACE_FEATURE_ENTRY \
EXPAND_FEATURE_ENTRIES({ \
"brave-workspace", \
"Brave Workspace", \
"Save sets of windows and tabs into loadable workspaces.", \
kOsWin | kOsLinux | kOsMac, \
FEATURE_VALUE_TYPE(features::kWorkspaces), \
})
#else
#define BRAVE_WORKSPACE_FEATURE_ENTRY
#endif
// Keep the last item empty.
#define LAST_BRAVE_FEATURE_ENTRIES_ITEM
@@ -1379,6 +1393,7 @@ constexpr flags_ui::FeatureEntry::Choice kVerticalTabCollapseDelayChoices[] = {
PSST_FEATURE_ENTRIES \
BRAVE_FORCE_POPUP_TO_BE_OPENED_IN_NEW_TAB_FEATURE_ENTRY \
EMAIL_ALIASES_FEATURE_ENTRIES \
BRAVE_WORKSPACE_FEATURE_ENTRY \
EXPAND_FEATURE_ENTRIES({ \
"brave-origin", \
"Enable Brave Origin", \
@@ -68,6 +68,7 @@
#include "brave/browser/ui/bookmark/bookmark_prefs_service_factory.h"
#include "brave/browser/ui/commands/accelerator_service_factory.h"
#include "brave/browser/ui/tabs/shared_pinned_tab_service_factory.h"
#include "brave/browser/workspaces/workspace_service_factory.h"
#include "brave/components/commands/common/features.h"
#include "chrome/browser/ui/tabs/features.h"
#else
@@ -254,6 +255,10 @@ void EnsureBrowserContextKeyedServiceFactoriesBuilt() {
#endif // BUILDFLAG(ENABLE_PSST)
serp_metrics::SerpMetricsServiceFactory::GetInstance();
#if !BUILDFLAG(IS_ANDROID)
WorkspaceServiceFactory::GetInstance();
#endif
}
} // namespace brave
+2
View File
@@ -417,6 +417,8 @@ if (!is_android) {
"//brave/browser/ui/webui/brave_new_tab_page_refresh",
"//brave/browser/ui/webui/brave_new_tab_page_refresh:mojom",
"//brave/browser/ui/webui/brave_welcome_page",
"//brave/browser/workspaces",
"//brave/browser/workspaces:impl",
"//brave/ui/webui/brave_color_change_listener",
"//chrome/browser/regional_capabilities",
"//chrome/browser/ui/thumbnails",
+1
View File
@@ -412,6 +412,7 @@ source_set("ui") {
"//brave/browser/ui/tabs:tab_strip",
"//brave/browser/ui/webui/brave_welcome_page",
"//brave/browser/ui/webui/settings/brave_account",
"//brave/browser/workspaces",
"//brave/common",
"//brave/components/brave_account",
"//brave/components/brave_account:features",
@@ -20,6 +20,9 @@
#include "brave/browser/ui/browser_commands.h"
#include "brave/browser/ui/focus_mode/focus_mode_utils.h"
#include "brave/browser/ui/sidebar/sidebar_utils.h"
#include "brave/browser/workspaces/features.h"
#include "brave/browser/workspaces/workspace_service.h"
#include "brave/browser/workspaces/workspace_service_factory.h"
#include "brave/components/ai_chat/core/common/buildflags/buildflags.h"
#include "brave/components/brave_news/common/buildflags/buildflags.h"
#include "brave/components/brave_rewards/core/rewards_util.h"
@@ -369,6 +372,17 @@ void BraveBrowserCommandController::InitBraveCommandState() {
ContainersServiceFactory::GetForProfile(browser_->profile()));
#endif
// Reload options if person has an update in workspaces
if (base::FeatureList::IsEnabled(features::kWorkspaces) &&
browser_->is_type_normal()) {
UpdateCommandForWorkspace();
pref_change_registrar_.Add(
kWorkspacesMetadataPref,
base::BindRepeating(
&BraveBrowserCommandController::UpdateCommandForWorkspace,
base::Unretained(this)));
}
if (browser_->is_type_normal()) {
// Delete these when upstream enables by default.
UpdateCommandEnabled(IDC_READING_LIST_MENU, true);
@@ -545,6 +559,17 @@ void BraveBrowserCommandController::UpdateCommandForSplitView() {
}
}
void BraveBrowserCommandController::UpdateCommandForWorkspace() {
auto* service = WorkspaceServiceFactory::GetForProfile(browser_->profile());
if (!service) {
return;
}
UpdateCommandEnabled(IDC_SAVE_WORKSPACE, true);
UpdateCommandEnabled(IDC_OPEN_WORKSPACE, !service->ListWorkspaces().empty());
}
void BraveBrowserCommandController::UpdateCommandForBraveSync() {
UpdateCommandEnabled(IDC_SHOW_BRAVE_SYNC, true);
}
@@ -818,6 +843,18 @@ bool BraveBrowserCommandController::ExecuteBraveCommandWithDisposition(
case IDC_TOGGLE_FOCUS_MODE:
brave::ToggleFocusMode(base::to_address(browser_));
break;
case IDC_SAVE_WORKSPACE:
if (auto* svc =
WorkspaceServiceFactory::GetForProfile(browser_->profile())) {
svc->ShowSaveWorkspaceDialog();
}
break;
case IDC_OPEN_WORKSPACE:
if (auto* svc =
WorkspaceServiceFactory::GetForProfile(browser_->profile())) {
svc->ShowOpenWorkspaceDialog();
}
break;
default:
LOG(WARNING) << "Received Unimplemented Command: " << id;
break;
@@ -104,6 +104,7 @@ class BraveBrowserCommandController : public chrome::BrowserCommandController
void UpdateCommandsForPin();
void UpdateCommandForFocusMode();
void UpdateCommandForSplitView();
void UpdateCommandForWorkspace();
bool ExecuteBraveCommandWithDisposition(int id,
WindowOpenDisposition disposition,
@@ -307,6 +307,25 @@ void BraveAppMenuModel::BuildMoreToolsSubMenu() {
need_separator = false;
}
// Save/Open workspaces
if (IsCommandIdEnabled(IDC_SAVE_WORKSPACE)) {
more_tools_menu_model->InsertItemWithStringIdAt(
next_target_index++, IDC_SAVE_WORKSPACE, IDS_SAVE_WORKSPACE);
need_separator = true;
}
if (IsCommandIdEnabled(IDC_OPEN_WORKSPACE)) {
more_tools_menu_model->InsertItemWithStringIdAt(
next_target_index++, IDC_OPEN_WORKSPACE, IDS_OPEN_WORKSPACE);
need_separator = true;
}
if (need_separator) {
more_tools_menu_model->InsertSeparatorAt(next_target_index++,
ui::NORMAL_SEPARATOR);
need_separator = false;
}
if (!browser()->profile()->IsOffTheRecord()) {
if (auto index =
more_tools_menu_model->GetIndexOfCommandId(IDC_NAME_WINDOW)) {
+61
View File
@@ -0,0 +1,61 @@
# Copyright (c) 2026 The Brave Authors. All rights reserved.
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at https://mozilla.org/MPL/2.0/.
source_set("impl") {
visibility = [
"//brave/browser/workspaces:*",
"//chrome/browser",
]
sources = [
"features.cc",
"workspace_service.cc",
"workspace_service_factory.cc",
"workspace_session_utils.cc",
"workspace_utils.cc",
]
deps = [
":workspaces",
"//base",
"//chrome/browser/ui",
"//chrome/browser/ui/tabs:tab_strip",
]
}
source_set("workspaces") {
sources = [
"features.h",
"pref_names.h",
"workspace_metadata.h",
"workspace_service.h",
"workspace_service_factory.h",
"workspace_session_utils.h",
"workspace_utils.h",
]
public_deps = [
"//base",
"//chrome/browser/profiles:profile",
"//components/keyed_service/core",
"//components/pref_registry",
"//components/sessions",
]
}
source_set("unit_tests") {
testonly = true
sources = [ "workspace_service_unittest.cc" ]
deps = [
":impl",
":workspaces",
"//base",
"//base/test:test_support",
"//chrome/test:test_support",
"//components/sessions",
"//components/sync_preferences:test_support",
"//content/test:test_support",
"//testing/gtest",
]
}
+15
View File
@@ -0,0 +1,15 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#include "brave/browser/workspaces/features.h"
namespace features {
// Allows saving open tabs and windows as a named workspace for later restore.
// NOTE: Being implemented in phases.
// See https://github.com/brave/brave-browser/issues/54738
BASE_FEATURE(kWorkspaces, base::FEATURE_DISABLED_BY_DEFAULT);
} // namespace features
+17
View File
@@ -0,0 +1,17 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#ifndef BRAVE_BROWSER_WORKSPACES_FEATURES_H_
#define BRAVE_BROWSER_WORKSPACES_FEATURES_H_
#include "base/feature_list.h"
namespace features {
BASE_DECLARE_FEATURE(kWorkspaces);
} // namespace features
#endif // BRAVE_BROWSER_WORKSPACES_FEATURES_H_
+12
View File
@@ -0,0 +1,12 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#ifndef BRAVE_BROWSER_WORKSPACES_PREF_NAMES_H_
#define BRAVE_BROWSER_WORKSPACES_PREF_NAMES_H_
// Profile preference key — stores a dict keyed by hash of the display name.
inline constexpr char kWorkspacesMetadataPref[] = "brave.workspaces.metadata";
#endif // BRAVE_BROWSER_WORKSPACES_PREF_NAMES_H_
+22
View File
@@ -0,0 +1,22 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#ifndef BRAVE_BROWSER_WORKSPACES_WORKSPACE_METADATA_H_
#define BRAVE_BROWSER_WORKSPACES_WORKSPACE_METADATA_H_
#include <string>
#include "base/time/time.h"
// Lightweight summary returned by ListWorkspaces(), used to populate UI.
// The full session state is stored as Chromium session commands on disk.
struct WorkspaceMetadata {
std::string name;
base::Time modified_at;
int number_of_windows = 0;
int number_of_tabs = 0;
};
#endif // BRAVE_BROWSER_WORKSPACES_WORKSPACE_METADATA_H_
+141
View File
@@ -0,0 +1,141 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#include "brave/browser/workspaces/workspace_service.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/functional/bind.h"
#include "base/hash/hash.h"
#include "base/logging.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "brave/browser/workspaces/workspace_session_utils.h"
#include "brave/browser/workspaces/workspace_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/sessions/core/session_id.h"
#include "third_party/abseil-cpp/absl/strings/str_format.h"
namespace {
std::string ComputeKey(const std::string& name) {
return absl::StrFormat("%08x", base::PersistentHash(name));
}
} // namespace
WorkspaceService::WorkspaceService(Profile& profile)
: profile_(profile),
workspaces_path_(profile.GetPath().AppendASCII("workspaces")),
pref_service_(*profile.GetPrefs()),
io_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::USER_VISIBLE,
base::TaskShutdownBehavior::BLOCK_SHUTDOWN})) {}
WorkspaceService::~WorkspaceService() = default;
std::vector<WorkspaceMetadata> WorkspaceService::ListWorkspaces() const {
return ListWorkspacesFromDict(
pref_service_->GetDict(kWorkspacesMetadataPref));
}
void WorkspaceService::SaveWorkspaceMetadata(const WorkspaceMetadata& meta) {
ScopedDictPrefUpdate updated(*pref_service_, kWorkspacesMetadataPref);
updated->Set(ComputeKey(meta.name), WorkspaceMetadataToDictEntry(meta));
}
void WorkspaceService::RemoveWorkspaceMetadata(const std::string& name) {
ScopedDictPrefUpdate updated(*pref_service_, kWorkspacesMetadataPref);
updated->Remove(ComputeKey(name));
}
base::FilePath WorkspaceService::GetWorkspacePathForName(
const std::string& name) const {
return workspaces_path_.AppendASCII(ComputeKey(name));
}
void WorkspaceService::SaveWorkspace(const std::string& name) {
if (name.empty()) {
return;
}
// Collect session commands on the UI thread, then write to disk on a
// background task (WriteWorkspaceToDisk does blocking file I/O).
WorkspaceMetadata workspace{.name = name, .modified_at = base::Time::Now()};
std::vector<std::unique_ptr<sessions::SessionCommand>> commands =
GenerateBrowserSessionCommandsForWorkspace(base::to_address(profile_),
workspace);
base::FilePath workspace_path = GetWorkspacePathForName(name);
auto backend = base::MakeRefCounted<sessions::CommandStorageBackend>(
io_task_runner_, workspace_path, kWorkspaceSessionType,
/*encryptor=*/std::nullopt);
// Save metadata optimistically now; roll it back if the write fails.
// BindPostTask ensures on_error always runs on the UI thread whether it is
// invoked by AppendCommands or directly by WriteWorkspaceToDisk on a
// directory-creation failure.
SaveWorkspaceMetadata(workspace);
// "Rolling back" is just removing the metadata from the dictionary.
auto on_error = base::BindPostTask(
base::SequencedTaskRunner::GetCurrentDefault(),
base::BindOnce(&WorkspaceService::RemoveWorkspaceMetadata, GetWeakPtr(),
name));
io_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&WriteWorkspaceToDisk, std::move(commands),
std::move(workspace_path), std::move(backend),
std::move(on_error)));
}
void WorkspaceService::RestoreWorkspace(const std::string& name) {
base::FilePath path = GetWorkspacePathForName(name);
auto backend = base::MakeRefCounted<sessions::CommandStorageBackend>(
io_task_runner_, path, kWorkspaceSessionType,
/*encryptor=*/std::nullopt);
io_task_runner_->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(&ReadWorkspaceFromDisk, std::move(path),
std::move(backend)),
base::BindOnce(&WorkspaceService::DoRestoreWorkspace, GetWeakPtr()));
}
void WorkspaceService::ShowSaveWorkspaceDialog() {
// TODO(https://github.com/brave/brave-browser/issues/55108)
SaveWorkspace("example-workspace");
}
void WorkspaceService::ShowOpenWorkspaceDialog() {
// TODO(https://github.com/brave/brave-browser/issues/55108)
RestoreWorkspace("example-workspace");
}
void WorkspaceService::DoRestoreWorkspace(
std::vector<std::unique_ptr<sessions::SessionCommand>> commands) {
if (commands.empty()) {
DVLOG(1) << "Could not load workspace: no commands";
return;
}
RestoreBrowserSessionCommandsForWorkspace(base::to_address(profile_),
std::move(commands));
}
base::WeakPtr<WorkspaceService> WorkspaceService::GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
void WorkspaceService::Shutdown() {
weak_ptr_factory_.InvalidateWeakPtrs();
}
+100
View File
@@ -0,0 +1,100 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#ifndef BRAVE_BROWSER_WORKSPACES_WORKSPACE_SERVICE_H_
#define BRAVE_BROWSER_WORKSPACES_WORKSPACE_SERVICE_H_
#include <memory>
#include <string>
#include <vector>
#include "base/files/file_path.h"
#include "base/memory/raw_ref.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/task/sequenced_task_runner.h"
#include "brave/browser/workspaces/pref_names.h"
#include "brave/browser/workspaces/workspace_metadata.h"
#include "components/keyed_service/core/keyed_service.h"
#include "components/sessions/core/session_command.h"
class PrefService;
class Profile;
// Per-profile service that manages saving and restoring named workspaces.
//
// Each workspace is stored in its own subdirectory under
// {profile_dir}/workspaces/{hashed_name}/
//
// Inside that subdirectory:
// Sessions/Session_* — full browser state in Chromium's session-command
// binary format (handled by CommandStorageBackend)
//
// Workspace metadata (display name, window/tab counts, creation time) is
// stored in a profile preference rather than a per-directory info.json file.
//
// Threading note: All workspace file I/O runs on |io_task_runner_|, a
// MayBlock SequencedTaskRunner created at construction time. Backends are
// constructed on the UI thread with |io_task_runner_| as their owning
// sequence, then all blocking I/O tasks are posted to that same runner.
class WorkspaceService : public KeyedService {
public:
explicit WorkspaceService(Profile& profile);
~WorkspaceService() override;
WorkspaceService(const WorkspaceService&) = delete;
WorkspaceService& operator=(const WorkspaceService&) = delete;
// Returns summary information for all saved workspaces, sorted by time
// modified (most-recent first). Reads from the profile preference; no disk
// I/O.
std::vector<WorkspaceMetadata> ListWorkspaces() const;
// Writes workspace metadata into the profile preference. Called on the UI
// thread after a successful WriteWorkspaceToDisk background task.
void SaveWorkspaceMetadata(const WorkspaceMetadata& meta);
// Removes the workspace metadata entry from the profile preference.
void RemoveWorkspaceMetadata(const std::string& name);
// Returns the directory that contains all workspace subdirectories.
const base::FilePath& GetWorkspacesPath() const { return workspaces_path_; }
// Returns the per-workspace subdirectory for |name|.
base::FilePath GetWorkspacePathForName(const std::string& name) const;
// ---- Browser-state commands (UI thread only) ----------------------------
// Serializes all open windows/tabs for this profile and writes them to disk
// under the given workspace name.
void SaveWorkspace(const std::string& name);
// Reads the named workspace from disk and opens its windows/tabs.
void RestoreWorkspace(const std::string& name);
// Placeholder entry points for the save/open dialogs.
void ShowSaveWorkspaceDialog();
void ShowOpenWorkspaceDialog();
base::WeakPtr<WorkspaceService> GetWeakPtr();
// KeyedService:
void Shutdown() override;
private:
// Called on the UI thread with the commands read from disk by
// RestoreWorkspace.
void DoRestoreWorkspace(
std::vector<std::unique_ptr<sessions::SessionCommand>> commands);
raw_ref<Profile> profile_;
const base::FilePath workspaces_path_;
raw_ref<PrefService> pref_service_;
scoped_refptr<base::SequencedTaskRunner> io_task_runner_;
base::WeakPtrFactory<WorkspaceService> weak_ptr_factory_{this};
};
#endif // BRAVE_BROWSER_WORKSPACES_WORKSPACE_SERVICE_H_
@@ -0,0 +1,53 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#include "brave/browser/workspaces/workspace_service_factory.h"
#include <memory>
#include "brave/browser/workspaces/features.h"
#include "brave/browser/workspaces/pref_names.h"
#include "brave/browser/workspaces/workspace_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_selections.h"
#include "components/pref_registry/pref_registry_syncable.h"
// static
WorkspaceServiceFactory* WorkspaceServiceFactory::GetInstance() {
static base::NoDestructor<WorkspaceServiceFactory> instance;
return instance.get();
}
// static
WorkspaceService* WorkspaceServiceFactory::GetForProfile(Profile* profile) {
return static_cast<WorkspaceService*>(
GetInstance()->GetServiceForBrowserContext(profile, /*create=*/true));
}
WorkspaceServiceFactory::WorkspaceServiceFactory()
: ProfileKeyedServiceFactory("WorkspaceService",
ProfileSelections::BuildForRegularProfile()) {}
WorkspaceServiceFactory::~WorkspaceServiceFactory() = default;
std::unique_ptr<KeyedService>
WorkspaceServiceFactory::BuildServiceInstanceForBrowserContext(
content::BrowserContext* context) const {
if (!base::FeatureList::IsEnabled(features::kWorkspaces)) {
return nullptr;
}
Profile* profile = Profile::FromBrowserContext(context);
return std::make_unique<WorkspaceService>(*profile);
}
void WorkspaceServiceFactory::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterDictionaryPref(kWorkspacesMetadataPref);
}
bool WorkspaceServiceFactory::ServiceIsCreatedWithBrowserContext() const {
return true;
}
@@ -0,0 +1,41 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#ifndef BRAVE_BROWSER_WORKSPACES_WORKSPACE_SERVICE_FACTORY_H_
#define BRAVE_BROWSER_WORKSPACES_WORKSPACE_SERVICE_FACTORY_H_
#include "base/no_destructor.h"
#include "chrome/browser/profiles/profile_keyed_service_factory.h"
namespace user_prefs {
class PrefRegistrySyncable;
} // namespace user_prefs
class WorkspaceService;
class Profile;
class WorkspaceServiceFactory : public ProfileKeyedServiceFactory {
public:
static WorkspaceServiceFactory* GetInstance();
static WorkspaceService* GetForProfile(Profile* profile);
WorkspaceServiceFactory(const WorkspaceServiceFactory&) = delete;
WorkspaceServiceFactory& operator=(const WorkspaceServiceFactory&) = delete;
private:
friend class base::NoDestructor<WorkspaceServiceFactory>;
WorkspaceServiceFactory();
~WorkspaceServiceFactory() override;
// ProfileKeyedServiceFactory:
std::unique_ptr<KeyedService> BuildServiceInstanceForBrowserContext(
content::BrowserContext* context) const override;
void RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) override;
bool ServiceIsCreatedWithBrowserContext() const override;
};
#endif // BRAVE_BROWSER_WORKSPACES_WORKSPACE_SERVICE_FACTORY_H_
@@ -0,0 +1,275 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#include "brave/browser/workspaces/workspace_service.h"
#include <memory>
#include <vector>
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/test/bind.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "brave/browser/workspaces/features.h"
#include "brave/browser/workspaces/workspace_metadata.h"
#include "brave/browser/workspaces/workspace_service_factory.h"
#include "brave/browser/workspaces/workspace_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/sessions/core/command_storage_backend.h"
#include "components/sessions/core/session_id.h"
#include "components/sessions/core/session_service_commands.h"
#include "components/sessions/core/session_types.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
WorkspaceMetadata MakeMeta(const std::string& name,
int windows,
int tabs,
base::Time t) {
WorkspaceMetadata meta{.name = name,
.modified_at = t,
.number_of_windows = windows,
.number_of_tabs = tabs};
return meta;
}
} // namespace
class WorkspaceServiceTest : public ::testing::Test {
protected:
void SetUp() override {
ASSERT_TRUE(profile_manager_.SetUp());
// Instantiate the factory so it registers with
// BrowserContextDependencyManager before profile creation, ensuring
// kWorkspacesMetadataPref is registered.
WorkspaceServiceFactory::GetInstance();
profile_ = profile_manager_.CreateTestingProfile("test");
service_ = std::make_unique<WorkspaceService>(*profile_);
}
content::BrowserTaskEnvironment task_environment_;
TestingProfileManager profile_manager_{TestingBrowserProcess::GetGlobal()};
raw_ptr<Profile> profile_ = nullptr;
std::unique_ptr<WorkspaceService> service_;
};
// ---- Metadata (pref) tests --------------------------------------------------
// No workspaces listed w/ empty profile
TEST_F(WorkspaceServiceTest, ListWorkspaces_InitiallyEmpty) {
EXPECT_TRUE(service_->ListWorkspaces().empty());
}
// Verify saving preference adds workspace to list
TEST_F(WorkspaceServiceTest, SaveMetadata_AppearsInList) {
base::Time t = base::Time::FromSecondsSinceUnixEpoch(1700000000.0);
service_->SaveWorkspaceMetadata(MakeMeta("My Workspace", 2, 5, t));
auto workspaces = service_->ListWorkspaces();
ASSERT_EQ(workspaces.size(), 1u);
EXPECT_EQ(workspaces[0].name, "My Workspace");
EXPECT_EQ(workspaces[0].number_of_windows, 2);
EXPECT_EQ(workspaces[0].number_of_tabs, 5);
EXPECT_EQ(workspaces[0].modified_at,
base::Time::FromSecondsSinceUnixEpoch(1700000000.0));
}
// Verify listing of workspaces orders by date modified desc
TEST_F(WorkspaceServiceTest, ListWorkspaces_SortedByModifiedAtDescending) {
base::Time t1 = base::Time::FromSecondsSinceUnixEpoch(1700000000.0);
base::Time t2 = base::Time::FromSecondsSinceUnixEpoch(1700000100.0);
base::Time t3 = base::Time::FromSecondsSinceUnixEpoch(1699999900.0);
service_->SaveWorkspaceMetadata(MakeMeta("Alpha", 1, 3, t1));
service_->SaveWorkspaceMetadata(MakeMeta("Beta", 1, 5, t2));
service_->SaveWorkspaceMetadata(MakeMeta("Gamma", 1, 2, t3));
auto workspaces = service_->ListWorkspaces();
ASSERT_EQ(workspaces.size(), 3u);
EXPECT_EQ(workspaces[0].name, "Beta");
EXPECT_EQ(workspaces[1].name, "Alpha");
EXPECT_EQ(workspaces[2].name, "Gamma");
}
// Removing preference removes it from the list
TEST_F(WorkspaceServiceTest, RemoveMetadata_DisappearsFromList) {
base::Time t = base::Time::FromSecondsSinceUnixEpoch(1700000000.0);
service_->SaveWorkspaceMetadata(MakeMeta("Work", 1, 3, t));
ASSERT_EQ(service_->ListWorkspaces().size(), 1u);
service_->RemoveWorkspaceMetadata("Work");
EXPECT_TRUE(service_->ListWorkspaces().empty());
}
// Saving a workspace over another one replaces it
TEST_F(WorkspaceServiceTest, SaveMetadata_SameNameOverwrites) {
base::Time t1 = base::Time::FromSecondsSinceUnixEpoch(1700000000.0);
base::Time t2 = base::Time::FromSecondsSinceUnixEpoch(1700000100.0);
service_->SaveWorkspaceMetadata(MakeMeta("Work", 1, 3, t1));
service_->SaveWorkspaceMetadata(MakeMeta("Work", 2, 7, t2));
auto workspaces = service_->ListWorkspaces();
ASSERT_EQ(workspaces.size(), 1u);
EXPECT_EQ(workspaces[0].name, "Work");
EXPECT_EQ(workspaces[0].number_of_windows, 2);
EXPECT_EQ(workspaces[0].number_of_tabs, 7);
}
// ---- Name / path computation tests -----------------------------------------
// Key is a stable hex hash — different display names get different paths.
TEST_F(WorkspaceServiceTest,
GetWorkspacePathForName_DifferentNamesDifferentPaths) {
base::FilePath path1 = service_->GetWorkspacePathForName("Work Space");
base::FilePath path2 = service_->GetWorkspacePathForName("Work-Space");
EXPECT_NE(path1, path2);
}
// Same name always resolves to the same path (deterministic key).
TEST_F(WorkspaceServiceTest, GetWorkspacePathForName_SameNameSamePath) {
EXPECT_EQ(service_->GetWorkspacePathForName("My Workspace"),
service_->GetWorkspacePathForName("My Workspace"));
}
// ---- Disk I/O tests (async, use RunUntil not RunUntilIdle) ------------------
// Verify that WriteWorkspaceToDisk saves the profile without calling onError.
TEST_F(WorkspaceServiceTest, WriteAndReadRoundTrip) {
using CommandList = std::vector<std::unique_ptr<sessions::SessionCommand>>;
SessionID window_id = SessionID::NewUnique();
CommandList commands;
commands.push_back(sessions::CreateSetWindowTypeCommand(
window_id, sessions::SessionWindow::TYPE_NORMAL));
commands.push_back(
sessions::CreateSetSelectedTabInWindowCommand(window_id, 0));
const size_t num_commands = commands.size();
base::FilePath workspace_path =
service_->GetWorkspacePathForName("roundtrip");
auto task_runner = base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::USER_VISIBLE,
base::TaskShutdownBehavior::BLOCK_SHUTDOWN});
auto backend = base::MakeRefCounted<sessions::CommandStorageBackend>(
task_runner, workspace_path, kWorkspaceSessionType,
/*encryptor=*/std::nullopt);
bool error_called = false;
bool write_done = false;
auto on_error = base::BindPostTask(
base::SequencedTaskRunner::GetCurrentDefault(),
base::BindOnce([](bool* flag) { *flag = true; }, &error_called));
task_runner->PostTask(
FROM_HERE,
base::BindOnce(&WriteWorkspaceToDisk, std::move(commands), workspace_path,
std::move(backend), std::move(on_error)));
// A sequenced task after the write runs only after it completes.
task_runner->PostTask(
FROM_HERE,
base::BindPostTask(
base::SequencedTaskRunner::GetCurrentDefault(),
base::BindOnce([](bool* flag) { *flag = true; }, &write_done)));
ASSERT_TRUE(base::test::RunUntil([&] { return write_done; }));
ASSERT_FALSE(error_called);
// Read back using a fresh backend (mirrors RestoreWorkspace's pattern).
auto read_backend = base::MakeRefCounted<sessions::CommandStorageBackend>(
task_runner, workspace_path, kWorkspaceSessionType,
/*encryptor=*/std::nullopt);
base::test::TestFuture<CommandList> read_future;
task_runner->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(&ReadWorkspaceFromDisk, workspace_path, read_backend),
read_future.GetCallback());
ASSERT_TRUE(read_future.Wait());
const auto& read_cmds = read_future.Get();
ASSERT_EQ(read_cmds.size(), num_commands);
// Verify content — not just count — survived the round-trip.
// Re-creating the reference commands from the same inputs and comparing
// serialized bytes confirms the window ID and TYPE_NORMAL encoding are
// preserved end-to-end (both are embedded in the serialized payload).
EXPECT_EQ(read_cmds[0]->Serialize(),
sessions::CreateSetWindowTypeCommand(
window_id, sessions::SessionWindow::TYPE_NORMAL)
->Serialize());
EXPECT_EQ(
read_cmds[1]->Serialize(),
sessions::CreateSetSelectedTabInWindowCommand(window_id, 0)->Serialize());
}
TEST_F(WorkspaceServiceTest,
WriteToDisk_DirectoryCreationFailure_CallsOnError) {
// Place a regular FILE where the workspace directory would be so that
// base::CreateDirectory fails on the background thread and on_error is
// posted back to the UI thread.
ASSERT_TRUE(base::CreateDirectory(service_->GetWorkspacesPath()));
base::FilePath workspace_path = service_->GetWorkspacePathForName("blocked");
ASSERT_TRUE(base::WriteFile(workspace_path, "not-a-directory"));
auto task_runner = base::ThreadPool::CreateSequencedTaskRunner(
{base::MayBlock(), base::TaskPriority::USER_VISIBLE,
base::TaskShutdownBehavior::BLOCK_SHUTDOWN});
auto backend = base::MakeRefCounted<sessions::CommandStorageBackend>(
task_runner, workspace_path, kWorkspaceSessionType,
/*encryptor=*/std::nullopt);
bool error_called = false;
auto on_error = base::BindPostTask(
base::SequencedTaskRunner::GetCurrentDefault(),
base::BindOnce([](bool* flag) { *flag = true; }, &error_called));
task_runner->PostTask(
FROM_HERE,
base::BindOnce(&WriteWorkspaceToDisk,
std::vector<std::unique_ptr<sessions::SessionCommand>>(),
workspace_path, std::move(backend), std::move(on_error)));
EXPECT_TRUE(base::test::RunUntil([&] { return error_called; }));
}
// ---- Factory tests ----------------------------------------------------------
class WorkspaceServiceFactoryTest : public ::testing::Test {
protected:
void SetUp() override { ASSERT_TRUE(profile_manager_.SetUp()); }
content::BrowserTaskEnvironment task_environment_;
base::test::ScopedFeatureList feature_list_;
TestingProfileManager profile_manager_{TestingBrowserProcess::GetGlobal()};
};
TEST_F(WorkspaceServiceFactoryTest, FeatureDisabled_GetForProfileReturnsNull) {
feature_list_.InitAndDisableFeature(features::kWorkspaces);
auto* profile = profile_manager_.CreateTestingProfile("test");
EXPECT_EQ(WorkspaceServiceFactory::GetForProfile(profile), nullptr);
}
TEST_F(WorkspaceServiceFactoryTest,
FeatureEnabled_GetForProfileReturnsNonNull) {
feature_list_.InitAndEnableFeature(features::kWorkspaces);
auto* profile = profile_manager_.CreateTestingProfile("test");
// ServiceIsNULLWhileTesting() suppresses service creation for test profiles.
// Provide a testing factory so the feature-enabled path is exercised.
WorkspaceServiceFactory::GetInstance()->SetTestingFactory(
profile,
base::BindLambdaForTesting(
[](content::BrowserContext* ctx) -> std::unique_ptr<KeyedService> {
auto* p = Profile::FromBrowserContext(ctx);
return std::make_unique<WorkspaceService>(*p);
}));
EXPECT_NE(WorkspaceServiceFactory::GetForProfile(profile), nullptr);
}
@@ -0,0 +1,231 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#include "brave/browser/workspaces/workspace_session_utils.h"
#include <algorithm>
#include <memory>
#include <set>
#include <vector>
#include "brave/browser/workspaces/workspace_metadata.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_tabrestore.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
#include "chrome/browser/ui/tabs/tab_group_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "components/sessions/content/content_serialized_navigation_builder.h"
#include "components/sessions/core/session_service_commands.h"
#include "components/sessions/core/session_types.h"
#include "components/tab_groups/tab_group_visual_data.h"
#include "components/tabs/public/tab_group.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/web_contents.h"
namespace {
// Appends session commands for a single browser window to |commands|.
// Serializes window type, bounds, tab groups, tabs (with full navigation
// history), pinned state, and the active tab index.
void AppendBrowserSessionCommands(
const SessionID& window_id,
TabStripModel* tsm,
gfx::Rect restored_bounds,
ui::mojom::WindowShowState restored_state,
std::vector<std::unique_ptr<sessions::SessionCommand>>& commands) {
commands.push_back(sessions::CreateSetWindowTypeCommand(
window_id, sessions::SessionWindow::TYPE_NORMAL));
commands.push_back(sessions::CreateSetWindowBoundsCommand(
window_id, restored_bounds, restored_state));
// Emit group metadata for every tab group in this window.
if (tsm->group_model()) {
for (const tab_groups::TabGroupId& group_id :
tsm->group_model()->ListTabGroups()) {
auto* group = tsm->group_model()->GetTabGroup(group_id);
if (!group || !group->visual_data()) {
continue;
}
commands.push_back(sessions::CreateTabGroupMetadataUpdateCommand(
group_id, group->visual_data()));
}
}
for (int i = 0; i < tsm->count(); ++i) {
content::WebContents* contents = tsm->GetWebContentsAt(i);
CHECK(contents);
SessionID tab_id = SessionID::NewUnique();
commands.push_back(sessions::CreateSetTabWindowCommand(window_id, tab_id));
commands.push_back(sessions::CreateSetTabIndexInWindowCommand(tab_id, i));
if (tsm->IsTabPinned(i)) {
commands.push_back(sessions::CreatePinnedStateCommand(tab_id, true));
}
std::optional<tab_groups::TabGroupId> group_id = tsm->GetTabGroupForTab(i);
if (group_id.has_value()) {
commands.push_back(sessions::CreateTabGroupCommand(tab_id, group_id));
}
// Serialize the full navigation history for this tab.
auto& controller = contents->GetController();
int nav_count = controller.GetEntryCount();
int current_entry = controller.GetCurrentEntryIndex();
for (int j = 0; j < nav_count; ++j) {
content::NavigationEntry* entry = controller.GetEntryAtIndex(j);
if (!entry) {
continue;
}
auto serialized =
sessions::ContentSerializedNavigationBuilder::FromNavigationEntry(
j, entry);
commands.push_back(
sessions::CreateUpdateTabNavigationCommand(tab_id, serialized));
}
commands.push_back(sessions::CreateSetSelectedNavigationIndexCommand(
tab_id, current_entry));
}
commands.push_back(sessions::CreateSetSelectedTabInWindowCommand(
window_id, tsm->active_index()));
}
} // namespace
std::vector<std::unique_ptr<sessions::SessionCommand>>
GenerateBrowserSessionCommandsForWorkspace(Profile* profile,
WorkspaceMetadata& workspace) {
std::vector<std::unique_ptr<sessions::SessionCommand>> commands;
SessionID active_window_id = SessionID::InvalidValue();
GlobalBrowserCollection::GetInstance()->ForEach(
[&](BrowserWindowInterface* bwi) {
if (bwi->GetProfile() != profile ||
bwi->GetType() != BrowserWindowInterface::Type::TYPE_NORMAL) {
return true;
}
int tabs = bwi->GetTabStripModel()->count();
if (tabs == 0) {
return true;
}
SessionID window_id = SessionID::NewUnique();
auto* window = bwi->GetWindow();
AppendBrowserSessionCommands(window_id, bwi->GetTabStripModel(),
window->GetRestoredBounds(),
window->GetRestoredState(), commands);
// Prefer the focused window; fall back to the first non-empty window.
if (window->IsActive() ||
active_window_id == SessionID::InvalidValue()) {
active_window_id = window_id;
}
workspace.number_of_windows++;
workspace.number_of_tabs += tabs;
return true;
});
if (workspace.number_of_tabs == 0) {
return {};
}
if (active_window_id != SessionID::InvalidValue()) {
commands.push_back(
sessions::CreateSetActiveWindowCommand(active_window_id));
}
return commands;
}
void RestoreBrowserSessionCommandsForWorkspace(
Profile* profile,
std::vector<std::unique_ptr<sessions::SessionCommand>> commands) {
// RestoreSessionFromCommands constructs SessionTab/SessionWindow objects
// whose constructors call SessionID::NewUnique(), which is sequence-checked
// to the UI thread. It must therefore be called here (UI thread), not in
// the background I/O task.
std::vector<std::unique_ptr<sessions::SessionWindow>> windows;
SessionID active_window_id = SessionID::InvalidValue();
std::string platform_session_id;
std::set<SessionID> discarded_window_ids;
sessions::RestoreSessionFromCommands(commands, &windows, &active_window_id,
&platform_session_id,
&discarded_window_ids);
if (windows.empty()) {
return;
}
// We restore tabs ourselves rather than using RestoreForeignSessionWindows
// because that API creates an empty new_group_ids map and never calls
// RestoreTabGroupMetadata, so tab group names/colors are silently dropped.
// By passing the original TabGroupId directly to AddRestoredTab, we avoid
// any ID remapping and can apply visual data with the same IDs afterwards.
for (const auto& window : windows) {
if (window->tabs.empty()) {
continue;
}
if (Browser::GetCreationStatusForProfile(profile) !=
Browser::CreationStatus::kOk) {
continue;
}
Browser::CreateParams params(Browser::TYPE_NORMAL, profile, false);
params.initial_bounds = window->bounds;
params.initial_show_state = window->show_state;
params.initial_workspace = window->workspace;
params.initial_visible_on_all_workspaces_state =
window->visible_on_all_workspaces;
params.should_trigger_session_restore = false;
Browser* browser = Browser::Create(params);
if (!browser) {
continue;
}
auto* tsm = browser->tab_strip_model();
for (size_t i = 0; i < window->tabs.size(); ++i) {
const auto& tab = window->tabs[i];
if (tab->navigations.empty()) {
continue;
}
chrome::AddRestoredTab(
browser, tab->navigations,
/*tab_index=*/i, tab->normalized_navigation_index(),
tab->extension_app_id,
/*group=*/tab->group,
/*select=*/false, tab->pinned,
/*last_active_time_ticks=*/base::TimeTicks(), tab->last_active_time,
/*storage_namespace=*/nullptr, tab->user_agent_override,
tab->extra_data,
/*from_session_restore=*/true,
/*is_active_browser=*/std::nullopt);
}
// Apply group names, colors, and collapsed state. Since we passed the
// original TabGroupIds to AddRestoredTab, no ID remapping is needed.
for (const auto& session_group : window->tab_groups) {
if (!tsm->group_model() ||
!tsm->group_model()->ContainsTabGroup(session_group->id)) {
continue;
}
tsm->ChangeTabGroupVisuals(session_group->id, session_group->visual_data);
}
int active = std::clamp(window->selected_tab_index, 0,
std::max(0, tsm->count() - 1));
if (active < tsm->count()) {
tsm->ActivateTabAt(active);
}
browser->window()->Show();
}
}
@@ -0,0 +1,35 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#ifndef BRAVE_BROWSER_WORKSPACES_WORKSPACE_SESSION_UTILS_H_
#define BRAVE_BROWSER_WORKSPACES_WORKSPACE_SESSION_UTILS_H_
#include <memory>
#include <vector>
#include "components/sessions/core/session_command.h"
#include "components/sessions/core/session_id.h"
#include "ui/base/mojom/window_show_state.mojom.h"
#include "ui/gfx/geometry/rect.h"
class Profile;
class TabStripModel;
struct WorkspaceMetadata;
// Iterates all normal browser windows for |profile|, serializes their state
// into session commands, and returns the result. |window_count| and
// |tab_count| are incremented to reflect what was captured. Returns an empty
// vector if there are no tabs to save.
std::vector<std::unique_ptr<sessions::SessionCommand>>
GenerateBrowserSessionCommandsForWorkspace(Profile* profile,
WorkspaceMetadata& workspace);
// Deserializes |commands| and opens the encoded windows/tabs in new browser
// windows belonging to |profile|. Must be called on the UI thread.
void RestoreBrowserSessionCommandsForWorkspace(
Profile* profile,
std::vector<std::unique_ptr<sessions::SessionCommand>> commands);
#endif // BRAVE_BROWSER_WORKSPACES_WORKSPACE_SESSION_UTILS_H_
+92
View File
@@ -0,0 +1,92 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#include "brave/browser/workspaces/workspace_utils.h"
#include <algorithm>
#include <memory>
#include <utility>
#include <vector>
#include "base/files/file_util.h"
#include "base/json/values_util.h"
#include "base/logging.h"
#include "base/time/time.h"
namespace {
constexpr const char kWorkspaceName[] = "name";
constexpr const char kWorkspaceWindowCount[] = "number-of-windows";
constexpr const char kWorkspaceTabCount[] = "number-of-tabs";
constexpr const char kWorkspaceModifiedAt[] = "modified-at";
} // namespace
std::vector<WorkspaceMetadata> ListWorkspacesFromDict(
const base::DictValue& dict) {
std::vector<WorkspaceMetadata> result;
for (const auto [key, value] : dict) {
const base::DictValue* entry = value.GetIfDict();
if (!entry) {
continue;
}
const std::string* name = entry->FindString(kWorkspaceName);
if (!name || name->empty()) {
continue;
}
WorkspaceMetadata info = {
.name = *name,
.modified_at = base::ValueToTime(entry->Find(kWorkspaceModifiedAt))
.value_or(base::Time::Now()),
.number_of_windows = entry->FindInt(kWorkspaceWindowCount).value_or(1),
.number_of_tabs = entry->FindInt(kWorkspaceTabCount).value_or(0)};
result.push_back(std::move(info));
}
std::sort(result.begin(), result.end(),
[](const WorkspaceMetadata& a, const WorkspaceMetadata& b) {
return a.modified_at > b.modified_at;
});
return result;
}
base::DictValue WorkspaceMetadataToDictEntry(const WorkspaceMetadata& meta) {
return base::DictValue()
.Set(kWorkspaceName, meta.name)
.Set(kWorkspaceWindowCount, meta.number_of_windows)
.Set(kWorkspaceTabCount, meta.number_of_tabs)
.Set(kWorkspaceModifiedAt, base::TimeToValue(meta.modified_at));
}
void WriteWorkspaceToDisk(
std::vector<std::unique_ptr<sessions::SessionCommand>> commands,
const base::FilePath& workspace_path,
scoped_refptr<sessions::CommandStorageBackend> backend,
base::OnceClosure on_error) {
if (!base::CreateDirectory(workspace_path)) {
DVLOG(1) << "Failed to create workspace directory: " << workspace_path;
std::move(on_error).Run();
return;
}
// AppendCommands posts |on_error| to the backend's callback_task_runner_
// (the UI thread, set when the backend was constructed) on write failure.
backend->AppendCommands(std::move(commands), /*truncate=*/true,
std::move(on_error));
}
std::vector<std::unique_ptr<sessions::SessionCommand>> ReadWorkspaceFromDisk(
const base::FilePath& workspace_path,
scoped_refptr<sessions::CommandStorageBackend> backend) {
// Only do file I/O here. Callers must call RestoreSessionFromCommands() on
// the UI thread because SessionTab/SessionWindow constructors call
// SessionID::NewUnique() which is sequence-checked to the UI thread.
sessions::CommandStorageBackend::ReadCommandsResult result =
backend->ReadLastSessionCommands();
if (result.error_reading || result.commands.empty()) {
DVLOG(1) << "Could not read workspace session from: " << workspace_path;
return {};
}
return std::move(result.commands);
}
+60
View File
@@ -0,0 +1,60 @@
/* Copyright (c) 2026 The Brave Authors. All rights reserved.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at https://mozilla.org/MPL/2.0/. */
#ifndef BRAVE_BROWSER_WORKSPACES_WORKSPACE_UTILS_H_
#define BRAVE_BROWSER_WORKSPACES_WORKSPACE_UTILS_H_
#include <memory>
#include <vector>
#include "base/files/file_path.h"
#include "base/functional/callback.h"
#include "base/memory/scoped_refptr.h"
#include "base/values.h"
#include "brave/browser/workspaces/workspace_metadata.h"
#include "components/sessions/core/command_storage_backend.h"
#include "components/sessions/core/command_storage_manager.h"
#include "components/sessions/core/session_command.h"
// Session type used for all workspace files. Exposed so callers can
// construct a CommandStorageBackend on the UI thread before posting I/O
// work to a background sequenced runner.
inline constexpr sessions::CommandStorageManager::SessionType
kWorkspaceSessionType =
sessions::CommandStorageManager::SessionType::kSessionRestore;
// Writes |commands| to a Chromium session-command binary file via |backend|.
// |on_error| is called directly if the workspace directory cannot be created,
// or is posted to the UI thread (via AppendCommands' callback_task_runner_) on
// write failure. Wrap with BindPostTask in the caller so it always lands on
// the UI thread regardless of which path fires.
//
// Must run on a background sequenced task runner.
void WriteWorkspaceToDisk(
std::vector<std::unique_ptr<sessions::SessionCommand>> commands,
const base::FilePath& workspace_path,
scoped_refptr<sessions::CommandStorageBackend> backend,
base::OnceClosure on_error);
// Reads the session-command binary via |backend| and returns the raw commands.
// Only does file I/O — callers must deserialize the commands into
// SessionWindow objects on the UI thread (SessionID::NewUnique() is
// sequence-checked to the UI thread). Returns an empty vector on error.
//
// Must run on a background sequenced task runner.
std::vector<std::unique_ptr<sessions::SessionCommand>> ReadWorkspaceFromDisk(
const base::FilePath& workspace_path,
scoped_refptr<sessions::CommandStorageBackend> backend);
// Parses a workspace metadata dict (the value stored under
// kWorkspacesMetadataPref) and returns a list sorted by modified_at descending.
std::vector<WorkspaceMetadata> ListWorkspacesFromDict(
const base::DictValue& dict);
// Serializes |meta| into a single dict entry suitable for storing under its key
// in kWorkspacesMetadataPref.
base::DictValue WorkspaceMetadataToDictEntry(const WorkspaceMetadata& meta);
#endif // BRAVE_BROWSER_WORKSPACES_WORKSPACE_UTILS_H_
+1
View File
@@ -509,6 +509,7 @@ test("brave_unit_tests") {
"//brave/browser/ui/webui/settings:unit_tests",
"//brave/browser/ui/webui/side_panel/customize_chrome:unit_tests",
"//brave/browser/ui/whats_new:unit_test",
"//brave/browser/workspaces:unit_tests",
"//brave/components/ai_chat/core/common/buildflags",
"//brave/components/brave_shields/core/common:mojom",
"//brave/components/brave_vpn/common/buildflags",