
サービス終了したSidekickブラウザのワークスペースを変更・追加・削除する方法
この記事のポイント
- ✓Sidekickブラウザのサービス終了後
- ✓UIからワークスペースの操作ができなくなった問題の回避策
- ✓Chrome DevTools Protocolを使ってchrome.storage.localを直接書き換える方法を解説します
Sidekickのワークスペースが操作できない
Sidekickブラウザは2025年にPerplexityに買収され、サービスを終了しました。後継は「Comet Browser」ですが、ワークスペースの概念が大きく異なるため、そのままSidekickを使い続けている方も多いのではないでしょうか。
サービス終了後、Sidekickの多くの機能はオフラインで動作し続けますが、ワークスペース名の変更、ワークスペースの追加、ワークスペースの削除がUIからできなくなります。
これは、ワークスペースのリネーム操作がSidekickのサーバーとの同期を前提とした設計になっているためです。サーバーが停止した今、UIからの変更は受け付けられません。
原因の調査
まず、ワークスペース名がどこに保存されているかを調査しました。
Preferencesファイルは「表示用キャッシュ」に過ぎない
Sidekickの設定ファイルは以下のパスにあります。
~/Library/Application Support/Sidekick/Default/Preferencesこのファイル内の sidekick.tab_menu にワークスペース名が含まれていますが、これはメニュー表示用のキャッシュです。ここを書き換えても、Sidekick再起動時に内部データから上書きされるため、意味がありません。
実データの保存場所
Sidekickの内部拡張(mcjlamohcooanphmebaiigheeeoplihb)の chrome.storage.local に実データが保存されています。
具体的には、以下の4箇所にワークスペース関連のデータが保存されています。
{userId}_workspaces → ワークスペース名・アバター・設定
{userId}_sessions → セッション名(ワークスペースと対応)
{userId}_app-accounts-by-workspace → ワークスペースごとのアプリアカウント
{userId}_backend_cache_/desktop/workspaces → バックエンドキャッシュuserId はSidekickアカウントのUUIDです(後述の手順で自動的に特定されます)。
ワークスペースの操作(名前変更・追加・削除)では、これら全ての箇所を整合性を保って更新する必要があります。特に _backend_cache_ を更新しないと、再起動時にキャッシュから復元されて変更が元に戻ります。
変更手順
前提条件
以下がインストールされている必要があります。
# Python 3 と websocket-client
pip3 install websocket-client手順1: Sidekickをデバッグモードで起動する
まず、起動中のSidekickを終了します。
osascript -e 'quit app "Sidekick"'3秒ほど待ってから、デバッグモードで起動します。
/Applications/Sidekick.app/Contents/MacOS/Sidekick \
--remote-debugging-port=9222 \
'--remote-allow-origins=*' &起動を確認します。
curl -s http://localhost:9222/json/list | python3 -m json.tool | head -5JSONが返ってくれば接続成功です。
手順2: 現在のワークスペース名を確認する
以下のPythonスクリプトで、現在のワークスペース名一覧を取得します。
import json, websocket, urllib.request
# background_pageに接続
resp = urllib.request.urlopen("http://localhost:9222/json/list")
pages = json.loads(resp.read())
bg = [p for p in pages if p.get('type') == 'background_page'][0]
ws = websocket.create_connection(bg['webSocketDebuggerUrl'])
# ワークスペース名を取得
js = """
new Promise((resolve) => {
chrome.storage.local.get(null, (items) => {
var result = [];
Object.keys(items).forEach(k => {
if (k.endsWith('_workspaces') && !k.includes('backend_cache')) {
var workspaces = items[k];
if (Array.isArray(workspaces)) {
workspaces.forEach(w => {
result.push({id: w.id, name: w.name});
});
}
}
});
resolve(JSON.stringify(result));
});
})
"""
ws.send(json.dumps({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": js, "returnByValue": True, "awaitPromise": True}
}))
resp = json.loads(ws.recv())
workspaces = json.loads(resp['result']['result']['value'])
for w in workspaces:
print(f" {w['name']} (ID: {w['id']})")
ws.close()出力例:
[email protected] (ID: e0f3eaa0-...)
ワークスペースA (ID: a88608c8-...)
ワークスペースB (ID: 7b4fb04a-...)手順3: ワークスペース名を書き換える
変更対象のワークスペースIDと新しい名前を指定して、以下のスクリプトを実行します。
import json, websocket, urllib.request
# === ここを変更 ===
TARGET_WORKSPACE_ID = "a88608c8-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # 手順2で確認したID
NEW_NAME = "新しい名前"
# ==================
resp = urllib.request.urlopen("http://localhost:9222/json/list")
pages = json.loads(resp.read())
bg = [p for p in pages if p.get('type') == 'background_page'][0]
ws = websocket.create_connection(bg['webSocketDebuggerUrl'])
js = f"""
new Promise((resolve) => {{
var targetId = "{TARGET_WORKSPACE_ID}";
var newName = "{NEW_NAME}";
chrome.storage.local.get(null, (allItems) => {{
var changes = {{}};
var changeCount = 0;
Object.keys(allItems).forEach(k => {{
// workspaces の名前とアバターを変更
if (k.endsWith('_workspaces') && !k.includes('backend_cache')) {{
var workspaces = allItems[k];
if (Array.isArray(workspaces)) {{
var oldName = null;
workspaces.forEach((w, i) => {{
if (w.id === targetId) {{
oldName = w.name;
workspaces[i].name = newName;
workspaces[i].avatar.text = newName.substring(0, 1);
changeCount++;
}}
}});
if (oldName) {{
changes[k] = workspaces;
// sessions の名前も変更
var sessionsKey = k.replace('_workspaces', '_sessions');
if (allItems[sessionsKey]) {{
var sessions = allItems[sessionsKey];
Object.keys(sessions).forEach(sk => {{
if (sessions[sk].name === oldName) {{
sessions[sk].name = newName;
changeCount++;
}}
}});
changes[sessionsKey] = sessions;
}}
// backend_cache も変更
Object.keys(allItems).forEach(ck => {{
if (ck.includes('backend_cache') && ck.includes('workspaces')) {{
var val = allItems[ck];
if (typeof val === 'string' && val.includes(oldName)) {{
changes[ck] = val.split(oldName).join(newName);
changeCount++;
}}
}}
}});
}}
}}
}}
}});
chrome.storage.local.set(changes, () => {{
resolve(JSON.stringify({{success: true, changeCount: changeCount}}));
}});
}});
}})
"""
ws.send(json.dumps({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": js, "returnByValue": True, "awaitPromise": True}
}))
resp = json.loads(ws.recv())
result = json.loads(resp['result']['result']['value'])
print(f"変更完了: {result['changeCount']}箇所を更新")
ws.close()手順4: Sidekickを再起動する
デバッグモードで起動したSidekickのプロセスを終了し、通常モードで再起動します。
# デバッグモードのプロセスを終了
osascript -e 'quit app "Sidekick"'
# 通常モードで起動
open -a Sidekickワークスペース一覧を開いて、名前が変更されていることを確認してください。
ワークスペースの追加
新しいワークスペースを追加するには、_workspaces、_app-accounts-by-workspace、_backend_cache_ の3箇所に同時にデータを追加します。
import json, websocket, urllib.request, uuid
# === ここを変更 ===
NEW_NAME = "新しいワークスペース"
BG_COLOR = "#4CAF50" # 背景色
FG_COLOR = "#ffffff" # 文字色
# ==================
new_ws_id = str(uuid.uuid4())
resp = urllib.request.urlopen("http://localhost:9222/json/list")
pages = json.loads(resp.read())
bg = [p for p in pages if p.get('type') == 'background_page'][0]
ws = websocket.create_connection(bg['webSocketDebuggerUrl'])
js = f"""
new Promise((resolve) => {{
chrome.storage.local.get(null, (allItems) => {{
var changes = {{}};
var newWsId = "{new_ws_id}";
var newName = "{NEW_NAME}";
Object.keys(allItems).forEach(k => {{
// 1. _workspaces に追加
if (k.endsWith('_workspaces') && !k.includes('backend_cache')) {{
var workspaces = allItems[k];
if (Array.isArray(workspaces)) {{
workspaces.push({{
avatar: {{
backgroundColor: "{BG_COLOR}",
foregroundColor: "{FG_COLOR}",
text: newName.substring(0, 1),
type: "Colored"
}},
id: newWsId,
isMinimized: false,
isRemovable: true,
isTeamWorkspace: false,
name: newName,
orderAppIds: [],
partitionDomain: newWsId,
pinnedOrderAppIds: [],
sandbox: false,
teamUuid: "00000000-0000-0000-0000-000000000000"
}});
changes[k] = workspaces;
// 2. _app-accounts-by-workspace に空エントリ追加
var appKey = k.replace('_workspaces', '_app-accounts-by-workspace');
if (allItems[appKey]) {{
var appAccounts = allItems[appKey];
appAccounts[newWsId] = {{}};
changes[appKey] = appAccounts;
}}
}}
}}
// 3. _backend_cache_ に追加
if (k.includes('backend_cache') && k.endsWith('workspaces')) {{
var cache = allItems[k];
if (typeof cache === 'string') {{
var parsed = JSON.parse(cache);
parsed.push({{
uuid: newWsId,
name: newName,
background: {{
type: "Color",
value: {{bg: "{BG_COLOR}", letterColor: "{FG_COLOR}"}}
}},
sandbox: false,
pinned_services_order: {{order: []}},
services_order: {{order: []}},
session_key: null,
last_active_service_id: null,
can_delete: true,
team_workspace: false,
team_uuid: "00000000-0000-0000-0000-000000000000"
}});
changes[k] = JSON.stringify(parsed);
}}
}}
}});
chrome.storage.local.set(changes, () => {{
resolve(JSON.stringify({{success: true, newId: newWsId}}));
}});
}});
}})
"""
ws.send(json.dumps({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": js, "returnByValue": True, "awaitPromise": True}
}))
resp = json.loads(ws.recv())
result = json.loads(resp['result']['result']['value'])
print(f"追加完了: ID={result['newId']}")
ws.close()実行後、Sidekickを再起動すればワークスペース一覧に追加されています。
ワークスペースの削除
削除は追加の逆の操作です。_workspaces、_app-accounts-by-workspace、_backend_cache_ の3箇所からデータを除去します。
注意: デフォルトのワークスペース(isRemovable: false)は削除しないでください。Sidekickが起動しなくなる可能性があります。
import json, websocket, urllib.request
# === ここを変更 ===
TARGET_WORKSPACE_ID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # 手順2で確認したID
# ==================
resp = urllib.request.urlopen("http://localhost:9222/json/list")
pages = json.loads(resp.read())
bg = [p for p in pages if p.get('type') == 'background_page'][0]
ws = websocket.create_connection(bg['webSocketDebuggerUrl'])
js = f"""
new Promise((resolve) => {{
var targetId = "{TARGET_WORKSPACE_ID}";
chrome.storage.local.get(null, (allItems) => {{
var changes = {{}};
var deletedName = null;
Object.keys(allItems).forEach(k => {{
// 1. _workspaces から削除
if (k.endsWith('_workspaces') && !k.includes('backend_cache')) {{
var workspaces = allItems[k];
if (Array.isArray(workspaces)) {{
var target = workspaces.find(w => w.id === targetId);
if (target) {{
if (!target.isRemovable) {{
resolve(JSON.stringify({{
success: false,
reason: "デフォルトワークスペースは削除できません"
}}));
return;
}}
deletedName = target.name;
changes[k] = workspaces.filter(w => w.id !== targetId);
// 2. _app-accounts-by-workspace から削除
var appKey = k.replace('_workspaces', '_app-accounts-by-workspace');
if (allItems[appKey]) {{
var appAccounts = allItems[appKey];
delete appAccounts[targetId];
changes[appKey] = appAccounts;
}}
// 3. _sessions から対応するセッションを削除
var sessionsKey = k.replace('_workspaces', '_sessions');
if (allItems[sessionsKey] && deletedName) {{
var sessions = allItems[sessionsKey];
Object.keys(sessions).forEach(sk => {{
if (sessions[sk].name === deletedName) {{
delete sessions[sk];
}}
}});
changes[sessionsKey] = sessions;
}}
}}
}}
}}
// 4. _backend_cache_ から削除
if (k.includes('backend_cache') && k.endsWith('workspaces')) {{
var cache = allItems[k];
if (typeof cache === 'string') {{
var parsed = JSON.parse(cache);
var filtered = parsed.filter(w => w.uuid !== targetId);
if (filtered.length !== parsed.length) {{
changes[k] = JSON.stringify(filtered);
}}
}}
}}
}});
chrome.storage.local.set(changes, () => {{
resolve(JSON.stringify({{
success: true,
deletedName: deletedName
}}));
}});
}});
}})
"""
ws.send(json.dumps({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": js, "returnByValue": True, "awaitPromise": True}
}))
resp = json.loads(ws.recv())
result = json.loads(resp['result']['result']['value'])
if result['success']:
print(f"削除完了: {result['deletedName']}")
else:
print(f"削除失敗: {result['reason']}")
ws.close()実行後、Sidekickを再起動すればワークスペースが削除されています。
なぜこの方法で変更できるのか
Sidekickはサービス終了後もローカルにキャッシュされたデータで動作し続けています。ワークスペース名の参照先は以下の優先順位になっています。
1. chrome.storage.local(内部拡張のローカルストレージ) ← ここが実データ
2. Preferencesファイルの sidekick.tab_menu ← 起動中に生成される表示キャッシュ
3. Sidekickサーバー ← サービス終了で接続不可
UIからの変更はサーバーへの書き込みを前提としているため失敗しますが、chrome.storage.local を直接書き換えれば、起動時にそこからデータが読み込まれるため反映されます。
Chrome DevTools Protocol(CDP)を使えば、Sidekickに限らずChromiumベースのブラウザの内部データにアクセスできます。--remote-debugging-port で起動し、WebSocket経由でJavaScriptを実行するというのが基本的なパターンです。
おまけ:「Sidekick Browser is Shutting Down」オーバーレイを消す
サービス終了後、Sidekickを起動するたびに「Sidekick Browser is Shutting Down」というオーバーレイが表示されます。毎回閉じるのは地味にストレスです。
これはSidekick内部拡張の overlay.js が product-shutdown-modal というモーダルを表示しているためです。このファイルを1箇所パッチするだけで消せます。
手順
Sidekickを終了した状態で、以下を実行します。
# バックアップを作成
OVERLAY_JS=~/Library/Application\ Support/Sidekick/Default/Extensions/mcjlamohcooanphmebaiigheeeoplihb/*/overlay/overlay.js
cp $OVERLAY_JS "${OVERLAY_JS}.backup"次に、Pythonで1箇所だけ書き換えます。
import os, glob
# overlay.jsのパスを自動検出
pattern = os.path.expanduser(
'~/Library/Application Support/Sidekick/Default/Extensions/'
'mcjlamohcooanphmebaiigheeeoplihb/*/overlay/overlay.js'
)
overlay_path = glob.glob(pattern)[0]
with open(overlay_path, 'r') as f:
content = f.read()
# shutdownモーダルの表示コンポーネントインデックスを-1(該当なし)に変更
old = '"product-shutdown-modal"?33'
new = '"product-shutdown-modal"?-1'
if old in content:
content = content.replace(old, new)
with open(overlay_path, 'w') as f:
f.write(content)
print("パッチ適用完了")
else:
print("パッチ対象が見つかりません(バージョンによりインデックスが異なる場合があります)")仕組み
overlay.js はSvelteで書かれたモーダル表示のルーターです。モーダルIDに応じてコンポーネントのインデックスを返す分岐があり、product-shutdown-modal の場合にインデックス 33 が返されます。
これを -1 に変えると「該当するコンポーネントなし」として扱われ、モーダルが表示されなくなります。
注意点
Sidekickが自動アップデートした場合、拡張のバージョンディレクトリが変わり、パッチが元に戻る可能性があります。ただし、サービス終了しているため自動アップデートが走ることはほぼないでしょう。
万が一元に戻したい場合は、バックアップから復元してください。
OVERLAY_JS=~/Library/Application\ Support/Sidekick/Default/Extensions/mcjlamohcooanphmebaiigheeeoplihb/*/overlay/overlay.js
cp "${OVERLAY_JS}.backup" "$OVERLAY_JS"おまけ2:カスタムWebアプリのアイコンを設定する
Sidekickでは左サイドバーにカスタムWebサイトをアプリとして追加できますが、サービス終了後はアイコンの設定・変更ができなくなっています。
これは、アイコン画像をSidekickのCDN(i.meetsidekick.com)にアップロードするAPIが停止しているためです。しかし、chrome.storage.local の _applications 内にある customIconUrl フィールドに任意の画像URLを直接書き込むことで解決できます。
手順1: アプリ一覧を確認する
デバッグモードで起動した状態で、以下のスクリプトを実行します。
import json, websocket, urllib.request
resp = urllib.request.urlopen("http://localhost:9222/json/list")
pages = json.loads(resp.read())
bg = [p for p in pages if p.get('type') == 'background_page'][0]
ws = websocket.create_connection(bg['webSocketDebuggerUrl'])
js = """
new Promise((resolve) => {
chrome.storage.local.get(null, (items) => {
var result = [];
Object.keys(items).forEach(k => {
if (k.endsWith('_applications')) {
var apps = items[k];
Object.keys(apps).forEach(id => {
var app = apps[id];
if (app.customUrl) {
result.push({
id: id,
name: app.name,
url: app.customUrl,
hasIcon: !!(app.customIconUrl || app.iconUrl),
iconUrl: app.customIconUrl || app.iconUrl || null
});
}
});
}
});
resolve(JSON.stringify(result));
});
})
"""
ws.send(json.dumps({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": js, "returnByValue": True, "awaitPromise": True}
}))
resp = json.loads(ws.recv())
apps = json.loads(resp['result']['result']['value'])
for app in apps:
icon = "OK" if app['hasIcon'] else "なし"
print(f" [{icon}] {app['name']} (ID: {app['id']})")
print(f" URL: {app['url'][:60]}")
ws.close()手順2: アイコンを設定する
変更対象のアプリIDと、アイコン画像のURLを指定して実行します。
アイコン画像には以下が使えます。
- 外部の画像URL(https://example.com/icon.png)
- favicon取得サービス(https://www.google.com/s2/favicons?domain=example.com&sz=128)
- Base64 Data URI(data:image/png;base64,...)
import json, websocket, urllib.request
# === ここを変更 ===
TARGET_APP_ID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # 手順1で確認したID
ICON_URL = "https://www.google.com/s2/favicons?domain=example.com&sz=128" # アイコン画像のURL
# ==================
resp = urllib.request.urlopen("http://localhost:9222/json/list")
pages = json.loads(resp.read())
bg = [p for p in pages if p.get('type') == 'background_page'][0]
ws = websocket.create_connection(bg['webSocketDebuggerUrl'])
js = f"""
new Promise((resolve) => {{
chrome.storage.local.get(null, (allItems) => {{
var changes = {{}};
var appName = null;
Object.keys(allItems).forEach(k => {{
if (k.endsWith('_applications')) {{
var apps = allItems[k];
if (apps["{TARGET_APP_ID}"]) {{
appName = apps["{TARGET_APP_ID}"].name;
apps["{TARGET_APP_ID}"].customIconUrl = "{ICON_URL}";
changes[k] = apps;
}}
}}
}});
chrome.storage.local.set(changes, () => {{
resolve(JSON.stringify({{success: true, appName: appName}}));
}});
}});
}})
"""
ws.send(json.dumps({
"id": 1,
"method": "Runtime.evaluate",
"params": {"expression": js, "returnByValue": True, "awaitPromise": True}
}))
resp = json.loads(ws.recv())
result = json.loads(resp['result']['result']['value'])
print(f"アイコン設定完了: {result['appName']}")
ws.close()実行後、Sidekickを再起動すれば左サイドバーにアイコンが表示されます。
Tips: Googleのfavicon APIを活用する
アイコン画像のURLを自分で探すのが面倒な場合は、Googleのfavicon取得APIが便利です。
https://www.google.com/s2/favicons?domain=対象サイトのドメイン&sz=128例えば、example.com のfaviconを128pxで取得する場合は以下のURLになります。
https://www.google.com/s2/favicons?domain=example.com&sz=128まとめ
Sidekickのサービス終了後にワークスペースを操作するには、以下の共通手順で対応できます。
1. Sidekickをデバッグモード(--remote-debugging-port=9222)で起動
2. Chrome DevTools Protocol経由で chrome.storage.local にアクセス
3. _workspaces、_sessions、_app-accounts-by-workspace、_backend_cache_ を整合性を保って更新
4. Sidekickを再起動
操作ごとの更新対象は以下の通りです。
| 操作 | _workspaces | _sessions | _app-accounts | _backend_cache |
|---|---|---|---|---|
| 名前変更 | 名前・アバター更新 | 名前更新 | - | 名前更新 |
| 追加 | エントリ追加 | - | 空エントリ追加 | エントリ追加 |
| 削除 | エントリ削除 | エントリ削除 | エントリ削除 | エントリ削除 |
また、起動時に毎回表示される「Sidekick Browser is Shutting Down」オーバーレイは、内部拡張の overlay.js を1箇所パッチするだけで消せます。カスタムWebアプリのアイコン設定も、_applications の customIconUrl を書き換えれば対応できます。
Sidekickをまだ使い続けている方の参考になれば幸いです。


