【実装】herdr app のサイドバーを作り替える — 折りたたみ時の番号だけ表示と「+」ボタンを直感的に

この記事でわかること
- 同梱 mod を複製してレイアウトから差し替える手順(複数 mod をまとめて)
- 折りたたみはフラグではなくサイドバーの幅(160px 未満)で判定されていること
- 番号だけの一覧を、色付きの頭文字・短い名前・ツールチップにする方法
- 「+」ボタンを何を作るか分かる形にした例と、一度失敗した配置
- スクロールバーをスクロール中だけ出す mode("scrolling")
macOS / herdr app(2026年10月時点の版)。mod の基本と、ここで使う my-layout(main レイアウトの複製)は前の記事(Files パネルの拡張)で作っています。
herdr app を使い始めて数時間で、画面の細かいところが気になり始めました。サイドバーを折りたたむと「1, 2, 3 … 11」と番号が並ぶだけで、どれがどのプロジェクトか分かりません。「+」ボタンが3か所にあって、それぞれ何を作るのかも見た目では区別がつきません。
前の記事で Files パネルを複製して拡張したのと同じやり方で、サイドバーとタブバーも作り替えました。
この記事でやること
- spaces・agents・tabs・workspace-chrome の4つの同梱 mod を複製して差し替える
- 折りたたんだサイドバーで space とエージェントを見分けられるようにする
- 「+」ボタンとスクロールバーを直す
はじめに
herdr app のサイドバーには space(プロジェクトごとの作業場所)とエージェントの一覧が並びます。幅を狭めた「折りたたみ」状態もあって、ターミナルを広く使いたいときはこちらにしています。ただ、この状態の表示が番号だけで、結局広げて確かめることになっていました。
対象読者
- herdr app の画面を自分好みにしたい人
- 前の記事で mod の基本を読んだ人
4つの mod をまとめて複製する
サイドバーとタブバーは、それぞれ別の同梱 mod でした。
| mod | 場所 |
|---|---|
spaces |
サイドバー上半分の space 一覧 |
agents |
サイドバー下半分のエージェント一覧 |
tabs |
上部のタブバー |
workspace-chrome |
サイドバー最上段と最下段(検索・開閉・設定) |
どれも起動のたびに元に戻るので、my- を付けた複製を作り、my-layout の参照先をまとめて置き換えました。前の記事のスクリプトと同じ要領で、対象を複数にしただけです。
TARGETS = {"spaces", "agents", "tabs", "workspace-chrome"}
def walk(x):
if isinstance(x, dict):
c = x.get("content")
if isinstance(c, dict) and c.get("mod") in TARGETS:
c["mod"] = "my-" + c["mod"]
for v in x.values(): walk(v)
elif isinstance(x, list):
for v in x: walk(v)アイコンなどの SVG は元の mods/workspace-chrome/ にあるものをそのまま参照しています。同梱 mod のフォルダは消えないので、複製側にアイコンを持たせなくても表示できます。
折りたたみは「幅」で判定されている
コードを読んで分かったのが、折りたたみはフラグではなくサイドバーの幅が 160px 未満かどうかで判定されていることでした。端をドラッグして狭めても同じ表示になります。
// The sidebar's rail goes by its width, not a flag: dragging its edge narrows it too.
const narrow = () => (find(layout(), "sidebar")?.size?.px ?? Infinity) < 160;開閉ボタンは、広いときの幅を共有状態に覚えておいて、macOS では 76px、それ以外では 44px に縮めます。macOS だけ広いのは、左上のウィンドウボタン(赤・黄・緑)を収めるためです。
function toggleSidebar() {
const width = find(layout(), "sidebar")?.size?.px;
if (!narrow()) setShared("sidebar.width", width);
setShared("sidebar.collapsed", !narrow());
run("layout.resize", { id: "sidebar",
px: narrow() ? (shared("sidebar.width") ?? 268) : window()?.platform === "macos" ? 76 : 44 });
}space 一覧を見分けられるようにする
広げた状態の space 一覧には、色付きの頭文字アイコンがあります。色は space ごとに共有状態(shared("space.colors"))に保存されていて、どの mod からも同じ色が取れます。折りたたんだときもこのアイコンを使い、下に短い名前を付けました。
compact({ w, i }, t) {
const color = (shared("space.colors") ?? {})[w.id] ?? "ansi6";
const fill = color.startsWith("#") ? color : (MARK[color] ?? t[color]);
const alert = w.status === "blocked" || w.status === "done";
return v_flex().id(`space-${w.id}`).relative().h(44).items_center().justify_center().gap(3).rounded(8)
.when(w.active, (el) => el.bg(t.selected).border_1().border_color(fill))
.tooltip(`${i + 1}. ${w.name}`)
.on_click(() => run("space.select", { id: w.id }))
.child(h_flex().size(20).rounded(6).bg(fill).items_center().justify_center()
.text_size(11).font_bold().text_color("#ffffff").child([...w.name][0]?.toUpperCase() ?? "?"))
.child(div().w_full().px(3).truncate().text_center().text_size(9.5).child(w.name))
.when(alert, (el) => el.child(div().absolute().top(4).right(8).size(8).rounded(4).bg(t[w.status])));
}詰めた工夫は次のとおりです。
- 頭文字は同じ文字が続きやすい(似た名前のリポジトリが並ぶと全部同じになる)ので、名前も小さく出す
- 名前は入りきらない分を省略し、ツールチップで全体を出す
- 今いる space はその space の色で枠を付ける
- エージェントが入力待ち(blocked)か完了(done)の space は、右上に点を付ける
変更前(同梱の main レイアウト)は、折りたたむと番号が並ぶだけでした。

