インディーハッカー道
サービス終了したSidekickブラウザのワークスペースを変更・追加・削除する方法
@SOHO記

サービス終了したSidekickブラウザのワークスペースを変更・追加・削除する方法

·25分で読める
平城寿
平城 寿(ひらじょう ひさし)
@SOHO創業者 / インディーハッカー

この記事のポイント

  • 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 -5

JSONが返ってくれば接続成功です。

手順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.jsproduct-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アプリのアイコン設定も、_applicationscustomIconUrl を書き換えれば対応できます。

Sidekickをまだ使い続けている方の参考になれば幸いです。

シェア
#Sidekick#ブラウザ#Chromium#DevTools#ワークスペース
平城寿
平城 寿(ひらじょう ひさし)
インディーハッカー。2004年に@SOHOを創業し、20年間1人で運営。
フォロー

関連記事