変更後は、色付きの頭文字と短い名前で見分けられます。今いる space には、その space の色で枠が付きます。

エージェント一覧のほうも番号だけだったので、Claude Code などのプログラムのアイコンに変え、左端にそのエージェントがいる space の色の縦線を入れました。space 一覧と色で対応が取れるので、どの space のエージェントが止まっているかが分かります。
「+」ボタンを直す
画面には「+」が3つありました。
| 場所 | 実際の動作 | 直した形 |
|---|---|---|
| タブバー | 新しいタブ(シェルかエージェント) | 「+ New tab」の文字入りボタン。タブとの間に区切り線 |
| space 一覧の見出し | 新しい space | フォルダに+のアイコン。広いときは「New space」の文字も |
| サイドバー下部 | 別マシンの追加 | サーバーのアイコン |
どれもツールチップに動作とショートカット(「New tab: shell or agent (⌘T)」など)を出しています。gpui-kit の要素には .tooltip(text) があるので、1行足すだけで済みます。
一度失敗した配置
macOS で折りたたむと、最上段はウィンドウボタンで埋まるので、同梱の workspace-chrome は検索と開閉のボタンを隠します。開くにはエージェント一覧の下にある「»」を押す必要があって、これが分かりにくかったんですよね。
そこで、折りたたんだときも space 一覧の一番上に検索と開閉を出すようにしました。最初はこの2つを横に並べ、新規 space のボタンをその下の段に置いたのですが、幅も位置も揃わず、新規 space だけ右にずれて違和感しかありませんでした。横並びの行と、既存の見出し行を流用した行で、余白の付き方が違っていたのが原因です。
最終的には、同じ大きさのアイコンボタンを中央に縦一列で並べる形にしました。ボタンは1つの関数で作って、大きさを揃えています。
railButton(t, id, icon, tip, action) {
return div().id(id).size(30).flex().flex_none().items_center().justify_center().rounded(7)
.text_color(t.muted).hover(s => s.bg(t.hover).text_color(t.foreground))
.tooltip(tip).on_click(action).child(svg(`mods/workspace-chrome/${icon}.svg`).size(16));
}上から検索・サイドバーを広げる・新規 space の順で並べ、その下に短い区切り線を入れて space 一覧と分けています。
.when(collapsed, el => el.child(v_flex().w_full().flex_none().items_center().gap(2).pt(2).pb(6)
.child(this.railButton(t, "rail-search", "search", "Search (⌘P)", () => run("mod.open", { name: "picker" })))
.child(this.railButton(t, "rail-expand", "sidebar", "Expand sidebar", () => toggleSidebar()))
.child(this.railButton(t, "space-new", "folder-plus", "New space (⌘N)", () => run("space.new")))
.child(div().w(28).h(1).mt(4).bg(t.border))))狭い列にボタンを置くなら、横に詰めるより同じ大きさで縦に揃えたほうが落ち着きます。VS Code などのアクティビティバーもそうなっているので、見慣れた形だったのもあると思います。
スクロールバーはスクロール中だけ
同梱の space 一覧とエージェント一覧は、スクロールバーを常に表示する設定(.mode("always"))になっていました。型定義を見ると選べるのは3つです。
export type ScrollbarMode = "scrolling" | "hover" | "always";"scrolling" にすると、macOS の標準と同じくスクロールしている間だけ出ます。自分で書いた mod も含めて、.overflow_y_scrollbar() の後ろにまとめて付けました。ターミナル部分のスクロールバーは app 本体が描いているので、mod からは変えられません。
Tips
- 複製した mod は同梱版の更新を受け取りません。app を更新したら、同梱版との差分をときどき見ておくと安心です
- 画面がおかしくなったら
Ctrl+Shift+Lでレイアウトを順に切り替えて、同梱の main に戻して確かめられます。複製側だけを直せばよいので、試行錯誤が気楽です - 色は
theme()の名前で取るようにしておくと、テーマを変えても自然に追従します
まとめ
- サイドバーとタブバーは spaces・agents・tabs・workspace-chrome の4つの同梱 mod で、複製してレイアウトからまとめて差し替えられる
- 折りたたみはサイドバーの幅(160px 未満)で判定されている
- 折りたたんだ一覧は、色・頭文字・短い名前・ツールチップで見分けられるようになる
- 「+」は何を作るかでアイコンと文字を分ける。狭い列のボタンは同じ大きさで縦に揃える
- スクロールバーは
.mode("scrolling")でスクロール中だけ表示できる


