Skip to content

2 screen

Claude Lin & Lay edited this page Oct 18, 2026 · 1 revision

画面の設計

画面の人の発言は右に寄せる(#185)

部屋の画面で、画面の人の発言だけを右端に寄せる。セッション(AI)の発言と通知の畳みは左のままである。チャットアプリと同じ見え方であり、誰の発言かを位置で見分けられるようにする(Master 要望、2026-09-27)。変わるのは画面の見え方だけであり、部屋・ログ・端末への入力・保存形式は変えない。

  • 判定は名前で行う。 発言者の名前が、この画面が名乗っている名前(アカウントの名前、無ければ human)と一致する行を右に寄せる。ログから読み戻した行は own を持たず(「部屋のログ」)、ライブの行と読み戻した行で寄せ方を食い違わせないには、行そのものが運ぶもので決めるしか無い。#193 でログがアカウントも運ぶようになったが、寄せ方はそれを読まない:アカウント id で決めれば、画面の人のアカウントを作り直した後や、別の画面から同じ config を開いた場合に、自分の言った行が左へ移る。ライブの行だけ own で決めると、同じ行がトピックを選び直したときに反対側へ移る。拒否が返した発言の描画も同じ規則による。
  • 名前で決めることの代償を受け入れる。 寄せ方が言うのは「この画面の名前で言われた行」であって「この画面の接続が言った行」ではない。この画面と同じ名前で入った別の参加者の行も右に出る。改名すれば、旧名で言った行は描き直した時点で左へ戻る。接続で決める own(「部屋プロトコル」)は色の梯子が読むままであり、寄せ方にだけ使わない。
  • 通知は常に左である。 畳まれる行(mcp アカウントの行、#193)は、画面の名前が何であれ畳みに入り、右には寄せない。
  • 行は塊ごと寄せる。 本文は左から読む向きのまま、塊の幅は行の長さに合わせる(上限は他の行と同じ 46em)。#225 までは色の線が寄せた側(右端)へ移り、名前と時刻の見出しも右から並んでいた。
  • 右の行は丸印も名前も持たない(#225)。 他の行の左の色線は頭文字の丸印に代わったが(下記「会話面の見た目」)、右に寄せた行には丸印を置かず、名前も出さない。位置と吹き出しの色(#187)が誰の行かを既に言っている。宛先があれば @宛先 を吹き出しの頭に、時刻は吹き出しの足に置く。
  • 続けて送った行は一塊である(#225)。 右の行が間に何も挟まずに続く間は吹き出しを詰めて並べ、時刻はその連なりの最後の一つにだけ出す。日の区切り線や他の参加者の行が挟まれば、連なりはそこで切れる。判定は画面の描き方だけで行い(.message.mine が次の .message.mine を持つか)、行やログには何も足さない。

画面の人の色は描いた行にもその場で届く(#189)

画面の人のアカウントの色を変えて保存すると、アプリを再起動しなくても、既に描かれている画面の人の行(ライブ・読み戻しとも)と、この後の発言の両方がその場で新しい色になる(Master 要望、2026-09-27)。部屋のログの保存形式(当時の五欄)は変えない。

  • この後の発言は席の宣言し直しで変わる。 画面の人のアカウントを保存すると room_join を呼び直し、名前・色・アカウントを席に宣言し直す(#40 以来の経路)。発言の hue は席から刻まれるため、保存の後に言った行は新しい色で届く。
  • 描いた行は一つの値を指す。 行の色は描いた時点で行に固定されていたため、宣言し直しは既に描いた行へ届かなかった。画面の人の行(isMine が右に寄せる行)だけは、行ごとの色ではなく部屋に置いた一つの値を指し、その値はアカウントの保存と起動時に画面が書く。画面の人の色は部屋に聞かずとも画面が知っている——アカウントの色である——ため、この一組だけがこの形を取れる。
  • 読み戻した行も同じ色になる。 ログは hue を持たないが(「部屋のログ」)、画面の人の行はログではなくアカウントから色を得る。宣言が無ければ自分の accent であり、色の梯子(下記)を自分として読んだ結果である。#187 の「ライブと読み戻しで同じ色」は、画面の人の行についてこれで成り立つ。
  • 名前で決めることの代償は寄せ方と同じである。 どの行が画面の人の行かは名前で決まるため(上記「画面の人の発言は右に寄せる」)、この画面と同じ名前で入った別の参加者の行も、右に寄るのと同じくこの画面の色で出る。
  • AI アカウントの色は対象外である。 その席の宣言はセッション起動時の hello にあり、変えた色は次の起動から届く。

会話面の見た目(#225)

2026-10-02 の対話で描いた試作に沿う(Master「大分いい感じ」)。変えたのは見た目だけであり、部屋・ログ・保存形式・送る中身は変えない。

誰の行かは頭文字の丸印で言う。 行の左端の色線に代えて、発言者の名前の一文字目を丸の中に置く。丸の地は発言者の色をページの地に薄く混ぜたもの、文字は発言者の色そのものである——吹き出しの地(#187)と同じ作り方であり、ライトとダークの両方で文字が読める。ライトでは #235 で混ぜ方が変わった(下記「ライトの色」)。色は色の梯子(下記「参加者ごとの色」)のままであり、参加者パネルの行も同じ丸印を出す(#253、下記「参加者パネル」)。丸印は読み上げから外す(名前が隣に在る)。通知の畳み(#169 / #193)の見出しにも同じ丸印を置く。

画像を持つアカウントの丸印は、頭文字の代わりにその画像を出す(#236)。 画像は丸く切り抜き、丸の大きさいっぱいに出す。地の薄い色は画像の下に残るため、角の透けた画像は発言者の色の上に載る。どの行の丸印も、その行のアカウント id で画像を引く——名前では引かない。アカウントを持たない話者、画像を持たないアカウント、読み込めなかった画像は頭文字のままである。画像の持ち方と置き場所は「アカウントは画像を持てる」。

アプリ自身の発言の丸印は、頭文字の代わりにアプリのアイコンを出す(#340)。 利用上限の知らせ(#294)など、発言者 Pullcept(自動通知) でアカウントを持たない発言である。マスターの希望(2026-10-08 の部屋)。決めるのは部屋が発言に付けた印(from_app、「部屋のログ」)であり、名前ではない——Pullcept と名乗る参加者の行は頭文字のままである。印はログにも残るため、読み戻した行と拒否が返した行も同じアイコンになる。絵はタイトルバーと同じ src-tauri/app-icon.svg——src-tauri/icons/ の全部を作る元の絵(#246)——であり、新しい絵は作らない。丸に切り抜かず、丸の中に収まるよう内側に余白を取って丸ごと出す。絵は自分の端まで描かれており、切り抜けば吹き出しの角が欠けるためである。地は他の丸印と同じく発言者の色の薄い地である。

丸印の大きさは会話面の文字サイズに連れる(#250、#258)。 直径と頭文字は丸印自身の em で持ち、会話面の文字サイズ(下記「会話面の文字サイズ」)と一緒に動く。既定の文字サイズでの直径はおよそ 2.66rem(頭文字 1.265em、直径はその 2.1em)である。#258 で、それまでの直径 2.31rem(頭文字 1.1em)から、直径と頭文字をともに 1.15 倍にした。Master「今の会話のアイコンサイズをアカウントのアイコンサイズに。で会話のアイコンもう少しだけ大きくできる?」(2026-10-03)。1.15 倍は「もう少しだけ」を受けた AI の判断である。参加者パネルの丸印は、変える前の会話面の丸印の大きさを受け継いだ(下記「参加者パネル」)——#260 から、その関係(パネル = 会話面の 1/1.15)は UI の倍率と会話面の文字サイズのどの組でも保たれる。

行の見出しは本文と同じ大きさで、名前は参加者の色で書く(#258)。 名前・宛先・時刻の一行(.message .meta)は本文と同じ文字の大きさであり、自分の吹き出しの下の時刻も同じである。それまでは見出しが本文の 0.78em、吹き出しの下の時刻が 0.72em だった。名前はその参加者の色(--speaker、アカウントで選んだ色、選んでいなければ名前から導いた色)で書く。それまでは --fg だった。時刻は --muted のままである。Master「チャット欄の名前の部分のフォントサイズを文章のフォントサイズと同じにしてほしいのと。アカウント設定で設定した色にしてほしいんだけど。」、続けて「時刻とあて先も文章と同じサイズがいいかも。」、自分の吹き出しの下の時刻について「そっちもそろえちゃって!」(2026-10-03)。宛先は #452 で、アットマークと名前を間を空けずにつないだ形にし、その宛先の人の色で書くようにした(それまでは「→ 宛先」を発言者の色で書いていた)。宛先が複数なら一人ずつ空白で区切って並べる。色は名前の色と同じ規則で引く——この画面の人なら自分の色、表示中のトピックに居る人ならその人の色、居なければそのアカウントの色、どれでもなければ名前から導いた色である。見出しは宛先の並び(to)から描くもので、送る本文の中の @名前 が宛先へ変わる仕組みとは関係しない。Master「矢印の部分を@にしたいんだよ」「できるなら頼む!」(2026-10-17)。

行間を広げる。 行と行の間を 0.75rem から 1.15rem へ、本文の行の高さを 1.6 から 1.7 へ広げた。

日付は区切り線で言う。 日が変わるところに、左右に線を引いた日付の行を一つ置く。各行の時刻は時と分だけになった——読み戻した行も同じである(#48 以来の「時刻には日付も出す」は区切り線へ移った)。日付の言い方はトピックの一覧と同じであり(今日 / 9月28日、今年でなければ年を付ける)、時刻の全文は時刻のツールチップに出る。区切り線は描いた時点の「今日」で言うため、新しい区切り線を描くたびに画面上の区切り線をすべて言い直す。開いたまま日をまたいでも「今日」が二つにならない。

画面の人の行は一塊にする。 上記「画面の人の発言は右に寄せる」の二項目(#225)。

上の帯は開いているトピックの名前と人数を言う。 タイトルバーの、ボタンの間の空いていた場所に置く(AI 判断)。名前はトピックの一覧の行と同じ語で出し(題の無い行は「未記入」)、索引にまだ無いトピック——起動や「新規」の後、最初の発言の前——は「新しいトピック」と出す。人数は部屋の名簿の人数、つまり今そのトピックに繋がっている者の数であり、起動していないアカウントと席を取らない MCP サーバ(#193)は数えない。名前は参加者パネルのものであり、帯には出さない。

タイトルバーは窓の最上段である(#252)

窓に Windows の標準の枠は無く、画面のタイトルバーが窓の最上段に立つ。それまでは標準の枠の最上段(アイコンと三つのボタン)の下に、このタイトルバーが二段目として並んでいた。Master「あとアプリの二段目のメニューバーを最上段に入れつつ最上段の全高をちょっとだけ上げて、色もサイドバーに合わせられる?」、続けて「じゃあ全高はそのままで」(2026-10-03)。枠を外した仕組みと、Tauri の側で確かめたことは「窓の枠は画面が描き、題は残す」。

一段にまとめ、高さは変えない。 帯の高さは二段目だった頃のタイトルバーの高さのままである(Master「じゃあ全高はそのままで」)。消えたのは標準の枠の一段であり、その分だけ会話面と左右の面が縦に広がる。

並びは、左端にアイコン、右端に窓の三つのボタン。 間に置く操作——トピックの開閉、トピック名と人数、端末、設定、アカウントの開閉——の並びと働きは変えていない。アイコンは src-tauri/app-icon.svg、つまりタスクバーのアイコンを作る元の絵そのものであり(#246)、帯のボタンの線画と同じ大きさで出す。飾りであって読み上げには出さない。窓の名前は題が持つ(#248)。

地はサイドバーと同じ --surface、下の境は --line。 帯と左右の面が一つの枠として部屋を囲む。

窓の三つのボタンも #221 のボタンである(AI 判断)。 最小化・最大化(元に戻す)・閉じるは線画だけで描き、枠も地も持たず、カーソルの下で --hover が出る。違うのは二点である。一つは、帯の高さいっぱいに伸び、帯の上端と右端に接していること。カーソルを窓の右上の角へ投げれば閉じるに当たる——標準の枠のボタンと同じ置き方である。帯の高さはほかのボタンが決めており、端まで伸ばしても帯は高くならない。もう一つは、閉じるだけがカーソルの下で赤(--close-ground、字は --close-ink)になること。標準の枠の閉じると同じ合図であり、ライトとダークで同じ値である。最大化は窓が最大化している間「元に戻す」と名乗り、絵も重なった二つの四角に変わる。名前と絵は窓の大きさが変わるたびに窓へ問い直す——帯のダブルクリック、Win+↑、画面の上端への引き寄せも最大化するが、どれもこのボタンを通らない。

帯の空いているところを掴んで窓を動かせる。ダブルクリックで最大化と元に戻すが入れ替わる。 トピック名と人数の上、アイコンの上も掴める。ボタンの上では掴まず、押したボタンが働く。

トピック名はダブルクリックで名前を変えられる(#469)。 名前の上だけはダブルクリックが最大化にならず、その場に一覧の行と同じ欄が開く。Enter で決定、Escape と欄の外を押すと取消、空のままなら変えない——一覧の行の名前変更と同じ三つの出口であり、同じ欄を使う。名前の上を押して引けば、帯のほかの所と同じく窓が動く。まだ一覧に載っていない「新しいトピック」は直せない(直す行がまだ無い)。

閉じるは今のアプリの終了の流れを通る。 下記「アプリの終了」。

失ったもの。 Windows 11 で最大化ボタンにカーソルを乗せると出るスナップの配置の一覧は出ない(受け入れた損失、AI 判断)。キーボードの Win+矢印と、窓を画面の端へ引くスナップは、標準の枠と同じ経路を通る。理由は「窓の枠は画面が描き、題は残す」。

二つの地色

面には地色が二つある。--bg(ライトで #ffffff)が窓の地であり、--surface(同 #fbfbf9、#235 までは #f4f4f7)が面そのものの色味である。ボタンはそのどちらも自分の地として持たない。

添付の札・ダイアログのログと注記・宛先の候補の選択中の項目は、--surface ではなく --tint(ライトで #f4f4f7、ダークで #202026)で塗る。#235 で面の地が窓の地にほぼ並ぶ色になり、--surface で塗った札は白の上で見えなくなるためである。値は #235 までの --surface のままであり、見た目は変わっていない。

ボタンは枠も塗りも持たない(#221、Master 判断 2026-10-02)。 画面のボタンは線画のアイコンだけで描き、普段は枠も塗りも無い。カーソルを乗せたときだけ薄い塗り(--hover)が出る。参考は Claude Desktop のタイトルバーのボタンである。対象は画面に載るボタンのすべて——タイトルバー、トピックの一覧(行・「新規」・削除)、参加者パネル(見出しの追加・行・行の操作とそのメニューの項目・セッション ID のコピー)、端末面のヘッダ(タブ・タブの閉じる・面を畳む)——である。

名前とキーはツールチップで出す。 語を持たないボタンは、何をするかを title(ツールチップ)と aria-label(読み上げ)で言う。キーを持つ操作は名前の後ろに括弧で添える——「トピックの開閉(Ctrl+B)」「送信(Enter)」。キーを持たない操作には何も添えない。

設定・添付・送信の線画は試作の形である(#234、Master 判断 2026-10-02)。 タイトルバーの「設定」は歯車、添付は斜めに傾いたクリップ、送信は紙飛行機で描く。三つとも 2026-10-02 の試作が使った Tabler Icons(outline、MIT ライセンス)の settings / paperclip / send を、その 24 の格子から画面の 16 の格子へ写したものであり、ライセンスの表記は NOTICE.txt に置く。#221 は「設定」を三本のスライダー、送信を上向きの矢印で描き、#223 は添付を立てたクリップで描いていた。スライダーにしたのは参加者パネルの行の編集の ⚙ と分けるためだったが、その ⚙ は #224 で行のメニューへ移り、行には無い。メニューの「編集」の歯車は自前の線画のまま残っており、「設定」の歯車とは形が違う。

送信だけは塗る。 送信 は 塗ったボタンとして残す。画面が前に出す操作を一つに絞るためである。他のボタンはすべてカーソルを待ってから現れる。

送信の地はアプリのアイコンの青である(#258)。 地はライトとダークのどちらでも #1e9be8——タスクバーのアイコン(src-tauri/app-icon.svg、#246)の青——であり、線画は白である。Master「さらに細かいんだけど、pullceptのアイコンの青色に。送信ボタンも合わせることって可能?」(2026-10-03)。それまでは --accent(ライト #185fa5、ダーク #7aa7d8)で塗り、線画は --bg だった。色は専用のトークン(--send-ground / --send-ink)に置き、--accent は変えない——参加者ごとの色は専用の明度・彩度トークンで描く(下記「参加者ごとの色」)。カーソルの下では地を黒へ 15% 寄せ、ライトとダークのどちらでも少し濃くなる。それまでは --fg へ寄せており、ダークでは明るくなっていた——地が両方の面で同じ一色になったため、寄せる先も一つに固定した。

カーソルの塗りは地の上に重ねる影であって、色ではない。 カーソルの塗り --hover は半透明の黒(ダークでは白)であり、:root とダークの両方に定義する。ボタンが --bg の上にあっても --surface の上にあっても、同じだけ持ち上がる。旧規則(下記)が枠で守っていた「操作はどの面に載っていても一つの形である」は、この形で保たれる。選択の塗り --selected も #235 までは同じ作りだったが、ライトでは Claude デスクトップのサイドバーの選ばれた行の色 #edece8 になった(下記「ライトの色」)。ライトの二つの地はどちらもほぼ白であり、一つの色でも両方の上で同じに読める。ダークは半透明の白のままである。

選択は塗りで言う。 選択中の端末タブ、参加者パネルの選択中の行、トピックの一覧の選択中の行(と、索引に無いトピックに居る間の「新規」)は --selected で塗る。--hover より一段濃く、カーソルの下の行が選ばれた行と読まれないようにする。端末タブは加えて文字色が --muted から参加者自身の色へ変わる。三つは同じ選択を同じ手で描く(#75)。設定の項目の列の、開いている項目の印も同じ塗りである。

欄は枠を残す(#221、AI 判断)。 入力欄・テキストエリア・選択肢は --bg を地に持ち、--line の枠で輪郭を持つ。ボタンは線画そのものが操作であることを言うが、空の欄には言うものが無い——枠を外すと、どこへ打ち込むのかが地と見分けられなくなる。

ダイアログの語のボタンはこの規則の外にある(#221、AI 判断)。 アカウントの窓・設定・確認のダイアログの 取消 決定 終了 削除 閉じる 保存 などは語で答えるボタンであり、これまでどおり枠を持ち、行為を担う一つ(決定、危険な側の答え)は塗る。語は枠が無ければダイアログの本文と見分けがつかない。枠を外せるのは、線画が自分で操作であることを言うからである。

状態の札は操作ではなく表示であり、塗ってよい。 「待機」などの札はこの規則に縛られない。

旧規則(2026-08-24 〜 #221)。 それまでは、入力欄・選択肢・テキストエリア・ボタン、そして選択中の端末タブはすべて --bg を背景に持ち、--line の枠で輪郭を持っていた(実機指示、2026-08-24)。操作を地から分けるのは枠であって塗りではない、という規則である。理由は、塗りで分けると同じボタンがタイトルバーでは白の上の灰色、参加者パネルでは灰色の上の灰色になり、操作の見え方が「どの面に載っているか」で変わることだった。選択中の端末タブも同じ理由で枠(未選択の transparent から --line へ)と文字色で選択を言い、参加者パネルの選択中の行と同じ「枠のある窓の地の箱」になっていた(#75)。その前は逆を向いており、パネルの地が --surface なので選択された行は --bg で浮き、端末ヘッダの地が --bg なので選択されたタブは --surface で沈んでいた。#221 はボタンと選択から枠を外したが、旧規則の理由は捨てていない——塗りを地の色ではなく地に重ねる影にしたのは、どの面の上でも同じ持ち上がり方をさせるためである。欄だけは同じ理由のまま枠に残る。

--surface は面の地に残る。 参加者パネルとトピックの一覧と設定の項目の列の地である。面自身の地は変えない——揃えるのは面の中の操作であって、面ではない。

ライトの色(#235)。 ライトの色づかいは 2026-10-02 の二枚目の試作(画面全体の案)に寄せ、左右の列を Claude デスクトップ(ライト)のサイドバーの色にした(Master「Cで、ダークは今のままでいいかな一旦」)。サイドバーの三色(面・選ばれた行・境の線)は Master が貼った画面写真の画素から拾った値である。ダークは変えていない。ライトのために足したトークンは、ダークでは #235 までと同じ見た目になる値を持つ。角の丸みも変えていない(Master「丸みは変えない方がいいね」)。

トークン・場所 ライト(#235) #235 まで
--muted #73726c #6b6b76
--accent #185fa5 #3a6ea5
--line(列の境など) #e2e2e1 #d8d8de
--surface(トピックの一覧・参加者パネルの地) #fbfbf9 #f4f4f7
--selected(選ばれた行。枠なし) #edece8 黒 10%
--hover 黒 4% 黒 6%
--composer-line(入力欄の枠) 黒 20% --line
状態の札(考え中…・出力中) 地 --ok-ground #eaf3de、文字 --ok #3b6d11 文字 #2e7a4e、地はその 14%
それ以外の札(未起動など) 地 --badge-ground #f1efe8、文字 --muted 地は --muted の 14%
頭文字の丸 地は参加者の色 13% を --bg に、文字は参加者の色 75% を黒に混ぜる 地 22%、文字は参加者の色そのもの
自分の発言の吹き出し 地は参加者の色 9% を --bg に、縁は 35% を --bg に混ぜた 1px の線 地 14%、縁なし
  • --hover を薄くした(AI 判断)。 「選択はカーソルより一段濃い」(下記「選択は塗りで言う」)を保つためである。黒 6% のままだと、#fbfbf9 の上のカーソルの塗りが #edece8 とほぼ同じ色になり、カーソルの下の行が選ばれた行と読まれる。
  • 吹き出しの縁は箱の内側に描く。 枠ではなく内側の影で 1px の線を引くため、吹き出しの寸法は変わらない。ダークでは縁の色が吹き出しの地と同じ値になり、線は見えない。
  • 「起動失敗」の札は変えていない。 両方のテーマで、--danger を薄く混ぜた地のままである。

端末のカーソルは文字色で描く(#445)。 端末のテーマは背景に --bg、文字に --fg を渡し、カーソルにも --fg、カーソルの下の文字に --bg を渡す。カーソルの色を渡さないと xterm は #ffffff で描くため、ライトテーマの地の上では見えなくなる(Master 実機、2026-10-17「pullceptのターミナルにカーソルとか表示されない」)。Claude Code も Codex も、入力欄では本物のカーソルを出している。

色は直値で書かない。 「白」はライトテーマでの見え方であって指定ではない。ダークテーマでは --bg は #17171a であり、#ffffff と書けばそこで壊れる。指定は常にトークンで書く。

入力欄(#222)

一つの囲みに本文と操作列を収める(Master 判断 2026-10-02、モックアップで合意)。 入力欄は角の丸い一つの箱であり、上に本文を書く欄、その下に操作列を持つ。操作列は左にアイコンのボタン、右に 送信 を置く。それまでは枠付きの大きなテキストエリアと、その右に独立した 送信 が並んでいた。

枠は箱が持ち、本文の欄は持たない。 箱そのものが欄であり、「欄は枠を残す」(上記「二つの地色」)は箱の枠で満たす。箱の枠は --composer-line であり、ライトでは --line より濃い(#235、上記「ライトの色」)。中の本文の欄(#354 からはエディター、下記「入力欄はコードを書けるエディターである」)は枠も地も持たない——枠が二重になれば、どれが打ち込む先かがかえって読めなくなる。本文に焦点がある間は箱の枠が --accent に変わり、打ち込む先を示す役を本文の欄の縁から引き継ぐ。

操作列のボタンは #221 の形に従う。 左のボタンは枠も塗りも無い線画であり、カーソルの下でだけ --hover が出る。名前とキーはツールチップで出す。送信 だけが塗りのボタンとして右端に残る(「送信だけは塗る」)。

宛先のボタンは @ を打つだけである。 押すとカーソル位置に @ が入り、@ キーを打ったのと同じ候補が開く。カーソルが語に接しているときは前に空白を一つ入れる——語の途中の @ では候補が開かないためである。候補から選んだ名前が宛先になる経路は @ キーと同じであり(#204 / #206)、ボタンが別の経路で宛先を持たせることはない。押しても本文から焦点は外れない。

添付は名前の札で本文の上に並ぶ(#223、Master 判断 2026-10-02、モックアップで合意)。 添付したファイルは、箱の中の本文の欄の上に、ファイルの名前の札として一つずつ並ぶ。札の右端の ✕(「外す」)で外せる。添付は本文と一緒に出ていくものであるため、本文と同じ箱に入れる。送ると札は消え、本文の後ろに保存先のパスが書かれる(「添付は保存したファイルのパスで届く」)。床に拒まれたときと送れなかったときは、本文と一緒に札も戻る。

  • 入口は三つである。 操作列の左の組の先頭の添付のボタン(斜めのクリップの線画、「添付」)がシステムのファイルの窓を開き、選んだファイルを札にする。一度に何個でも選べる。入力欄へファイルを落としても札になる。本文に焦点がある間の Ctrl+V は、クリップボードがファイルや画像を持ち、文字を持たないときだけ札にする——Excel やブラウザでコピーしたものは文字の横に選択範囲の画像も持っており、それを貼るのは文字を貼ることである。
  • 落とす先は入力欄だけである。 ドロップは webview のページへは届かず、Tauri が受けてパスと落ちた位置を知らせる(dragDropEnabled、既定で有効)。画面は位置が入力欄(#composer)の上かどうかで決め、他の場所に落としたものは何もしない。ファイルを入力欄の上に持っている間は、箱の枠が --accent になり、地がうっすら色づく。
  • 札は表示であり、✕ は操作である。 札は --tint で塗る(上記「二つの地色」の「状態の札は操作ではなく表示であり、塗ってよい」)。✕ は #221 の形——枠も塗りも無い線画で、カーソルの下でだけ --hover が出る。長い名前は札の幅(18rem)で末尾を省略し、✕ は押し出されない。全文はツールチップに出る。札も ✕ も UI 全体の軸であり、会話面の文字サイズに従わない。
  • 押しても本文から焦点は外れない。 添付のボタンと ✕ は宛先のボタンと同じく、押下で焦点を奪わない。ファイルの窓を閉じた後とドロップの後は本文に焦点を戻す。
  • 画像の札は名前の代わりに縮小した絵を出す(#318)。 ボタンで選んだ画像と貼り付けた画像(png / jpg / jpeg / gif / webp)は、3.5rem 四方に切り抜いた絵の札になり、✕ は絵の右上の角に丸い地で載る。名前はツールチップと読み上げに残る。絵を押すと、会話の絵と同じ大きな表示が開く(下記「添付は会話の中に絵と札で出る」)。✕ は外すだけで、大きな表示は開かない。ドロップした画像は送るまで名前の札のままである——ドロップはパスで届き、画面は添付フォルダの外のファイルを読まないためである(「添付は保存したファイルのパスで届く」)。

キーの案内は小さく出す。 「@ 宛先 / Enter 送信 / Shift+Enter 改行」を操作列のボタンの間に、小さく --muted で出す。それまではテキストエリアの placeholder に書かれており、一文字目を打った時点で消えていた。案内は一行の高さに収め、幅が足りないときは収まらない案内を末尾から一つずつまるごと落とす——一つの案内を途中で切った形は出さない。placeholder は「部屋に発言する」だけになった。

#222 が変えたのは見た目と配置だけである。 送る中身(添付を除く。添付は上記、#223)、部屋への渡し方、@ の候補・Enter で送信・Shift+Enter で改行の挙動はそのままである。会話面の文字サイズに従うのは今も本文の欄だけであり、操作列は UI 全体の軸に残る(下記「会話面の文字サイズ」)。

添付は会話の中に絵と札で出る(#318)

添付した画像やファイルを、会話面の中でそのまま見られるようにする(Master 要望 2026-10-07。見本は Claude デスクトップ)。変えたのは見え方だけであり、発言の本文・ログ・端末へ届く文字は変えない(「添付は保存したファイルのパスで届く」)。

添付: の段は、パスの文字ではなく絵と札で出す。 発言の本文の末尾に 添付: の段があり、そのパスがすべてこの部屋の添付フォルダの下を指すとき、画面は段の文字を出さず、本文の下に段の並びのまま添付を並べる。画面の人の行では吹き出しの中に右寄せで、他の参加者の行では名前の下に左寄せで並ぶ。添付だけの発言は本文の行を持たない。

  • 画像は縮小した絵で出す。 png / jpg / jpeg / gif / webp を、8rem 四方に切り抜いた絵(object-fit: cover)で出す。大きさは絵が届く前から決まっているため、絵が届いても行の高さは変わらず、読んでいる位置が押し動かされない(AI 判断)。読めなかった絵・壊れていた絵は、何も言わずにそのファイルの札に替わる。
  • それ以外は名前と種類の札で出す。 札は --tint で塗り、名前の後ろに拡張子を大文字で --muted に小さく添える(拡張子が無ければ「ファイル」)。長い名前は札の幅(18rem)で末尾を省略し、全文はツールチップに出る。札は押しても何も開かない——開く先を画面が決めれば、添付フォルダの外へ出る口になりうるためである。
  • 添付フォルダの外を指す段は、文字のまま出す。 AI が書いた自分のパスや、引用された段がそれに当たる。読み替えないパスを隠すことはしない。その文字のパスは、本文のほかのパスと同じくリンクになる(下記「発言の URL とパスは押して開ける」、#349)。
  • 画面が読みにいくのは、添付フォルダの中のファイルだけである。 判定はアプリの側で、パスを実体へ解決してから行う(「添付は保存したファイルのパスで届く」の #318 の項)。

絵を押すと、窓を暗くして大きく見せる。 会話の絵と入力欄の画像の札のどちらからも開く。窓全体を暗くし、絵をその真ん中に、絵そのものの大きさで出す——窓と周りの余白(3rem)に収まらないときだけ縮める。右上に ✕(「閉じる」)を置き、✕・絵の周りの暗い所を押す・Esc のどれでも閉じる。閉じれば焦点は開く前の場所へ戻る(入力欄の札から開いたときは本文の欄)。開くときは暗がりと絵がふわっと現れ、OS の「動きを減らす」がオンなら一度に出る(下記「動き」と同じ線)。新しいライブラリは足していない。

三つのバッククォートで囲んだ部分はコードの枠で出る(#348)

発言の本文と入力欄で、``` で囲んだ部分をコードの枠で見えるようにする(Master 依頼 2026-10-09。見本は Claude デスクトップの入力欄)。入力欄の側は #354 でエディターに作り替えた(下記「入力欄はコードを書けるエディターである」)。この節の入力欄についての記述のうち、文字の後ろの層・等幅の書体・Enter の扱いは #354 で置き換わっている。変えたのは見え方だけであり、発言の本文・部屋の記録・各席の端末へ打ち込まれる文字は変えない。送る本文は ``` の行を含め、打ったとおりである。

どこが枠になるかは CommonMark のコードフェンスに合わせる。 行頭(空白 3 つまで可)に ``` 以上の連続したバッククォートがある行で枠が始まり、同じ数以上のバッククォートだけの行(後ろの空白は可)で閉じる。開く行のバッククォートの後ろにバッククォートがあれば枠にしない——文の途中の ``` や、一行の中で閉じる ```x``` は枠にならない。閉じていない ``` は、そこから最後までを枠にする。 判定は一つの関数(findCodeFences)で行い、発言と入力欄の両方がそれを使う——同じ文が二つの場所で違う形に出ないためである。

開く ``` の後ろは、一語の言語名のときだけ言語名とみなす(#352)。 一語とは、英字・数字と - _ + . # だけでできた、空白を含まない語である(ts・c++・c#・objective-c など。前後の空白は除く)。それ以外の文が続いていれば、それは枠の一行目の中身である——```あ。。。 は「あ。。。」で始まるコードの枠になり、```python title も「python title」が一行目の枠になる。この読み分けは枠の始まりと終わりを変えない。どちらの場合も枠はその行で開き、閉じる行・入力欄の枠・Enter の扱い・添付: の読み分けは言語名のある枠と同じである。CommonMark は後ろの文の最初の語を言語名とするが、日本語の文を書いた人の意図と合わないため、ここだけ外した(Master の実機確認 2026-10-09 による)。

発言では、枠の中を等幅の文字で、改行と空白をそのまま描く。 枠は --code-line の線と --code-ground の地を持つ。この二つは本文色(--fg)を薄く混ぜた色であり、会話の地にも自分の吹き出しの地にも、ライトにもダークにも一組で乗る。``` の行そのものは描かない。開く行に言語名があれば、コードの上に小さく --muted で添え、コードの中には入れない。枠の幅を超える行は折り返さず、枠の中で横に送る——コードの並びを崩さないためである。自分の吹き出しは、そのような行があっても部屋の幅を超えない。枠の外の文は linkifyText へ渡り(#431 からは Markdown として読んだ後の地の文が渡る。下記「発言は Markdown として描く」)、URL とパスがリンクになる(下記「発言の URL とパスは押して開ける」)。枠の中はリンクにしない。HTML としては読まない(文字と、色分けの span だけで描く)。枠の中は色分けする(#354)。 開く行の言語名を使い、言語名が無いか、知らない言語名なら自動で判定する。色分けは入力欄と同じ関数(highlightedCode、src/composer.ts)で行い、同じコードは発言でも入力欄でも同じ色になる。

入力欄の枠は #354 のエディターが描く。 #348 は入力欄をテキストエリアのままにし、文字の後ろの層(#input-fences)で枠を敷き、枠がある間は入力欄全体を等幅にしていた(Master 判断 2026-10-09、案 B)。開いた ``` の中の Enter は改行で、#352 は閉じていない枠の空の行での Enter に閉じる ``` を入れさせていた。この三つは #354 で取り除いた。今の形は下記「入力欄はコードを書けるエディターである」。

枠の中に書いた 添付: は添付として読まない。 閉じていない ``` の後ろに 添付: の段があっても、それは枠の中の文字であり、コードとして描く(「添付は会話の中に絵と札で出る」の読み替えはしない)。アプリが添付を付けた発言でも、本文が閉じていない ``` で終わっていれば、添付のパスは枠の中の文字として出る——書いた人の文字と、アプリが後ろに付けた段を、本文からは見分けられないためである(AI 判断。#348 のレビューの指摘による)。

入力欄はコードを書けるエディターである(#354)

入力欄を、Codex デスクトップアプリの入力欄のようにコードを書ける欄に作り替えた(Master 判断 2026-10-09、案 C)。テキストエリアをやめ、エディターの部品(Tiptap。中身は ProseMirror)を置いた(#input、src/composer.ts)。手本にしたのは確かめた動きだけであり、ほかの製品のコードは写していない。

``` を打つと、その行がコードの枠になる。 行頭で三つ目のバッククォートを打った時点で、バッククォートは消え、その行が枠になる。行の後ろに文字があれば、それも枠の中身になる。枠の中の文字は等幅で、折り返す(入力欄の幅を超える行で横に送ると、カーソルが見えなくなるため。AI 判断)。枠の外の文は会話面と同じ書体のままである。

枠の左上に言語のボタンを置く。 最初は「自動」で、判定した言語があれば「自動(JavaScript)」のように括弧の中に出す。判定は打つ手が止まって少ししてからやり直す。押すと、検索欄のついた言語の一覧が開く(一覧の先頭が「自動」)。検索欄に打つと、言語の名前・識別子・別名で絞り込む。上下の矢印で選び、Enter か押下で決め、Esc か一覧の外を押すと閉じる。決めると枠の色分けがその言語に変わり、ボタンは言語の名前になる。言語は highlight.js のよく使う言語の組(37 種)であり、色分けと判定はこの組で行う。

枠の中は打っているあいだも色分けする。 言語を決めていなければ自動で判定する。言語を決めていない 5,000 文字を超えるコードは判定せず、色を付けない——判定は組のすべての言語でコード全体を読むため、入力欄ではキーを打つたびに重くなる(AI 判断)。言語を決めたコードは長さによらず色を付ける。色は画面の地の色に合わせてライトとダークで一組ずつ持つ(--hl-*、src/styles.css)。発言の枠も同じ色で描く。

キー操作は Codex に合わせる。

  • Enter はいつでも送信である。 枠の中でも、空の行でも送る。日本語入力の変換を確定する Enter と、宛先の候補が開いているときの Enter(候補を選ぶ)は除く。
  • Shift+Enter は改行である。 枠の中ではコードの次の行になり、枠からは抜けない。枠の外では次の行になる。
  • 下矢印: 枠の最後の行(折り返した行は見えている最後の行)で押すと、枠の下の行に出る。下に行が無ければ空の行を作る。
  • 上矢印: 枠のすぐ下の行の最初の行で押すと、枠の最後の行の末尾に戻る。本文の先頭にある枠の最初の行で押すと、枠の上に空の行を作ってそこへ出る——そうしないと、枠より前に書く道が無いため(AI 判断)。
  • Backspace: 枠の最後の空の行で押すと、その行が枠から出て、枠の下の行になる(カーソルもそこへ移る)。枠全体が空なら、枠が消えて空の行になる。中身のある枠の先頭で押しても枠は消えない——すぐ上が空の行ならその行を消して枠を上へ寄せ、そうでなければ何もしない。枠の途中の空の行では、普段どおり行をつなぐ——そこで枠を抜けると枠が二つに割れるため(AI 判断)。
  • #353 で入れた「閉じていない枠の空の行で Enter を押すと枠を抜ける」動きは取り除いた。Enter は常に送信である。

送る中身は今までと同じ Markdown の文字列である。 エディターの中は行と枠の並びであり、送るときに文字列へ直す。枠の外の行はそのまま一行ずつ、枠は ```言語名 で始まり ``` で終わる(言語が「自動」なら ``` だけ)。枠の中の空の行も残る。コードの中に閉じる ``` と読まれる行があるときは、それより長いバッククォートで囲み、受け取った側でもコードが切れないようにする。部屋のフレーム・ログ・各席の端末へ打つ文字の決まりは変えていない。送れなかったときと床に拒まれたときに入力欄へ戻す文字列も、同じ規則(findCodeFences)で行と枠に読み戻す。

貼り付けは文字だけを読む。 枠の外へ貼った文字に ``` の枠があれば枠として入り、空の行も残る。ブラウザなどからの貼り付けでも HTML は読まず、文字だけを使う。枠の中へ貼った文字はそのままコードになる。VS Code からコピーした文字を枠の外へ貼ると、VS Code の言語の枠として入る(Tiptap の動き)。コピーは、一行や一つの枠の中だけならその文字を、行と枠をまたぐなら送るときと同じ文字列をクリップボードへ置く。

今の入力欄にある機能はすべて残す。 @ で開く宛先の候補(候補はカーソルのある行のカーソルより前で探す)、添付(ボタン・画像の貼り付け・ファイルのドロップ、送るときの 添付: の段)、送れなかったときに本文と札を戻すこと、日本語入力、会話面の文字サイズ、ダークテーマは変えていない。元に戻す(Ctrl+Z)とやり直し(Ctrl+Y / Ctrl+Shift+Z)はエディターの履歴で行う。送った後と戻した後の書き換えは履歴に載せない。

箱の高さは中身に合わせて伸びる。 空のときはテキストエリアの rows="2" と同じ二行の高さで、書くほど伸び、窓の高さの 4 割を超えると中でスクロールする。テキストエリアのような手で高さを変えるつまみは無くした(AI 判断)。placeholder(「部屋に発言する」)は、本文が空の一行のときだけ欄の外側の要素が描く(#input[data-empty])——エディターの中身を placeholder のために描き直さないためである。

使うライブラリのライセンス。 入力欄が使うライブラリと、それらが使うライブラリはすべて MIT か BSD-3-Clause である(GPL など縛りの強いものは入れていない)。採用したバージョンごとの名前・ライセンス・著作権表示とライセンス本文は THIRD-PARTY-NOTICES.txt にまとめ、インストーラーに NOTICE.txt と一緒に同梱する(src-tauri/tauri.conf.json の bundle.resources)。このファイルは scripts/third-party-notices.mjs が package-lock.json と各パッケージの LICENSE から作る。

パッケージ 版 ライセンス
@tiptap/core, @tiptap/pm, @tiptap/extension-code-block, @tiptap/extension-code-block-lowlight, @tiptap/extension-document, @tiptap/extension-paragraph, @tiptap/extension-text, @tiptap/extensions 3.31.4 MIT
lowlight 3.3.0 MIT
highlight.js 11.11.2 BSD-3-Clause
prosemirror-model / -state / -view / -transform / -commands / -keymap / -history / -inputrules / -schema-list / -dropcursor / -gapcursor / -changeset / -tables 1.25.12 / 1.4.4 / 1.42.6 / 1.12.2 / 1.7.2 / 1.2.3 / 1.5.1 / 1.5.1 / 1.5.1 / 1.8.4 / 1.4.1 / 2.4.4 / 1.8.5 MIT
orderedmap, rope-sequence, w3c-keyname 2.1.1, 1.3.4, 2.2.8 MIT
devlop, dequal 1.1.0, 2.0.3 MIT
@types/hast, @types/unist(型の定義だけで、画面には入らない) 3.0.5, 3.0.3 MIT

発言の URL とパスは押して開ける(#349)

部屋の発言の本文に出てくる URL とファイルパスを、押せるリンクとして描く(Master 要望 2026-10-09「pullceptのチャット欄でURLとかファイルパスとかリンクさせたい」)。変えたのは見え方と押したときの動きだけであり、発言の本文・ログ・端末へ届く文字は変えない。入力欄と、添付の絵と札(#318)は対象外である。札は今も押して何も開かない。

リンクになるものは二つである。

  • URL は http:// と https:// で始まるものだけ。 大文字と小文字は問わない。file:・javascript: などほかの scheme はリンクにしない。
  • パスは Windows の絶対パスだけ。 ドライブのパス(C:\…、D:/…)と UNC(\\server\share\…)である。相対パスはリンクにしない。\\.\ と \\?\ で始まるもの(装置と verbatim の形)もリンクにしない。
  • 英数字の直後からは始めない。 語の途中や、URL やほかのパスの中にあるドライブ名を、別のリンクとして拾わないためである。

リンクは一続きの文字であり、空白の手前で終わる。 パスに使えない文字(< > " | * ? :)、バッククォート(コードとして書かれたパス)、文を区切る和文の約物 、。「」『』【】 でも終わる。名前にはまず現れず、パスの直後に続けて書いた文には必ず現れるためである(AI 判断)。それ以外の全角文字はパスの一部として扱う。そのため C:\メモ.mdを開いて のように空白を置かずに文を続けると を開いて までがパスになり、押すと「見つかりません」になる。

  • 末尾の句読点・引用符はリンクに含めない。 . , ; : ! ? ' " * > と 。、,.:;!?〉》」』】 である。
  • 閉じ括弧は、リンクの中に対になる開き括弧が無いときだけ外す。 ) ) ] ] } } が対象である。…/Foo_(bar) の括弧はリンクに残り、(C:\資料) の括弧は文へ戻る。

押すと開く。 押すのはマウスのクリックだけであり、右クリック・長押し・キーボードの挙動は増やしていない。リンクの上で文字を選ぶドラッグでは開かない。

  • URL は既定のブラウザで開く。
  • フォルダはエクスプローラーで開く。
  • ファイルは、その種類を開く既定のアプリで開く。
  • 開くと実行されるファイルは、実行せずに場所を示す。 プログラム・スクリプト・インストーラー・ショートカットなど(.exe・.bat・.cmd・.ps1・.vbs・.js・.msi・.lnk・.py・.reg・.iso など)は、エクスプローラーでそのフォルダを開き、ファイルを選んだ状態にする。発言はだれでも書けるものであり、押すことは実行するという判断ではないためである。一覧は crates/post-link の RUNS_WHEN_OPENED にある。拡張子は Windows と同じく、名前の末尾の . と空白を除いてから読む。
  • 存在しないパスは開かず、ステータス行に「見つかりません: <パス>」と出す。 ほかの理由で開けなかったときも、その理由がステータス行に出る。

何を開くかはアプリの側で決め直す。 画面のリンクの切り出しは文字の読み方であり、守りではない。押したリンクはアプリの command(open_post_url / open_post_path)へ渡り、scheme、パスの形、存在するか、開くと実行されるかをそこで判定してから tauri-plugin-opener で開く。画面の capability(opener:default)は広げていない。UNC のパスは存在を確かめるのにサーバーの答えを待つことがあるため、確かめるのは画面の処理の外で行う。

リンクはアクセント色(--accent)に下線で描く。 発言者の色と分けるためである。--accent はダークで値を持ち替えるため、明暗のどちらも同じ規則で描ける。ポインタを載せると下線が太くなる。長いパスは本文と同じ所で折り返す。

リンクを作るのは、地の文を受け取って描く部品を返す関数 linkifyText である(src/main.ts)。 改行と空白は、本文を textContent で描いていたときのまま返る(.message .body は white-space: pre-wrap)。本文の一部を別の形で描くもの(``` の枠など)は、その外側の文字だけをこの関数へ通せる。

HTML の枠を静的にプレビューする(#436)

明示的な html fence は、本文ではコードを描かずファイル名の一行だけを出す。この一行は角丸のカードで、ファイルアイコン・名前・「開く ›」を並べ、ホバーで色が変わる(#439)。名前は、開く行の html の後の文字(```html report.html なら report.html)、無ければ HTML の <title>(空白は一つにまとめる)、それも無ければ「HTML」の順で決める。文字が届いただけでは開かず、その一行を押すと、枠の HTML と CSS を会話の横の一つの領域に描き、領域の見出しにも同じ名前を出す。別の枠を押せば内容が切り替わる。コードはプレビュー側の「プレビュー|コード」の切り替えタブ(押下状態を aria-pressed で持つ)で表示と切り替えて読み、選択・コピーできる。閉じるボタンは × アイコンで、ラベルは「プレビューを閉じて会話に戻る」のまま保つ(#439)。表示に失敗したときは領域がコードを開いて知らせ、コードは失われない。閉じると押した一行に焦点を戻す。狭い画面では会話を覆うが、プレビュー自身の閉じるボタンから会話へ戻れる。本文中の生 HTML は引き続き文字である。投稿の JavaScript は、横の領域でも独立ウィンドウでも下記の範囲で枠の中だけで動く(独立ウィンドウ #443、横の領域 #466)。

左にウィンドウと右上向き矢印のアイコンを添えた「新しいウィンドウで開く」を明示的に押すと同じ内容を一つの独立ウィンドウで読む。独立ウィンドウが開けたと確認できてから横の領域を閉じる(独立ウィンドウは閉じない)。開けなかった場合は横の表示とエラー文を残す。独立ウィンドウには同じ形(アイコンと文字)の「メインウィンドウで開く」があり、押すと独立ウィンドウを閉じ、同じ内容を本体の横の領域に表示して本体ウィンドウに焦点を移す。独立ウィンドウはアプリのコマンドを持たず、この要求は固定のナビゲーションとして本体側が受け取り遮断する。再度押しても増えず、その窓に焦点を移す。枠の切替は開いている窓も更新するが焦点を奪わず、利用者が閉じた窓を自動では開き直さない。トピックを離れると横の領域を消し、独立ウィンドウを破棄する。本体 webview の起動・再読み込み時にも残っている独立ウィンドウを閉じ、前のトピックの表示を引き継がない。本体の破棄時にも独立ウィンドウを閉じる。通常の終了確認を取り消した場合は本体もプレビューも残る。プレビューは保存しない。

独立ウィンドウも標準の枠を持たず、横の領域の見出しをタイトルバーの段に置く(#468)。 Master の要望(2026-10-18)。本体と同じく decorations(false) で開き(src-tauri/src/html_preview.rs)、最上段は左からファイル名・プレビュー|コード・メインウィンドウで開く、右端に窓の三つのボタン(最小化・最大化(元に戻す)・閉じる)を並べる。「HTML プレビュー - Pullcept」はどこにも描かない。窓の題(タスクバーと Alt+Tab に出る名前)は「ファイル名 - Pullcept」で、名前が無ければ「Pullcept」。三つのボタンの線画・幅・閉じるの赤は本体の帯(タイトルバーは窓の最上段である)に合わせる。注記の段落は帯の下に残す。プレビュー|コードは横の領域の切替と同じ働きで、コード表示は投稿の元のコードを textContent で出すだけであり(色分けはしない)、枠も script も使わない。名前とコードは本体が html_preview コマンドの name・code 引数で渡し、Rust 側が文書と一緒に eval で JSON 文字列として渡す。横の領域の子ビュー(?pane)では帯もコード表示も描かない。

独立ウィンドウの窓の操作は、capability を足さずに固定のナビゲーションで本体側が行う。 独立ウィンドウには capability が無く(#449/#465/#467)、本体の帯が使う窓の API(data-tauri-drag-region の script も plugin:window を呼ぶ)は ACL で拒否される。そこで「メインウィンドウで開く」(#463)と同じく、帯の操作は /html-preview-window/{drag,toggle-maximize,minimize,close} への移動として出し、窓の on_navigation がそれを止めて Rust 側で start_dragging・最大化/元に戻す・minimize・close を行う。帯の空いているところ(ファイル名の上も含む)の左ボタンの押下で窓を動かし、二度目の押下(ダブルクリック)で最大化と元に戻すが入れ替わる——Tauri 2.11.5 の window/scripts/drag.js が Windows で行うのと同じ判定で、ボタンの上では掴まない。動かすのは OS の移動の流れ(WM_NCLBUTTONDOWN/HTCAPTION)なので、画面端への引き寄せ(Aero Snap)は本体の窓と同じに働く。最大化のボタンの名前と絵は、窓の大きさが変わるたびに Rust 側が is_maximized を eval で知らせて入れ替える。投稿の枠は自分の枠しか移動できず、その移動も frame-src about: で止まるため、枠からこれらの操作は起こせない。Windows 11 の最大化ボタンに重ねて出るスナップレイアウトの一覧は、本体の窓と同じく出ない(OS の当たり判定 HTMAXBUTTON を返す仕組みが無い)。

横の領域の中身は本体ウィンドウにはめ込んだ子ビューが描く(#466)。 見出し(名前、プレビュー|コード、新しいウィンドウで開く、×)とコード表示は本体の DOM に残し、中身の表示領域(.html-preview-content)の上にだけ、Tauri の child webview(ラベル html-preview-pane、src-tauri/src/html_preview.rs)を重ねる。本体のページには枠を置かない。子ビューの中身は独立ウィンドウと同じ固定の html-preview.html で、?pane を付けて読み、独立ウィンドウ用の見出しと注記を描かず枠だけを出す。文書は独立ウィンドウと同じく window.eval で渡す。本体は表示領域の位置と大きさを物理ピクセル(getBoundingClientRect() × devicePixelRatio)で、既存の html_preview コマンドの pane 引数として送る(新しい command は足さない)。送るのは変わったものだけで、文書は切り替えたときだけ、位置と大きさはパネルの開閉・幅の変化・ウィンドウの大きさの変化(ResizeObserver と resize)・コード表示への切り替えのたびに送る。隠すときは位置を送らず、子ビューは文書を持ったまま隠れる。閉じる・トピック切替・本体の起動と再読み込みでは空の文書を送って隠す。子ビューの最上位も on_navigation で固定の /html-preview.html 以外への移動を止める。

子ビューは本体のページより常に手前に描かれる。 本体の DOM の要素は z-index に関係なく子ビューの下になる。そこで、横の領域に重なりうるもの——開いている <dialog>(設定、アカウント、終了確認、添付画像の表示など)、アカウントのメニュー(#account-menu)、コードの言語の一覧(.lang-menu)——が開いている間は子ビューを隠し、閉じたら戻す(src/html-preview.ts の OVERLAYS、open・hidden 属性の変化を見る)。ここに無い浮く要素を横の領域に重ねるときは OVERLAYS に足す。子ビューに焦点がある間、Escape などのキーは子ビューへ行くため、横の領域を閉じるのは見出しの × で行う。本体ウィンドウが webview を二つ持つ間、Tauri は本体のファイルのドロップをウィンドウ宛てにだけ届けるため、入力欄へのドロップはウィンドウの購読(getCurrentWindow().onDragDropEvent)で受ける。Rust 側も本体を get_webview_window では引けなくなるため、html_preview は呼び出し元の webview のラベルを確かめ、本体ウィンドウは get_window で引く。

src/html-preview-document.ts は DOM を作らない parse5 で投稿を読み、静的な HTML 要素と表示用属性だけを組み直す。見出し、段落、表、リスト、レイアウト用要素、style 要素とインライン CSS を扱う。URL属性・フォーム・画像・外部 CSS・meta/base・iframe/object/embed・SVG/MathML は残さない。リンクは表示だけである。script とイベント属性(on*)は残す(#443)。横の領域と独立ウィンドウは同じ文書を使う(#466 で横の領域向けのスクリプト無しの文書は無くした)。残すのはインラインの classic/module script だけで、src 付き・import map・speculation rules などの script は落とす。script の中身は <!-- と <script・</script の < を \x3C に置き換え、parse5 が閉じた位置で閉じる。html 要素のイベント属性は CSP より前に来るため残さない。html と body の安全な class/id 等と body のインライン CSS は保持する。html のインライン CSS は CSP より後の html ルールへ移すため、元のインライン指定より詳細度が低くなる場合がある。

既知の文書の先頭に固定 CSP を置き、その後だけに投稿の CSS と要素を置く。通信・画像・フォント・フレーム・フォーム・base を禁止し、インライン CSS だけを許す。CSS の URL も許可しない。スクリプトは script-src 'unsafe-inline'(インラインだけ)とする。投稿は opaque-origin の srcdoc iframe に入れ、親 DOM へ挿入しない。枠の sandbox は allow-scripts だけを足す(allow-same-origin・allow-top-navigation*・allow-popups・allow-forms・allow-modals は付けない)。枠内のスクリプトは自分の枠の DOM だけを書き換えられ、fetch などの通信は CSP で止まり、親や top の document には届かない。srcdoc の枠は html-preview.html の CSP も引き継ぐため、その script-src に 'unsafe-inline' を足した(html-preview.html は固定の文書で、投稿を自分の DOM に入れない)。スクリプトは自分の枠を location で移動でき、その判定は親の frame-src で行われるため、html-preview.html の frame-src は about: だけにして、外部へも同じオリジンのページへも枠を移動させない。Windows の WebView2 では Tauri の初期化スクリプトが枠の中にも入り、枠から __TAURI_INTERNALS__.invoke を呼べる(#443 の調査)。独立ウィンドウにも横の領域の子ビューにも capability が無いため、その要求は ACL で拒否される。これがスクリプトを許す根拠である。本体のページの中の iframe では main の権限で通ってしまうため、横の領域は子ビューにした(#466)。ACL は「webview が一致、または window が一致」で許すため(tauri 2.11.5 の src/ipc/authority.rs)、main の capability はウィンドウ(windows)ではなく本体の webview(webviews: ["main"])を指す。ウィンドウで指すと、同じウィンドウにいる子ビューにも届く。独立ウィンドウと子ビューのトップ文書は固定の html-preview.html であり、投稿の文書へ移動しない。専用 Rust コマンドは本体の webview(ラベル main)からだけ受け付ける。子ビューのウィンドウも main なので、ウィンドウのラベルでは確かめない。アプリの command はどれも main の capability だけが許し、独立ウィンドウにも子ビューにも何も許さない(アプリの command の権限)。アプリの既存 capability の権限と全体 CSP は広げない。横の領域は見出しの下の注記で、独立ウィンドウは窓の中で、「HTML 内の JavaScript はこの表示(枠)の中だけで動き、外部との通信や外部のスクリプト・画像の読み込みはできない」ことを伝える。独立ウィンドウでの動作と枠からの invoke が拒否されることの実機確認は #443、横の領域で JS が動くこと・子ビューの枠からの invoke が拒否されること・本体の機能が今までどおり動くこと・パネルの開閉と幅の変化で位置がずれないことの実機確認は #466 の完了条件である。

部屋のフレーム・ログ・端末配送・保存形式は変えない。追加の実行時依存 parse5 とその entities は MIT であり、notice 生成の対象に含める。ブラウザ試験と native 試験の確認範囲は 開発・テスト に記す。

発言は Markdown として描く(#431)

部屋の発言の本文を Markdown として描き、表・コード・強調・図(Mermaid)を読める形で出す(Master 2026-10-16「pullcept表とか図とか扱えないじゃん」、2026-10-17 着手の指示)。変えたのは見え方だけであり、部屋のフレーム・ログ・各席の端末へ打ち込まれる文字の決まりは変えない。送る本文は今までどおり打ったとおりの Markdown の文字列である。入力欄は対象外である。

読むのは GFM である。 段落・見出し・箇条書き(番号付き、チェック付きを含む)・引用・区切り線・表(列の寄せを含む)・強調・太字・取り消し線・インラインのコード・コードの枠・リンクを描く。文字列を marked で字句に分け、要素はすべて画面の側で一つずつ作る(src/markdown.ts の appendMarkdown)。段落の中の改行一つは、今までどおりその場の改行として出る(.message .body の white-space: pre-wrap による。breaks は使わない)。

発言の中の生の HTML は描かない。 ブロックでも文中のタグでも、書かれた文字のまま出す。他の席や外部の通知が書いた文字で、画面にスクリプトや要素を入れられないようにするためである。marked の HTML 出力も innerHTML も使わない(Mermaid の図だけは下記のとおり)。画像の記法(![説明](URL))は説明の文字だけを出し、絵を読み込まない。

リンクは #349 の規則を通る。 地の文は linkifyText へ渡し、URL とパスはこれまでどおりリンクになる。隣り合う文字と Markdown のエスケープはつなげてから渡す——\\server\share の頭の \\ はエスケープとして読まず、書いたまま残す(UNC のパスが切れないようにするため、AI 判断)。[文字](URL) の形のリンクは、URL が #349 の規則でそのまま一つの URL(http:// / https://)と読めるときだけ押せるリンクになり、それ以外の宛先は文字だけを出す。押したときの動きは #349 と同じである。

コードの枠は #348 / #354 の枠のまま描く。 枠の言語名は開く行の最初の語である。インラインのコードは --code-ground の地に等幅で描く。

```mermaid の枠は図として描く。 枠はまずコードとして描き、Mermaid が描けたら図に置き換える。Mermaid の読み込みは最初の図のときだけ行う(別の塊として遅れて読み込む)。Mermaid は securityLevel: "strict" で動かし、ラベルは Mermaid が DOMPurify で消毒し、クリックの動作やスクリプトは残らない。この消毒済みの SVG だけを要素に入れる。図の色はそのときの画面の明暗(prefers-color-scheme)に合わせる。描けない図は元のコードのまま出す(Mermaid が読めない、または描くのに失敗したとき)。

表と図は吹き出しの中で横に送る。 幅の広い表と図は枠の中でスクロールし、自分の吹き出しは部屋の幅を超えない。

使うライブラリのライセンス。 発言の描画で増えたのは marked(MIT)と mermaid(MIT)と、mermaid が使うライブラリである。すべて MIT・ISC・BSD・Apache-2.0・Unlicense のどれかであり、GPL など縛りの強いものは入れていない。dompurify は MPL-2.0 と Apache-2.0 の選択であり、Apache-2.0 で受ける。mermaid は 11 系を使う——12 系は EPL-2.0 の elkjs を直に引き込むためである(AI 判断)。THIRD-PARTY-NOTICES.txt は scripts/third-party-notices.mjs で作り直した。スクリプトは、SPDX の (A OR B) で許すライセンスを一つでも含むもの、package.json にライセンスの記載が無くライセンスのファイルがそれを名乗るもの(khroma)、ライセンスを README の ## License 節にだけ持つもの(fastdom)を名前を挙げて受け、fastdom の strictdom は mermaid が読み込まない fastdom-strict.js だけが使うため数えない。

最新の発言へ戻るボタン(#243)

Master「コメント書き込み欄の上にある下までスクロールボタン追加できる?」(2026-10-02、Claude デスクトップで発言欄の上の中央に出る丸い ⌄ ボタンの画面写真を添えて)。

さかのぼっている間だけ出る。 会話面が一番下から 40px 以内に在る間は出さず、それより上を読んでいる間だけ出す。この判定は、新しい発言が届いたときに会話面が下へ追従するかどうかの判定と同じ一つの関数(roomAtBottom)であり、ボタンが出ている間は新しい発言で下へ追従せず、出ていない間は追従する。表示はスクロール・会話面の大きさの変化・発言の描画・通知の畳みの開閉・会話面の文字サイズの変更のたびに判定し直す。

押すと一番下へ移り、ボタンは消える。 移動はなめらかに流す(#307、下記「動き」)。#307 までは一度に移っていた。押しても入力欄の焦点は外れない(入力欄のボタンと同じ)。キーボードで押したときは、消えるボタンに焦点を残さず入力欄へ渡す。

会話面の下端の中央に浮かせる。 会話面とボタンを一つの枠(#room-frame)に入れ、ボタンはその枠の下端の中央に重ねる。行を一つ取らないため、出ても消えても会話面と入力欄の高さは変わらない。枠を会話面のものにしたのは、診断面(端末)が会話面と入力欄の間に開いているときも、ボタンが会話面の上に留まるようにするためである(AI 判断)。端末が閉じている間は、入力欄のすぐ上の中央に在る。

丸い小さなボタンで、線画は下向きの山形である。 Tabler Icons(outline、MIT ライセンス)の chevron-down を 16 の格子へ写したものであり、ライセンスの表記は NOTICE.txt に置く。語は aria-label と title(「最新の発言へ」)に置く(#71)。

地と枠を持つ(AI 判断)。 #221 のゴーストボタンの規則は、パネルの上に立つボタンのためのものである。このボタンは会話の文字の上に重なるため、地の無い線画では後ろの行の一部に読まれる。地は会話面と同じ --bg、枠は --line、影は --float-shadow であり、ライトとダークの両方で同じ形に読める。カーソルの下では、ほかのボタンと同じく --hover の影を自分の地に重ねる。大きさは UI 全体の軸に従い、会話面の文字サイズには従わない(#66)。

動き(#307)

席の動きと画面の移り変わりに動きを付け、今どうなっているかを見てわかるようにする(Master 要望、2026-10-06 / 10-07。参考は Claude デスクトップと ChatGPT のアプリ)。足したのは動きと、動いている席を言う一行だけである。札の語・色・出る条件、部屋・ログ・送る中身は変えない。

動きは CSS だけで書き、OS の「動きを減らす」に従う。 動きはすべて src/styles.css の @media (prefers-reduced-motion: no-preference) の中に在り、prefers-reduced-motion: reduce がオンの画面ではどれも働かない——札は明滅せず、会話面は一度に移り、行はすぐ出て、項目はすぐ切り替わる。#307 までの見え方そのものである。新しいライブラリは足していない。

「考え中…」「出力中」の札はやわらかく明滅する。 参加者パネルの二つの札(上記「参加者パネル」の「状態は色付きの札で出す」)だけが、1.6 秒の周期で薄くなっては戻る。発話がまだ終わっていないことを言う語だけに色を付ける線(#148 / #161)と同じ二語であり、終わりの側の語(待機・制限中・未起動・終了など)は動かない。パネルは語が変わるほかにも描き直されるため、札の明滅の位相はページの時計に合わせる——描き直された札は前の札の続きから明滅し、描き直すたびに明滅が頭から始まることは無い。

会話面の下に、いま「考え中…」「出力中」の席を一行で出す。 「Claude Lin が考え中…」の形であり、二つ以上なら「、」で続ける。名前はその参加者の色、語の前には「考え中…」「出力中」の札と同じ --ok の三つの点が左から右へ流れる。根拠は札と同じ一つの関数(rowActivity)であり、札が言わない席をこの行が言うことは無い——新しい手がかりも、観測していない状態の語も足していない(#82 の線、下記「走っているアカウントが何をしているか」)。語が消えればこの行も空になる。それがこの節の目的である「発話の終わりが見えること」を、パネルを開いていないときにも会話面の側で言う。

  • 行は空のときも高さを持つ(AI 判断)。 出力のバーストのたびに会話面の高さが伸び縮みすれば、会話面の最後の行が押し隠され、読んでいる位置がちらつく。会話面と入力欄の間(診断面が開いていれば会話面と診断面の間)に、常に一行ぶんの場所を取る。
  • 一行に収める。 収まらない分は末尾で省略し、二行にしない。大きさは UI 全体の軸に従い、会話面の文字サイズには従わない(#66)。表示であって会話の文ではないためである。
  • 言うことが変わったときだけ描き直す。 点の流れが、パネルの描き直しのたびに止まらないためである。新しく言うことは、ふわっと現れる。
  • 読み上げには流さない。 出力のバーストのたびに語が出入りするため、aria-live にすれば読み上げが絶えず割り込む。札と同じく、見に行けば読める。

新しい発言が来たとき、会話面を下までなめらかに流す。ただし、来る前から一番下にいたときだけである(Master、2026-10-07)。上を読んでいる間は動かさない。追従するかどうかの判定は #243 の一つの関数(roomAtBottom)のままである。流れている途中は一番下にいるものとして数える——流れの途中にもう一つ発言が来ても続けて追い、途中で「最新の発言へ」が一瞬出ることも無い。流れている最中に人がホイールやドラッグで動かせば、流れはそこで止まり、以後はふつうに判定する。トピックを開いたときに一番下へ移るのは一度に行う——開いたのは場所であって、届いた発言ではない。

「最新の発言へ」で戻るときも、なめらかに流す。 上記「最新の発言へ戻るボタン」。

新しい発言はふわっと現れる。 届いた発言の行は、透明から 0.26 秒で現れる。トピックを開いて読み戻した行は現れ方を持たない——既に言われたものであり、届いたのではない。動かすのは透明さだけで、位置は動かさない。行を動かすと、その間だけ会話面のスクロールできる高さが変わり、下までの流れが行き過ぎたり届かなかったりするためである。

アカウントの窓の横のメニューを切り替えたとき、移りをなめらかにする。 選んだ項目の印(--selected の塗り)がやわらかく移り、開いた項目の欄はふわっと現れる。窓の項目分けは「アカウントの窓は項目を横に並べる」(#304)。

トピックの一覧

会話面の左に常時出る列を持つ。トピックの一覧であり、選べばそのトピックが部屋へ戻る(「トピック」)。

この列は過去の発言を一本の流れで並べる帯だった(#48)。求められていた形は一覧であり、埋めた粒度が外れていた——#48 で Master が判断したのは「左サイドバーに履歴として表示」「起動時は新規ルーム」の二点であって、一本の流れか、トピックの一覧か、ではなかった(#115)。

選んだトピックは部屋の本文へ戻る。 #48 の「過去ログを #room の本文へ復元しない」はここで巻き戻る。その一行は上書きされたものとして扱う。起動時の部屋が新規であることは残る——変わったのは、過去が読める場所ではなく、過去へ戻れるかどうかである。

新しい順に並べる。 会話の一覧が読まれるのはこちらの端からである。ファイルは起きた順(追記の順)を保ち、逆順は読みの判断であって保存の判断ではない。

索引に無い現在のトピックは行にしない(#125、決定1)。 遅延生成のため、毎回の起動直後には部屋に在って索引に無いトピックがちょうど一つ在る(「トピック」)。以前はそれを一覧の先頭へ架空の行として足していた。その行は「名前の無いトピック」を名乗るが、実際には誰も始めていない会話であり、しかも索引に無い以上そこから消すものも持たない。代わりに「新規」が本当のこと——新しいトピックが始まる場所——を言う。部屋がそのトピックに居る間、「新規」は選択状態で描く(AI 判断4)。 そうしないと、自分の居る場所が一覧のどこにも無い状態になり、架空の行が避けていた問題が形を変えて戻る。

一件が持つのは名前と作成日である。 日付は短く言う——今日なら「今日」、それ以外は「9月28日」、今年でなければ年を付ける(#225)。時刻はツールチップに出る。#225 までは年月日と時刻を出していた。名前は一行で切り、溢れは省略する——一覧は左端を縦に読むものであり、三行に折り返した題は次の行の位置を前の行の長さで決めることになる。題の無い行は「未記入」と出す(#125、AI 判断3)。 空白にすると、読み出しに失敗した行と見分けが付かない。確認ダイアログと削除後の状況行も同じ語を差し込む——差し込まなければ「トピック「」を削除します。」と出る。名前を直すときの欄は空のままである。「未記入」は空の題について画面が言う語であって、題ではない。

名前はその場で直す。 行をダブルクリックすると入力欄になり、Enter が決定、Escape とフォーカス喪失が取消である。生成された題は編集できる欄に置かれた出発点であり、アカウントの名前と同じものである。

行から消せる(#119)。 行の右端に ✕ を置く。✕ はカーソルがその行に在るとき、またはキーボードの焦点がその行に在るときだけ見える(#225)。列は見えない間も幅を持ち、出たときに題が動かない。透明にするだけで取り除かないため、Tab で辿れば焦点が着いたところで見える。押すと確認のダイアログが開き、そこで答える——参加者パネルの行のメニューの 終了 と同じ危険色であり、同じ種類の操作だからである:その行で唯一取り消せない操作である。線画は ✕ のまま残す——セッションの終了は再生の停止の四角(#239)で描き、トピックの削除は消す操作の印で描く。こちらはそれが、面を切り替えるだけの操作の隣に在る。行そのものは flex のトラックになり、.pick が残りの幅を取る。長い題は ✕ を押し出さずに省略される。

題の無い行にも出す(#125、決定2)。 一覧に出る行はすべて索引の行になったため、どの行も消せる。#119 決定5 とその前提が外れた経緯は「トピック」に書いた。

ダイアログは #end-dialog / #quit-dialog と同じ作法である。 webview 内の <dialog>、取消に焦点、Escape は取消。要素は別に持つ——一つの要素が三つの問いを持てば、開くたびにどの顔で開くかを決めることになり、答えは訊いた側へ帰る道を探すことになる。CSS は同じ規則で描く。形は一つであり、二つ目の写しは一つ目からずれる。

ダイアログは二行を持つ。 一行目はトピック名と、何が消えるか。二行目は止まるセッションの名前であり、走っていなければ出さない(「トピック」、決定4から導かれること)。二行目は一行目の限定ではなく、失われる二つ目のものであるため、別の文にする。

「新規」は見出しの横の + である(#225)。 参加者パネルの見出しの + と同じ形・同じ規則で描く。名前「新しいトピックを始める」はツールチップで出る。作る操作はそれが現れるリストの隣に置く(#59)という判断は残る。#125(決定1)から #225 までは一覧の先頭に行と同じ形で固定して置いていた。索引から描き直す一覧の外に在ることは変わらない:索引の行ではないため、索引が空でも在る。索引に無いトピックに居る間は + を --selected で塗り(AI 判断4 を引き継ぐ)、そのトピックへの起動が飛んでいる間は + の角にアクセント色の点を置く(#141)。その間のトピックの名前は上の帯が「新しいトピック」と言う(上記「会話面の見た目」)。

参加者パネルと対にする。 幅(16.5rem)と地色を揃え、枠は会話面に向く側に置く(左は border-right、右は border-left)。部屋を挟む二つの面が別々の寸法を持てば、揃っていないこと自体が読まれる。中の部品は同じ語彙(.panel-head / .panel-title / .empty)を使う。選択中の行は、参加者パネルの選択中の行と同じく --selected で塗る(#75、#221。上記「二つの地色」)。

読み戻した行は部屋の行と同じ形で描く。 別の面ではなくなったため、寸法も同じである。トピックは日をまたぐため 14:32 だけではどの日か決まらないが、日は行の時刻ではなく区切り線が言う(#225、上記「会話面の見た目」)。色は名前から導き、own はどの行にも立てない。 ログは宣言色も見る側の位置も持たないためである(「部屋のログ」)。ただし画面の人の行は例外であり、ログではなくアカウントから色を得る(上記「画面の人の色は描いた行にもその場で届く」、#189)。それ以外に過去であることの印は付けない——同じ会話であり、「起動のどちら側で言われたか」は発言の属性ではない。

読み戻しでは、最後に描いた行を透かしにする(#141)。 部屋の床は切り替えで空にならず、床に載る発言はすべて受理と同じ取得の中でログへ書かれているため、ログの最後の行は、床に発言が在る限り床の最新である。床が空のとき、または前回の実行の行であるときは、部屋はその値を「何も見ていない」と読み、費用は拒否の往復一回であって、二重には描かない(下記)。以前は床が切り替えで空になっていたため透かしを消していた。描いた message_id は残す——拒否が保持窓の全件を返したときに、既に描いた行を二重に描かないためである(#108)。

画面の再読み込みの後も、開いているトピックの発言を描く(#141)。 そのトピックで走っているセッションは席から端末ごと繋ぎ直されるため(「画面の再読み込みに耐える」)、本文だけが空であれば、話し手が覚えている会話を画面だけが忘れた形になる。

セッションが走っているトピックには印を付ける(#141、AI 判断5)。 走らせたまま離れられる以上、どこで走っているかが一覧から見えないと止め忘れに気づけない。一覧はすべてのトピックが同時に画面に出る唯一の場所である。印は行の隅のアクセント色の ● で、名前の幅は削らない。起動途中の席も数える——すぐに走るセッションであり、押した直後に離れたトピックに印が出ないのは、押したばかりのものを言わない形になる。一覧に無い現在のトピックでは「新規」に同じ印を付ける(起動がトピックを実体化するまでの間だけ起きる)。

空とエラーを分ける。 トピックが無いのは初回の実行であって失敗ではないため「トピックなし」と出す。読み出しが失敗したときだけ理由を添えて出す。

参加者パネル

会話面の右に常時出る列を持つ。参加者のリストと、その下にセッションの裏の値(部屋ソケット / セッションの生死と終了コード / セッション ID / 接続方法 / 起動コマンド / 作業ディレクトリ / 開始時刻 / ウィンドウ / モデル / エフォート / 5 時間 / 週次 / コンテキスト)を並べる。

末尾の五つは軸が違う。 手前の欄が答えるのは「何として起動したか」であり、この五つが答えるのは「いま何を使っているか」である(#155、「セッションは自分の様子をステータスラインで知らせる」)。出所も違う——手前は起動がアプリの側で知っている値であり、五つはそのセッション自身が寄越す値である。並びの末尾に固めるのはその区別であって、重要度ではない。

見出しは「アカウント」である。 並ぶものはアカウントであり、+ が作るのも、行のメニューの「編集」が開くモーダルの見出しも同じ語である(「アカウント」)。見出しだけが別の語であれば、一つのものが同じ面の中で二つの名前で呼ばれる(#78)。読み上げの名前(このパネルの aria-label)も同じ語を持つ——見える語と読み上げる語が分かれれば、名前はやはり二つになる。

部屋の参加者を指す語は変えない。 変えたのは見出しとその読み上げだけである。リストが空のときの「参加者なし」は残る——この行が出るのは名簿とアカウント一覧の両方が空のときであり、言っているのは行の不在である。アカウントを持たない接続(「ゲスト」)もこのリストに並ぶ以上、行の不在をアカウントの不在とは言えない。この節の呼び名「参加者パネル」も同じ理由で残る。

読む値と、見る値を分ける。 ここに出るのは、詰まったときに読みに行く値である。CLI の端末だけは別で、あれは読む値ではなく映しているものであるから診断面に残る。

起動の値は「詳細」の下に畳む(#225、2026-10-02 の試作に沿う)。 開いたまま出すのは、セッションの生死と、末尾の五つ(いま何を使っているか)である。部屋ソケット・セッション ID・接続方法・起動コマンド・作業ディレクトリ・開始時刻・ウィンドウは「詳細」の折りたたみに入り、既定では閉じている。開閉は覚えない。それまでは「折りたたみの下に置くと、開く操作を思いつくかどうかが読めるかどうかになる」として全部を開いて出していた。その代償は受け入れるが、一つだけ残す:部屋ソケットが待ち受けていないとき、「詳細」の見出しが危険色になる。畳んだ欄の中の失敗が畳んだことで見えなくならないためである。

使用率は数と棒で出す(#225)。 5 時間・週次・コンテキストの三つは、数の下に棒を置く(並びは下の #320)。棒は 100% で満ち、それ以上は伸びない——数は伸びる(spend limit は 100% を超えうる、#161)。100% 以上で棒は危険色になる。「制限中」が立つのと同じ線である。届いていない値は「—」だけで、棒を出さない:空の棒は 0% と読まれる。数は小数を切り捨てて整数で出す(#370)。100% と出たら上限に達している、と読めるようにするためである——四捨五入では 99.5% が「100%」と出ており、2026-10-11 にパネルが 100% を示す間、ChatGPT アプリは「残り 1%」を示して制限はまだかかっていなかった。棒と危険色は丸める前の値で決める。

5 時間と週次の行は、Claude デスクトップと同じ並びにする(#306、#320)。 行はパネルの全幅(名前と値の二つの桁)を使い、1 行目の左に名前、右にリセットの文言と % を並べ、2 行目にバーを全幅で出す。名前の桁の幅も文言に回る。文言は 5 時間が残り時間の「4時間10分後にリセット」、週次が曜日つきの現地時刻の「2:00 (月) にリセット」であり、どちらも Claude デスクトップと同じ形である。時間の経過に合わせて書き直し、リセット時刻を過ぎたら消す。パネルの幅が足りないときは文言のほうが「…」で縮み(全文は title に残す)、% は縮まない。コンテキストの行も同じ並び(1 行目に名前と %、2 行目にバー)にし、三つの行を揃える。出所と数え方は「セッションは自分の様子をステータスラインで知らせる」にある。

宛先の候補の出所は部屋の名簿(room-participants)だけである。 画面側に第二の一覧を持たない。持てば、宛先に選べる名前と画面に並ぶ名前が食い違う余地ができる。候補は入力欄の @(または操作列の宛先のボタン、#222)で出る(「宛先は本文の中の @ で指定する」)。

リストは一つである

部屋の名簿と、このアプリのアカウント一覧を、アカウント id で突き合わせて一つのリストにする。行は「誰が居るか」と「その相手に何ができるか」の両方に答える。

以前はここが二つに割れていた——名簿(鍵は接続)と端末の列(鍵はアカウント id)である。割れていた理由は、Participant がアカウント id を持たず、二つを結ぶ手段が名前しか無かったことである。名前で結ぶ形は #40 と #53 が却下しているため、#57 は結ばずに二つ並べる形を採った。部屋が id を運ぶようになったので(「部屋プロトコル」)、結べる。二つの見た目を揃えるのではなく、一つにする(#59)。

突き合わせは常に id であり、名前ではない。名前は編集できる属性であり、二つのアカウントが同じ名前を名乗ることもあれば、稼働中に改名されることもある。どちらの場合も名前での突き合わせは別の相手に当たる。

リストは種別で分かれ、見出しが種別名と件数を示す。 admin — 1 / AI — 2 の形である。アカウントを持たない接続は種別の宣言が無いため「ゲスト」に入る。これは種別ではなく、宣言の不在である。

mcp アカウントは AI アカウントの下に、種別の見出し MCP として並ぶ(#193、Master 判断 2026-09-28)。 並びは admin、Claude Code、Codex CLI、CLI、MCP、ゲストである。サーバは席を取らないため(「webhook の通知」)、この行は部屋の名簿ではなくサーバの実行状態を読む:実行中なら色を落とさずに何も言わず、在室しているセッションの行と同じ見え方になる。それ以外は note が状態を言う——「起動中」「終了」「起動失敗」「未起動」はセッションの行と同じ語を同じ意味で使い、手で止めたものは「停止」、ファイルにエントリの無いアカウントは「未登録」である。「終了」と「起動失敗」の title は、その実行が終わった理由である。全文のログは窓にある。

種別は作成時の宣言であり、接続の種類から推測しない。 部屋は接続の種類しか知らず、別のクライアントから参加した人間はセッションと同じ種類の接続で来る。推測すれば取り違える。宣言できる唯一の時点はアカウントを作る瞬間であり、そこには既にフォームがある(「アカウント」)。

起動していないアカウントも最初から並ぶ。 一度起動することを条件にしない。

行の見え方。 名前はその参加者の色で表示する。オフラインの行は色を保ったまま、呼称・元の名前・丸印を opacity 0.35 で薄くする(#393)。開始とメニューの操作は薄くしない。灰色に落として色を捨てない——色はアカウントの同一性を担っており、居ないだけで別人に見えるのは誤りである。行が示す補足は、この画面の本人なら「(あなた)」、プロセスが在って部屋の名簿にまだ現れていないなら「起動中」、席の無いアカウントなら「未起動」、セッションが終了して出力だけ残っているなら「終了」である。走っているセッションについては、出力が届いている間は「考え中…」または「出力中」、ターンが利用制限で終わっていれば「制限中」、端末が 1 秒以上黙っていれば「待機」が同じ場所に出る(下記「走っているアカウントが何をしているか」)。名前の前には会話面と同じ丸印を置く(#253)——アカウントの画像があれば画像、無ければ頭文字を参加者の色で描く。#225 から #253 までは参加者の色の小さな点だった。点が担っていた「会話の色の凡例」の役目は、丸印の色が引き継ぐ。丸印は会話面と同じ関数で描き、二通りの描き方を持たない。引くアカウントは、部屋に居る行なら席が宣言したアカウント(会話面の行が持つものと同じ)、居ない行ならリストのアカウントである。アカウントを持たないゲストの行は頭文字のままであり、mcp の行は通知の畳みの見出しと同じ丸印になる。画像は会話面が読み込んだものを使い回し、行ごとに読み込み直さない。直径は会話面の文字サイズ(--room-font-size)の 2.31 倍、頭文字は 1.2375 倍である——会話面の丸印(直径は文字サイズの 2.1 × 1.265 倍)の 1/1.15 にあたる。#258 で、会話面の文字サイズが既定のときの、それまでの会話面の丸印(1rem の会話面で頭文字 1.1em、直径はその 2.1em)と同じ直径にし、頭文字も同じ比で上げた(#253 では直径およそ 1.4rem、頭文字 0.75rem)。行の高さは丸印に合わせて伸び、▶/■ と ⋯ の列の並びと幅は変わらない。

丸印の大きさだけは会話面の文字サイズの軸に在る(#260)。 #253 では丸印も rem で持ち、パネルの他の部分と同じく UI 全体の倍率(#66)に連れ、会話面の文字サイズには連れなかった。#258 の値(2.31rem)は「UI の倍率が 1・会話面の文字サイズが既定」のときに会話面の丸印の 1/1.15 になる値であり、会話面の文字は UI の倍率を割り戻している(下記「会話面の文字サイズ」)ため、どちらかの設定が既定でないと比が崩れ、二つがほぼ同じ大きさに見えた。Master「アカウントのアイコンサイズがチャット欄のアイコンサイズと同じになってない?」(2026-10-03)。#258 の意図は会話面の丸印との大きさの関係(Master「今の会話のアイコンサイズをアカウントのアイコンサイズに。で会話のアイコンもう少しだけ大きくできる?」、2026-10-03)であり、それを保つために丸印の大きさを会話面の文字サイズの軸へ移した。パネルの文字・行・操作は UI 全体の軸のままである。

状態は色付きの札で出す(#225)。 名前の隣の語のうち状態を言うものは、角の丸い札に入れ、語の色を薄く混ぜた地を敷く。札の色は語の種類による:「考え中…」「出力中」は --ok(緑系。:root とダークの両方に定義)、「起動失敗」は --danger、それ以外(未起動・起動中・終了・待機・制限中、MCP サーバの停止・未登録)は --muted である。発話がまだ終わっていないことを言う語だけに色を付ける、という #148 / #161 の線は動かさない。#225 までの「考え中…」「出力中」は本文色であり、--accent を避けた理由(このパネルでは accent が自分の色である)は --ok でも同じく守られる。「(あなた)」は状態ではなく誰の行かを言う語であり、札にしない。札は縮まず、桁が足りなければ名前の側が省略される。ライトでは「考え中…」「出力中」と --muted の札の地が語の色を混ぜた地ではなく試作の決まった色になった(#235、上記「ライトの色」)。「起動失敗」の札とダークの札は語の色を混ぜた地のままである。「考え中…」「出力中」の札はやわらかく明滅し、同じ二語は会話面の下の一行にも出る(#307、上記「動き」)。

行にアカウントの操作が乗る。 端末を持つアカウントの行は、クリックでその端末を選ぶ——選ばれている端末、行のハイライト、パネル下部のセッションの値が切り替わる。診断面が開いていれば、見えている端末がその端末へ切り替わる。畳まれていれば面は畳まれたままで、端末へフォーカスも移らない。後で 端末 ボタンで面を開くと、選んだ端末が出ている。選ぶことと面を開くことは別の操作であり、面の開閉は 端末 ボタンと面の ✕ が担う(#175)。▶ による起動も面の開閉を変えない(#215、下記「診断面」)。行の鍵がアカウント id であることが、行に操作を載せられる条件である——名前で突き合わせると、二つのアカウントが同じ名前を名乗った時点で別の行に当たる。

開始と終了は行にもメニューにもある。それ以外の操作は行のメニューにある(#224、#239)。 行を右クリックすると、そのアカウントのメニューが開く。項目は「端末を開く」「編集」「色を変える」と、区切り線の下の「開始」または「終了」である(#241)。#224 までは行に終了 ✕ と編集 ⚙ が常に並んでおり、開始と終了は一つのライフサイクルの両端として同じ列の同じ場所に在った(開始は会話面の上のランチャ行から #62 で移した。「ランチャ行は無い」)。#224 は終了と編集をメニューへ移し、開始だけを行に残した。その形では、止まっている行には ▶ があるのに、動いている行には終える手が行に無く、メニューの中にしか無かった。Master「アカウントにセッション開始の三角ボタンがあるのに終了ボタンがメニュー内になったのがちょっとなぁ。終了ボタンとして四角を入れるかどちらもメニュー内にするかのどちらかにしたいね」「じゃあボタンとメニューどちらにも入れとこうか」(2026-10-02)。#239 で終了を行の開始の場所へ戻し、開始をメニューにも置いた。開始と終了は、行のボタンからもメニューからも行える。

  • 右クリックの代わりの入口を行に一つ残す。 二列目の ⋯(「◯◯ の操作」)が同じメニューを開く。右クリックだけにすると、副ボタンの無いポインタでは開けず、キーボードでは名前がボタンでない行(この画面に端末の無いアカウント)から編集へ辿る手が無くなる。✕ と ⚙ の二つが ⋯ の一つになる。焦点のあるボタンでのコンテキストメニューキーと Shift + F10 も同じメニューを開く。
  • 使えない項目は出さない。 灰色にして残さない。「端末を開く」はこのトピックに端末を持つ行にだけ、「開始」は起動する種別のアカウントでセッションが走っていない行にだけ(admin と mcp には出ない、#59・#193)、「終了」は終了できるセッションを持つ行にだけ出る(起動が id を返す前は出さない、#57)。端末を持たないアカウントに端末の操作を出さないという条件(下記)と同じである。アカウントを持たない行(ゲスト)にはメニューが無い。
    • 開始と終了も、行えない方を出さない(#239、AI 判断)。 二つが同時に出ることは無い。メニューの他の項目と同じ規則に揃えるためである——押せない形で残すのは、この規則の例外を一つ作ることになる。もう一つの理由は、出ている方がそのまま行の一列目と対応することである。メニューに出ている方が、行の一列目で押せるボタンと同じものである。起動が返るまでの間は、どちらも出ない——開始は押された後であり、終了はまだ id が無い。行には押された ▶ が押せない状態で残っており、その間の状態は行が言う。
  • 取り返しのつかない操作は区切って危険色で示す。 「終了」は区切り線の下の最後に置き、語も線画も --danger で描く。線画は行の一列目の終了と同じ四角である(#239)。選べば従来どおり確認のダイアログが開く(下記「診断面」)。メニューが変えたのは問いを開く場所であって、問うかどうかではない。終了の武装状態の扱いは #72 で別に検討している。
    • 開始も、終了と同じ区切り線の下の最後に置く(#241)。 メニューを開くと、行が止まっていれば一番下は「開始」、動いていれば「終了」である。行の一列目で開始と終了が互いの場所に替わって出るのと同じく、一つのセッションの両端をメニューでも同じ場所に置く。Master「いまさ、メニューだと開始が上側で終了が下側じゃん?これ開始も下側にしつつ線も入れた終了と同じ形式にしてほしい。」(2026-10-02)。#239 では開始をメニューの先頭に置いていた。揃えるのは区切り線と位置だけで、開始の語と線画は危険色にしない(AI 判断)——取り消せない操作は終了だけであり、危険色はそれを示すための色である。開始は、替わって出る終了が取り消す(下記「開始は確認を挟まない」)。起動が返るまでの間は開始も終了も出ないので、区切り線も出さない。線の下が空になるためである。
  • 「端末を開く」は面を開く。 行のクリックは端末を選ぶだけで、畳まれた面を開かない(上記、#175)。この項目は「開く」と言う操作であるため、その端末を選び、面を開き、端末へ焦点を移す。
  • 「色を変える」はアカウントのモーダルを「色」の欄で開く。 色はアカウントの属性であり、書き換える場所はモーダルだけである。メニューに色の選択を別に置かず、モーダルを開いて焦点を「色」の欄に置く。項目の頭には、線画の代わりにそのアカウントの今の色の点を置く。
  • 見た目は #221 のボタンに揃える。 項目は枠も地も持たない線画と語であり、カーソルやキーボードの焦点の下でだけ --hover の塗りが出る。開いている間、行の ⋯ は --selected の塗りを持つ。項目の箱は宛先の候補と同じく、窓の地・線・影で描く。
  • 閉じ方はメニューの作法に従う。 キーボードでは上下の矢印で移り、Home / End で端へ飛び、Escape と Tab で閉じて ⋯ へ焦点を戻す。メニューの外を押す、⋯ をもう一度押す、パネルがスクロールする、窓の大きさが変わる、窓が焦点を失う、のいずれでも閉じる。部屋と端末のスクロールでは閉じない——文字が届くたびに閉じれば、セッションが出力している間は使えない。項目は選ばれた時点の状態で動く。開いている間にセッションが終わっていれば、「終了」は何もしない。メニューの開閉では行を描き直さず、⋯ の開いている印(aria-expanded)だけをその場で書き換える(#229)。描き直すと、止まったままのポインタの下で ⋯ が別の要素に替わり、実機では Escape で閉じた直後の一回目の押下が効かず、二回目で開いた。

行の操作は名前の後ろの二列である。一列目が開始または終了、二列目がメニュー。 列は行の状態で動かない。

  • 一列目 = 開始 ▶ または終了 ■。列は一つであり、行の状態で中身が替わる。
    • 開始 ▶ は、起動する種別のアカウントの行で、セッションが走っていないときに出る(Claude Code、Codex CLI と CLI(汎用)。#156 / #272。admin と mcp には出ない、#193)。押してから起動が返るまでは、押せない状態で残る(#62)。
    • 終了 ■ は、終了できるセッションを持つ行に、開始と同じ場所に出る(#239)。起動が id を返した後であり、その前は押された ▶ が残っている(#57)。押すとメニューの「終了」と同じ確認のダイアログが開き、押しただけでは止めない(下記「診断面」)。線画は開始 ▶ と同じ色で描き、危険色にしない(#241)。Master「違う違うメニューのことじゃなくて□を黒にしてほしかったんだよ。メニューは赤でもいい。」(2026-10-02)。#239 では、その行で唯一取り消せない操作として、メニューの終了とトピックの削除と同じ --danger で描いていた。取り消せないことを守るのは、押すと開く確認のダイアログである。メニューの終了とトピックの削除は危険色のまま残す。
  • 二列目 = メニュー ⋯。アカウントを持つ行には常に出る。
  • 列は中身の有無に依らず幅を持つ。 一列目に何も持たない行——種別 admin と mcp——も列を閉じない。閉じればメニューがその行だけ左へ寄る。

行の操作は線画で表示する。 どれも語ではなく線画(開始の三角・終了の四角・メニューの三点)であり、語は aria-label と title に入る。メニューの項目は線画と語を並べる。線画は読み上げに耐えず、名前ではない——何をする操作かを言うのは label の側である。#221 までは絵文字だった。絵文字はカラーフォントが描くため color を受けず、淡色・危険色・カーソルの色をボタンと一緒に取れない(上記「二つの地色」)。

操作の列は幅を固定する。 ボタンの寸法を固定し、線画はその中で中央に置く。絵文字だった間(#71 〜 #221)は、送り幅がホストの選ぶフォントで決まり、字面から幅を取れば同じ画面が機械ごとに違う形になることが固定の理由であった。名前の列は残りを全て取る——パネルは 16.5rem しかなく、実測で本人の行(「マスター」+「(あなた)」)が出なくなるのが、ライフサイクルの列を広げない理由であった(#71 以前は終了が武装時に広がるため、この列は下限だけを持っていた)。

端末を持たないアカウントに端末の操作は出ない。未起動のアカウントに出せる端末の操作は無い(#57 の条件を維持)。開始はこれに含まれない——端末の操作ではなく、端末を作る操作である。

種別 admin の行にライフサイクルは無い。 人間は起動されるものではなく、start_session が既に拒否している(「アカウント」)。列は空のまま残り、その行のメニューは他の行と同じ位置に並ぶ。

開始は確認を挟まない。 終了が確認を取るのは取り消せないためである(下記「診断面」)。開始は取り消せる——間違えて始めたセッションは、同じ場所に替わって出る終了 ■ が終わらせる。取り消せる操作に確認を付けると、undo が効く失敗を防ぐために全ての意図的な操作が二段階になる。

起動中と起動失敗は行が読める。 押した直後から起動が返るまで、その行の開始は押せなくなる。状態の語は名前の隣の note に入る——「未起動」「終了」と同じ場所であり、note は状態を、ボタンは行為を言う。

note 行の状態
起動中 プロセスが在り、終了していない。開始を押して起動が返っていない窓と、起動は返ったが部屋の名簿にまだ現れていない窓の両方を含む
起動失敗 直前の起動が失敗した。理由は title、全文は status 行

「起動中」の判定はプロセスの生死であり、部屋の名簿ではない(#89)。 一つの画面に「走っている」の定義が二つ在ったことが欠陥であった——状態パネルはプロセス(view.ended === null)で、行は部屋の名簿(row.participant)で判定していた。CLI がプロセスとして起動済みで部屋の websocket にまだ繋いでいない間、二つは食い違い、行は「未起動」まで落ちた。走っているセッションと本当に起動していないアカウントが同じ語を出し、区別はボタンにしか残っていない状態である。行がパネルの定義に合わせる形を採り、語は新設しなかった——パネルが既に「起動中 ↔ 終了」の対でこの語をこの意味で使っており、二つ目の語彙を持ち込む理由が無い。

この枝は在室の枝より後ろに置く。在室中の行が何も言わない形(上記「行の見え方」、#82)を壊さないためであり、部屋に居ない行だけがこの枝に落ちる。起動直後の窓と、一度入った CLI が接続を失った場合の両方がこれで読める。終了の出現条件は変えていない(#224 からはメニューの「終了」)——プロセスは実在し、終了できるべきである(#84 が直したのと同じ穴を開けない)。

起動に失敗した端末は破棄される(下記「診断面」)。行に残さなければ、押したこと自体が画面から消え、行は押される前と同じ「未起動」へ戻る。この記録は次にそのアカウントの開始が押された時点で消える——失敗が消えるのは、同じ問いがもう一度立てられた時である。

宛先の候補は増えていない。 オフラインの行は部屋の名簿に居ないため、宛先には出ない。届かない相手を名指せる状態は、宛先を名簿から引くという判断(#43)の理由に反する。

参加者ごとの色。 発言の丸印の色(#225 までは縁の色線)と名簿の丸印の色(#253 までは点)は同じであり、参加者ごとに異なる。宣言色と名前からの導出色は、テーマごとの共通の明度・彩度(--speaker-l / --speaker-c)で色相だけを振る。#395 で調整した明度を保ち、#397 で彩度を元の設定(ライト 0.131 / ダーク 0.087)より高くして鮮やかな色へ調整した。アカウントの一覧・チャット・宛先など、同じ話し手色を使う表示へ共通して反映する。宣言の無い本人は引き続き --accent で描く。

明度 彩度
ライト 0.54 0.16
ダーク 0.76 0.11

色相の出所は 3 段の梯子であり、上から順に決まる。

  1. 宣言 — 参加者が参加の時点で選んだ色相。
  2. 自分の accent — 宣言が無く、かつ自分自身のとき。
  3. 名前からの導出 — それ以外。到着順ではない。順で決めると、再接続した参加者が別の色で戻り、色が「誰か」ではなく「いつ入ったか」を表すことになる。

導出だけでは足りないことは実測で判明した。Lin 130.5 度 / Lay 165.5 度 / マスター 205.5 度と、実際に使う 3 名が 75 度の帯に収まる。accent と同じ低彩度では 35 度差を見分けられず、色で追うという目的が、まさに使う名前の組で弱まる。名前由来かつ安定という性質と、任意の 2 名の分離保証は両立しない。分離を得るには色を導出ではなく宣言にする必要がある(#40)。

宣言できるのは色相だけである。明度と彩度は上表のまま動かさない。選んだ色相によらずライト / ダークの両方で読める、という条件を保つためである。宣言は色相順の 16 色から選ぶ(#393)。既存の赤 25・橙 55・黄 95・緑 145・青緑 195・青 250・紫 300・桃 350 を保ち、その間に紅 7.5・朱 40・山吹 75・黄緑 120・ミント 170・水色 222.5・藍 275・藤 325 を加える。「自動」と保存済みの色相は引き続き使える。連続した色相環にしない。買う必要があるのは「任意の 2 名を見分けられること」であって、度数を人間に判断させることではない。

宣言が accent より上にあるのは、宣言した本人にだけ出ない色は宣言した色ではないからである。以前の色分けは自分 / 他人の二値であり(#39)、自分以外の全員が同じ色になっていた。accent は自分の既定として残るため、色を宣言しない参加者の見え方はこれまでと変わらない。宣言した場合に自分を自分と判じる手段は、名簿の「(あなた)」と、行に出ている自分の名前である。色はその 3 つのうち最も速い経路であって、唯一の経路ではなかった。宣言によって、色は割り当てられるものから選ぶものへ移る。

名前から導出する色相は accent の周囲 50 度を空ける。宣言した色相は空けない。空けるのは名前がたまたま自分の色に当たることを避けるためであり、選んだ結果として当たるのは選択である。

端末そのものはこのパネルへ畳まない。設計キャンバスは右下へ同居させているが、パネルの幅では CLI が枠を描けない(下記「診断面」)。パネルに乗るのは端末を選ぶ操作と終了であって、端末の中身ではない。

パネル下部のセッションの値(セッション ID・接続方法・起動コマンド・作業ディレクトリ・開始時刻・ウィンドウ・生死)は、選択されている端末のものである。最後に起動したセッションのものではない。部屋ソケットだけは部屋自身の値であり、選択に依らない。

セッション ID は索引の記録を出す(#139)。 端末で閉じたセッションはアプリが観測しないため、次回の起動が再開の線に乗らず、手で claude --resume <id> する必要が生じる。その id を画面から得る手段として置く。出す値は、選択中の端末が起動したトピックについて、索引がそのアカウントに記録している id である(決定1)。起動の応答ではなく記録を読むため、アプリを開き直してもトピックの記録が同じなら同じ値が出る。記録が取り消されれば(「トピック」の再開先の取り消し、#127 / #131)表示も消える。トピックはその端末の起動が入ったトピックである。画面に出る端末はすべて画面に出ているトピックのものであるため(#141)、この二つは一致するが、一致するのは構造によってであって、画面の開いているトピックを読むことによってではない。記録の書き込みと取り消しの仕組みは変えない。画面は読むだけである。

値の無い行は「なし」と出す(決定2)。引数に id を宣言しないアカウント、または記録が無い場合がこれに当たる。起動の応答が返る前はトピックがまだ決まらないため、他の値と同じく「—」である。「なし」は手で再開する先が無いという答えであり、まだ分からないこととは別である。値が在るときは隣にコピーのボタンを出す(決定3。#221 から語ではなく線画で、名前はツールチップ)。手で再開するために要るのは読めることではなく写せることである。コピーするのは画面に出ている値そのものである。

端末が無いときも記録を出す(#144)。 アプリを開き直した直後は端末ペインが一つも無く、上の決まりのままでは索引に id が記録されていても「—」になり、手で再開する id を画面から得られない。そこで、画面に端末が無いときは、アカウント一覧で選ばれている行のアカウント(選択が無ければ一覧の並びで最初の AI アカウント)について、開いているトピックに索引が記録している id を出す(決定1)。「セッション」は「未起動」、id は記録値であり、記録が無ければ「なし」である。コピーは値が在るときに出る既存の振る舞いのままである(決定3)。起動して端末が画面に出れば、値はこれまでどおりその端末のものに切り替わる(決定2)。現状、端末を持たない行は選択の操作を持たないため、再起動直後に出るのは最初の AI アカウントの記録である。

走っているアカウントが何をしているか

四つの語(未起動 / 起動中 / 終了 / 起動失敗)の出所は席の生死と起動の成否であり、プロセスが在るか無いかしか見ていない。走っている間の様子はどこにも出ていなかった。この画面が観測できる合図を二つ足す(#82)。

この節が買っているのは、AI の発話の終わりが見えることである。

状態表示があることで AI の会話の終わりが見えるようになるのが大事だったからね。(Master、2026-08-24)

これが目的である。状態表示は「AI が何をしているか」を細かく言うためのものではない。語が出ている間はまだ終わっていない、語が消えたら終わった——読む側が受け取るのはこの一つであり、語の種類ではない。この一文が無いと、次に読む者は粒度を上げる方向(「ツール使用中」など)を改善として読む。改善の向きは粒度ではなく、終わりの可視性である。

note 出所
考え中… その名前が部屋で宛先にされ、まだ発言を返していない。かつ端末に出力が届いている
出力中 端末に出力が届いている。宛先にはされていない
待機 セッションが走っていて、端末が 1 秒以上黙っている(#148)
制限中 そのセッションがステータスラインで知らせた 5 時間または週次の使用率が 100% に達している(#161)。Codex CLI の席は、アプリが利用上限での停止を見つけてから解除を確かめるまで(#294)

「出力中」は部屋への到着を主張しない。 言っているのは端末に出力が届いていることであって、部屋へ発言が来ることではない。「出力中」と出たのに部屋へ発言が来ない場合が実際にある(実機、2026-08-24)。この食い違いは受容する(Master、同日)——語がその意味を主張していないためである。到着まで言おうとすれば、この節は端末の出力ではなく say_to_room の往復を見ることになり、出所が二つに割れる。

CLI の出力は読まない。 届いたという事実だけを数え、中身は端末へ渡すだけである。「考え中」と「ツール使用中」を言い分けられるのは出力を解析する形だけであり、それは CLI ごとの実装で、相手の表示が変わるたびに壊れる。この節が出せるのは上の四語までであって、「ツール使用中」は出せない。これは範囲の宣言であり、後退ではない(#82 の判断)。「制限中」はこの判断の外側から来る(下記)——出力を解析したのではなく、CLI 自身が構造化された合図を寄越している。app-server 方式の Codex の席の様子(下記、#326)と、hook で知らせる Claude Code の席の様子(下記、#331)も同じ外側から来る。

app-server 方式の Codex の席は、server の知らせで様子を言う(#326、Master「すすめて」2026-10-07、調査は #324)。 この席だけは、アプリが席の server のクライアントであり続け、server がスレッドの様子を構造化した知らせで送ってくる(「Codex の席は自分の様子を app-server の知らせで知らせる」)。端末の文字を読まないため、#82 の判断は動いていない。足す語は次のとおりである。

札 会話面の下の一行・札の title 出所
許可待ち 許可待ち thread/status/changed の activeFlags に waitingOnApproval
答え待ち 答え待ち 同じく waitingOnUserInput
実行中 コマンド実行中 item/started の commandExecution
編集中 ファイル変更中 fileChange
ツール ツール使用中(server/tool) mcpToolCall(server/tool)、dynamicToolCall(namespace/tool)
検索中 Web 検索中 webSearch
要約中 要約中 contextCompaction
画像生成 画像生成中 imageGeneration
画像参照 画像参照中 imageView
委任中 サブエージェント実行中(tool) collabAgentToolCall の item/started から item/completed まで(tool は spawnAgent・wait などの操作名、#406)
様子不明 様子不明(app-server との接続が切れた) 席の server との接続が切れた(下記)
  • この席の知らせが先に立つ。 優先は、上限で止まったと分かっている席の「制限中」(#342、#372、下記)>「許可待ち」「答え待ち」>仕事の種類>スレッドが idle と分かっているときの「制限中」「待機」(#329、下記)>「考え中…」「出力中」>「制限中」>「様子不明」>端末の沈黙による「待機」である。許可を待つ TUI は画面を描き直すので「出力中」と出、黙れば「待機」と出る——この節が言い分けられなかった二つの場合であり、知らせはそれを言える。長いコマンドの間に端末が黙っても、「待機」ではなく「実行中」が立つ。宛先にされていても仕事の種類のほうを出す——どちらも「まだ終わっていない」ことを言い、種類のほうが多くを言う。
  • スレッドが idle と分かっていれば、端末の合図を見ずに「待機」を出す(#329、Master「Lunaがずっと出力中表示になってるな」2026-10-08)。 TUI が画面を描き直し続けると端末は黙らず、何もしていない席が「出力中」のまま残った。そこで事象は語とは別に、スレッドの様子(thread_status:idle/active/まだ分からない)を持つ。接続中でスレッドが idle なら、端末に文字が届いていても「待機」とし、利用上限なら「制限中」とする。語が無いことは idle ではない——推論中や答えを書いている active のターンにも語は無い。その間と、様子がまだ分からない間は、今までどおり端末の合図(「考え中…」「出力中」、黙れば「待機」)を出す。切断では様子を忘れ、「様子不明」は変わらない。様子は thread/status/changed と thread/start/thread/resume の答えの thread.status(idle/active。systemError・notLoaded は「分からない」に戻す)、turn/started(active)、turn/completed(idle)から読み、thread/closed で「分からない」に戻す。起動・再開の答えの idle も、ターンの前の idle の知らせも、受け取ったときにそのまま送る(#368、Master「Lunaが出力中で10分以上たってる?」2026-10-10)。 端末が始めたターンの知らせはアプリの接続にも届く——2026-10-10 に記録で確かめた(実装状況)。それまでは届くかが分からないとして、ターンがこの接続に届くと示す知らせを一度受け取るまで様子を「分からない」として送っていたため、起動し直した席は最初のターンまで「出力中」のまま残った。席の接続に届いた知らせの種類は一行ずつ記録する(同じ節)。
  • 待ちは仕事より優先する。 item/started は承認の前にも届く(#324)。flag が立っている間は「実行中」と出さない。二つの flag が同時に立てば「許可待ち」を出す。
  • 仕事は集合で持つ。 並列の道具があるため、item の id ごとに持つ。札は最後に始まったものを言い、一行は残りの数を「ほか N 件」で足す。
  • 言うのは種類と最小限の名前までである。 コマンドの全文、道具の引数、本文は出さない。道具の名前は制御文字を落とし、48 文字で切る。メッセージ・推論・計画の item は語にしない(「考え中…」「出力中」の受け持ちである)。サブエージェントは、この席のスレッドがそれを呼ぶ item(collabAgentToolCall)を Claude Code の席と同じ「委任中」として出す(#406、Master 観察 2026-10-12)。サブエージェント自身のスレッドの知らせは今どおりこの席のものとして読まない。
  • 札の語は四文字までにする。 下記「語の長さはこの場所の制約を受ける」のとおり、名前と note は一つの桁を分け合う。長い形(道具の名前を含む)は札の title と会話面の下の一行が持つ。この一行が札の言わない席を言うことは無いという線(#307)は変わらない——同じ一つの関数(rowActivity)から、札の語とその長い形を読む。
  • 色と動き。 仕事の語は「考え中…」「出力中」と同じ --ok の札で、同じく明滅する。「許可待ち」「答え待ち」も発話が終わっていない語として同じ色で出し、会話面の下の一行にも載るが、明滅はしない——動いているものが無いためである。新しい動きは足していない(動きは「動き(#307)」と同じく prefers-reduced-motion に従う)。
  • 古い語を残さない。 ターンの始まりと終わり、スレッドが active でなくなったこと、スレッドが閉じたことで、仕事と flag を消す。server との接続が切れたら、仕事と flag とスレッドの様子は消え、札は「様子不明」になる。「待機」には戻さない——待機中の切断でも、仕事の途中の切断でも同じである。端末の沈黙は、席が承認の画面で止まっているのか長いコマンドを走らせているのかを言えず、それを言っていた知らせはもう届かないためである。端末に出力が届いていれば「出力中」「考え中…」が、利用上限なら「制限中」が先に立つ——どちらも別の観測から来る語である。「様子不明」は地の色で、明滅しない。同じ席を起動し直せば、新しい端末の席として接続中から始まる(前の起動の知らせは端末の id で弾く)。
  • 利用上限で止まった Codex の席は、端末の合図より先に「制限中」を出す(#372、Master「いまのでまたルナが出力中のままになったね。制限中で何もできないのに。」2026-10-11)。 上限で止まったとき server が送るスレッドの様子は idle ではなく systemError であり(2026-10-10T15:41:43Z の記録)、上の idle の線が効かず、TUI の描き直しが「出力中」として立った。アプリが上限での停止を見つけてから解除を確かめるまで(#294、limited_source が codex)は、Claude の親の拒否(#342)と同じく、どの語より先に「制限中」を出す。
  • 再読み込みを越えない。 知らせは変わったときだけ届く。再読み込みで拾い直した画面は、次の変化まで知らない(「制限中」や五欄と同じ形)。
  • 承認には答えない。 承認は今どおり端末(TUI)で行う。アプリは server からの要求を読まない。

「考え中…」と「出力中」は、出力が届いている間しか出ない。 端末が 1 秒黙れば消え、「待機」に替わる。語がそれの指すものより長く残るのは、語が無いより悪いためである。特に「宛先にされた」ことには終わりが無い——確認プロンプトで止まったセッションは永久に答えないため、宛先だけを根拠にすれば「考え中…」はアプリを開いている限り立ち続ける。それが最悪の形であると issue が名指したものである。沈黙はこの節が持つことを許された失敗であり、観測していない状態の語は許されない。

「待機」が言うのは、端末が黙っていることだけである(#148)。 入力待ちと確認プロンプトでの停止を区別しない——区別するには CLI の出力を読むことになり、上の #82 の判断に反する(Master、2026-09-17)。「待機」はこの節の「観測していない状態の語は許されない」に反しない。主張しているのは観測した沈黙そのものであって、黙っている理由ではないためである。

「制限中」だけは沈黙の理由を言い、それでも出力を読んでいない(#161)。 出所はステータスラインの JSON の rate_limits.five_hour.used_percentage と rate_limits.seven_day.used_percentage である(「セッションは自分の様子をステータスラインで知らせる」)。Codex CLI の席は使用率から読まない。アプリが rollout の usage_limit_exceeded で停止を見つけてから、Codex の app-server が解除を確かめるまでの間であり、session-stats の limited で届く(#294、「Codex の席が利用上限で止まったとき」)。rollout の使用率は止まった席で 99 のまま動かなかった(#290)。読んでいるのは CLI が構造化して寄越す報告であって端末に出た文字ではないため、#82 の判断は動いていない。語が言うのはそのアカウントの枠が埋まっていることであり、セッションが走れなくなったことではない。

どちらか一方が 100% に達していれば出す(#161、決定1)。 5 時間と週次を重み付けせず、片方で足りる——5 時間の枠を使い切ったセッションは、週にどれだけ余っていても止まる。逆も同じである。

しきい値は 100% であり、判定は >= である(#161、決定7)。 公式 docs が spend limit は 100% を超えうると書いているため、等号だけでは取りこぼす。CLI が寄越さなかった欄は 0% ではない——欠けている枠は何も言わず、パネルの「—」と同じ線に立つ。

「制限中」は使用率が下がれば自動で消える(#161、決定2)。 消える縁を探す必要が無い。最後に届いた報告だけを読んでいるため、次の報告が低い値を運んでくればその時点で語は消える。#149 は「次の出力のバーストの先頭で消す」という縁を持っていたが、その規則ごと不要になった。

「待機」は計った沈黙にだけ出る。 端末を繋いだ時点から沈黙の時計を回し、1 秒のあいだ出力が届かなければ「待機」になる。何も出力しないまま始まったセッションも同じ 1 秒で「待機」になり、何も言わないまま残らない。繋いでから最初の 1 秒は何も出ない——その間の沈黙はまだ計っていない。再読み込みで拾い直したセッションも同じ形であり、以前の画面が計った沈黙を引き継いだとは言わない。

「待機」は色を付けない。 「考え中…」「出力中」を本文色に上げているのは、発話がまだ終わっていないことを言う語だからである。「待機」はその終わりの側に立つ語であり、「未起動」「終了」と同じ地の色に置く。語が現れても消えても、色の付いた語が消えたことが終わりの合図である点は変わらない。「制限中」も同じ地の色である——同じく終わりの側の語であり、色の軸を増やさない。

「制限中」は「待機」より優先し、「考え中…」「出力中」には譲る(#161、決定6。#149 の決定5を引き継ぐ)。沈黙という一つの状態について、「待機」より言えることが多いほうを出す——二つが同時に成り立つときに「考え中…」を出すのと同じ理由である(下記)。出力が届いていれば「出力中」「考え中…」が立つ:それはこの画面自身が観測した、動いているセッションである。

「制限中」は再読み込みを越える席と越えない席がある。 ステータスラインの使用率から立つ「制限中」は、報告が届いた時点の画面が持つ。再読み込みで走ったままのセッションを拾い直した画面は、次の報告が来るまで使用率を知らない——宛先の記録(#82)やパネルの五欄(#155)と同じ形であり、知らないことは言わない。アプリが止まりを見つけて立てる「制限中」は越える:Claude の親の拒否(#342)と Codex の席の利用上限(#294)は、席の見張りが毎秒その時点の値を送り直す(#374)。Codex の席は #374 まで変わったときにしか送っておらず、2026-10-11 に取り込みで再読み込みされた画面では、上限で止まった Luna の席が「出力中」のまま残った(Master「セッションを再起すればちゃんと制限中になったね。そうじゃないときはまだ出力中だったけど」)。

二つが同時に成り立つときは「考え中…」を出す。 好みではない。二語は同じ一つの観測(この端末が出力している)の上に立っており、宛先はそこに理由を足す——部屋に答を借りていて、まだ返していない。多く言えるほうを出す。

「動作中」ではなく「出力中」である。 観測したのはバイトが届いたことであって、CLI が働いていることではない。待機中のプロンプトの再描画も出力であり、「動作中」はこの画面に確かめる手段の無い主張になる。2026-08-24 の実機で見えていた Enter to confirm がまさにその場合である。

再描画は語が変わる瞬間だけである。 バイトごとではない。1 バーストにつき 2 回——出力が始まった時と、1 秒の沈黙で消える時——であり、出力が続いている間は再描画しない。@ の宛先の候補は、開いているときだけ名簿に合わせて描き直す。閉じている候補を名簿の動きで開くことはしない。

宛先の記録は、この画面が開いている間に届いた投稿だけから作る。 部屋の履歴は再生されないため、再読み込みで走ったままのセッションを拾い直した場合(#84 / #86)、そのアカウントが宛先にされていたことは知らない。その場合は何も言わない。出力の合図のほうは拾い直した端末でも同じに働く——次のバイトから成立する。

宛先は名前で突き合わせる。 to が運ぶのは表示名であり、部屋は投稿を全員へ配る。同じ名前を名乗る二人は両方が宛先である。宛先が複数あれば、それぞれを記録する(#204)。誰も名乗っていない名前は記録しない——記録すれば、後からその名前を取ったアカウントが、自分が走る前に立てられた問いで「考え中…」と出る。

語の長さはこの場所の制約を受ける。 「考え中…」は 4 文字、「出力中」は 3 文字、「待機」は 2 文字、「制限中」は 3 文字であり、既に出る「起動失敗」の幅を超えない。位置は note であって新しい列ではない——「未起動」と「考え中…」は排他であるため一つの場所で足り、#71 が固定した二列には触れない。

リセット時刻は添えない(#161、決定5 の但し書き)。 rate_limits.*.resets_at は届いており、決定5 は収まるなら「制限中(1:10 まで)」のように添えよと言うが、収まらない。名前と note は一つの桁を分け合っている。16.5rem のパネルから二つの固定列と余白を引くと、名前と note に残るのは 148px 前後である(0.78rem、全角 1 文字 12.5px 前後)。例の形はそれだけで 102px 前後を取り——この桁の幅を決めた実測「マスター(あなた)」の 112px(上記「参加者パネル」)とほぼ同じ幅である——名前に残るのは全角 4 文字に足りない。その 4 文字がまさに「マスター」である。桁が足りなければ縮むのは ellipsis を持つ名前の側であり、色を担っている名前が消える。したがって note は「制限中」だけである。

Claude Code の席は hook で様子を言う(#331)

Claude Code の席は、hook の知らせで様子を言う(#331、Master「やっちゃって」2026-10-08、調査は #324)。 起動の --settings に載せた http 型の hook が、ツール・許可待ち・サブエージェントの始まりと終わりをアプリへ送る(「Claude Code の席は自分の様子を hook で知らせる」)。端末の文字を読まないため、#82 の判断は動いていない。語と出し方は app-server 方式の Codex の席にそろえ、同じ seat-activity の事象で届く。

札 会話面の下の一行・札の title 出所
許可待ち 許可待ち(ツール名)。サブエージェントのものは 許可待ち(種類:ツール名) PermissionRequest
ツール ツール使用中(ツール名) 本体のエージェントの PreToolUse から PostToolUse / PostToolUseFailure まで
委任中 サブエージェント実行中(種類)。ツールが動いていれば(種類:ツール名) SubagentStart から SubagentStop まで
  • 優先は 許可待ち>ツール>委任中 であり、どれも無ければ今の語に任せる。 今の語とは「考え中…」「出力中」「制限中」「待機」である。この席の事象は connected が常に true、thread_status が常に null なので、「様子不明」にも、スレッドの idle による「待機」にもならない。
  • ツールは tool_use_id ごとの集合で持つ。 並列に走るためである。札は最後に始まったものを言い、一行は残りの数を「ほか N 件」で足す。サブエージェントも agent_id ごとの集合で持ち、同じく「ほか N 件」を足す。持つ数はそれぞれ 64 までで、超えれば古いものから捨てる(終わりの合図が来なかったものが積もり続けないため)。
  • サブエージェントの中のツールは、そのサブエージェントの側に数える。 agent_id の付いたツールの hook は本体の「ツール」にしない。Agent ツール(旧名 Task)そのものもツールとして数えない——それが始めたサブエージェントを「委任中」として出す。数えれば、前面で走るサブエージェントの間ずっと「ツール(Agent)」が「委任中」を隠す。
  • agent_type が空のサブエージェントは数えない。 CLI が自分の機能のために走らせる内部のエージェントであり、SubagentStop の agent_type が空になる(公式 docs hooks「SubagentStop」、2026-10-08 に読んだ)。始まりを数えていない agent_id のツールと許可も数えない。
  • 許可待ちは PermissionRequest から、人が答えのキーを打つまで(本体の待ちだけ、下記)、同じエージェントの次の PreToolUse か、同じツール名の PostToolUse / PostToolUseFailure までである。 PermissionRequest は tool_use_id を持たない(同 docs「PermissionRequest input」)ため、終わりはツール名で突き合わせる。次の PreToolUse で終わったときは拒否と読み、待ちが始まった時点で動いていた同じ名前のツールも一緒に消す——拒否されたツールには PreToolUse だけが来て、PostToolUseFailure は来ない(同 docs「PostToolUseFailure」の注記)。
  • 本体の許可待ちは、人がその席の端末に答えのキーを打ったところで閉じる(PR #332 のレビュー、Lay・Luna 2026-10-08)。 PreToolUse は許可の画面より前に来るため、人が許可したツールは終わるまで hook を一つも送らない。閉じなければ、許可した cargo build の間ずっと「許可待ち」が残る。数えるキーは、許可の画面に答えられるものだけである:Enter、Esc、数字(画面の番号つきの選択肢)、y / n(大文字も)。矢印などカーソルを動かすキー、ほかのエスケープ列(フォーカスやカーソル位置の報告を含む)、貼り付け、ほかの文字は数えない。数えるのは画面の端末の欄に人が打ったキー(write_pty)だけで、部屋の発言をアプリが端末へ打ち込む経路は通らない。閉じたとき「ツール」にはしない。 キーは許可の証拠ではない——許可か拒否か、Esc での中断かは分からない。そこで待ちと、待ちが尋ねていたツールの呼び出しを一緒に外し、札は今の語に任せる:許可したツールが出力していれば「出力中」、端末が黙れば「待機」である。「ツール」と出せば、観測していない状態を言うことになり、#82 の「観測していない状態の語は許されない」に反する。サブエージェントの待ちはキーでは閉じず、上に書いた終わり方に任せる——そのキーがどの待ちに答えたかを言えないためである。
  • サブエージェントの許可も待ちとして出す。 対話のセッションでは、背景で走るサブエージェントの許可もその場で本体のセッションに出て、人の答えを待つ(公式 docs sub-agents「Run subagents in foreground or background」、2026-10-08 に読んだ)。issue の当初の方針は agent_id 付きを待ちにしないとしていたが、待たずに拒否されるのは非対話のモードの場合であった(同 docs hooks「PermissionRequest」)。そのサブエージェントの次の PreToolUse、同じツール名の終わり、SubagentStop で終わる。
  • ターンの区切りで本体の残りを片付ける。 Stop と UserPromptSubmit で、本体のツールと許可待ちを全部消す。Esc で中断したターンには終わりの合図が来ず、Stop も来ない(同 docs「Stop」)ため、その残りは次の UserPromptSubmit で消える(時間切れでは消さない)。
  • サブエージェントはターンの区切りでは消さない。 対話のセッションではサブエージェントは既定で背景で走り、本体のターンが終わった後も動き続ける(同 docs sub-agents)。区切りで消せば、いちばん見たい場面で「委任中」が消える。代わりに、Stop の本文の background_tasks に type が subagent のものが一つも無いときだけ、持っているサブエージェントを全部消す(SubagentStop が来なかったものの受け皿)。background_tasks が無い Stop では消さない。
  • 本文が読めないときの扱い(#331 のレビュー、Lin 2026-10-08)。 アプリは本文を 4 MiB で読み切りをやめるため、大きな結果を運ぶ PostToolUse がいちばん切れやすい。PostToolUse / PostToolUseFailure の本文が JSON として読めなければ、どのツールが終わったか分からないので、その席の動いているツールと許可待ちを全部消す(本体もサブエージェントも。サブエージェントそのものは残す)。並列のツールが早く消えることはあるが、「使用中」が残り続けるよりよいとして受け入れる。Stop と UserPromptSubmit は、本文が読めなくても上の片付けをする——出来事はパスで分かる。それ以外の出来事は、読めなければ数えない(tool_input が大きく切れた PreToolUse のツールは出ない)。JSON ではあるが hook_event_name がパスと違う本文は、どの出来事でも読まない。
  • 言うのはツールの名前と、サブエージェントの種類までである。 名前は制御文字を落とし、48 文字で切る。ツールの引数・結果、プロンプト、発言は読まない。MCP のツールは mcp__<server>__<tool> の名前のまま出す。
  • 色と動き、再読み込み、起動し直しは Codex の席と同じである。 「ツール」「委任中」は --ok の札で明滅し、「許可待ち」は同じ色で明滅しない。再読み込みで拾い直した画面は次の変化まで知らない。同じ席を起動し直せば、新しい端末の席として空の様子から始まる。
  • 許可には部屋から答えられる(#336)。 札と一行は今までどおり何も決めない。答えるのは、下の「許可の確認に部屋から答える」のカードである。札の一行は MCP のツールを mcp__<server>__<tool> の名前のまま出す。

許可の確認に部屋から答える(#336)

Claude Code の席が許可の画面で止まったとき、部屋にカードを出し、画面の人がそこから 拒否 / 常に許可 / 一度だけ許可 を返せるようにする(Master 要望、2026-10-08、#334 の実機から)。 形は Claude デスクトップの Remote Control のカードに寄せた:誰が・どのツールを使ってよいか・中身、の下に三つのボタンを並べる。端末の許可の画面はそのまま出ており、どちらからでも答えられる。

  • カードは PermissionRequest の hook から出る。 アプリはその hook にすぐ答えず、接続を開けたまま部屋の答えを待つ(permission_prompt::HOLD_SECS、180 秒)。hook の答えを待つ間も端末の許可の画面は出る(2026-10-08 23:01 の実機、30 秒待つ hook の間に約 3 秒で出た、#336)。この hook だけ timeout を待つ時間より少し長い 190 秒にし、ほかの hook は 5 秒のままである(「Claude Code の席は自分の様子を hook で知らせる」)。
  • カードが言うこと。 席の名前と丸印、「許可の確認」(サブエージェントのものは「許可の確認・サブエージェント(種類)」)、ツール、ツールの入力(tool_input)の各項目である。MCP のツールはサーバ名とツール名に分け、「サーバ / ツール を使ってよいですか?」と言う。サーバ名は本文の mcp_server.name があればそれで切り、無ければ mcp__ の後の最初の __ で切る。入力は項目ごとに 2000 文字、24 項目までで切り、切ったことを書く。制御文字は改行とタブのほかは落とす。
  • 中身はカードにだけ出す。 #332 は、ツールの中身を記録にも画面にも出さないと決めていた。人が判断するには中身が要るため、この面に限ってその決めを改める。カードは部屋の発言ではない:床にも、トピックの記録にも載らず、どの席の端末にも打ち込まれない。画面の事象(permission-request)として届くだけであり、トピックを開き直したときは待っているものだけを記録の後ろに置き直す。
  • ボタンの意味。 拒否は decision.behavior: "deny" に、AI へ「部屋から拒否された」と伝える message を付けて返す(interrupt は付けない。ターンは止めず、AI は別の道か部屋での確認に進む)。一度だけ許可は behavior: "allow"。常に許可は behavior: "allow" に、要求が持ってきた permission_suggestions のうち type: "addRules"・behavior: "allow" のものをそのまま updatedPermissions として返す(公式 docs hooks「Permission update entries」、2026-10-08 に読んだ:hook は受け取った提案をそのまま返してよい)。モードを変える提案(setMode など)は返さない。足す規則と書き込み先はカードに書いておく。そうした提案が無い要求には 常に許可 を出さない。
  • 押せるのは画面の人だけである。 答えはウェブビューのコマンド(permission_answer)でしか届かず、セッションはそれを呼べない。セッションが話す道は部屋のソケットと端末であり、どちらにも待っている要求へ答える道は無い。部屋の発言の「許可」という文字も答えとして読まない。要求の鍵はアプリが付ける id で、席(トピック・アカウント)・起動(端末)・エージェントと一緒に持つ——PermissionRequest は tool_use_id を持たないためである。答えは一度だけ届き、届いた時点でその要求は待ちから外れる。
  • ほかで片付いたら、カードは押せなくなる。 次のどれかで、アプリは hook に {} を返して待ちをやめ、カードはボタンを外して終わり方を言う:その席の端末に答えのキー(Enter・Esc・数字・y / n)が打たれた(どの許可に答えたか分からないので、その席の待ちを全部やめる)、そのエージェントの許可待ちが終わった(上の「許可待ち」の終わり方と同じ)、同じ席が起動し直された、CLI が接続を閉じた、待つ時間が過ぎた。部屋から返した答えも「部屋から〜を返しました」と言うだけで、採られたとは言わない——端末が先に答えていれば、そちらが採られうる。
  • 誰も押さないとき、Pullcept が落ちているときは、今と同じく端末で答える。 待つ時間が過ぎれば {} を返し、何も決めない。Pullcept が落ちていれば hook は接続に失敗し、止めないエラーとして許可の流れが続く(同 docs「HTTP response handling」)。
  • 待たないもの。 本文が読めない要求(4 MiB で切れた、JSON でない、tool_name が無い)と、許可待ちとして数えていないエージェントの要求(始まりを聞いていないサブエージェント、CLI の内部のエージェント)は、カードを出さずにすぐ {} を返す。
  • Codex の席は対象外である。 app-server 方式の Codex の席の承認に部屋から答えることは、別の作業として残す(#336 の Luna の調査)。
  • 待った要求がどう終わったかを一行ずつ記録する(#346)。 アプリのデータの logs\permission-requests.log に、カードの id、席(トピック・アカウント)、ツール、エージェント、終わり方(answered・elsewhere・closed・timeout)、押したボタン、押し方、ほかで片付いたときはその合図(wait-ended:hook が待ちの終わりを言った、terminal-key:端末に答えのキー、relaunch:起動し直し)、カードを出した時刻と終わった時刻、待った長さ、答えを接続へ書けたか、を書く(permission_prompt::log_line)。押し方は、画面がクリックから読んだポインタの種類(mouse など。キーで押したときは key)、クリックの回数(キーなら 0)、ブラウザが作った出来事か、の三つである。ボタンを押したことと、ほかの片付き方とを実機で分けるためである。2026-10-09 の実機では、say_to_room の許可に PermissionRequest の hook から allow が三回返っていた(19:16:08・19:21:55・19:23:04)。allow を返す道はカードのボタンのクリックだけであり、マスターは大半を押していないと言っている。どう押されたかを次の実機でこの記録から確かめる。

許可待ちの席には投稿を打ち込まない(#346)

Claude Code の席が許可の画面で止まっている間、部屋の発言をその席の端末へ打たず、アプリが預かる。待ちが終わってから、届いた順に一度だけ打つ。 部屋は発言を端末へ打ち込み、最後に送信キーを送る(「発言は端末へ入る」)。許可の画面が出ている端末では、その送信キーが画面の既定の選択肢(「はい」)を選ぶ。2026-10-09 の実機では、手動モードのカードが誰も押さないまま 5〜10 秒で閉じ、hook の答えの記録は残っていなかった。その間も部屋の発言は端末へ打ち込まれていた(#346)。

  • 待ちは席の hook から読む。 本体かサブエージェントのどれか一つでも許可待ちであれば待ちとする(Activity::any_waiting)。どのエージェントの画面も同じ一つの端末に出るためである。待ちの始まりと終わりは上の「許可待ち」と同じである:PermissionRequest で始まり、人が端末に打った答えのキー(本体だけ)、そのエージェントの次の PreToolUse、同じツール名の終わり、ターンの区切り、SubagentStop、読めない PostToolUse で終わる。
  • 預かるかどうかは、発言を端末の列へ渡すときに決める。 列で順番を待っている間に許可の画面が出た発言は、そのまま打たれる。PermissionRequest の hook は画面より約 3 秒早く届く(上の「カードは PermissionRequest の hook から出る」)ため、この隙間は短い。
  • 待ちが終わってすぐには打たない。 終わりを聞いてから 1.5 秒(prompt_hold::RELEASE_GRACE)待ち、その時点でまだどのエージェントも待っていなければ渡し始める。端末で答えた直後に次の許可(並列の別のツール)が続くことがあり、その要求の hook は画面より前に届くためである。渡している途中で待ちが始まれば、そこで止め、残りは次の待ちの終わりまで預かる。
  • 順番を守り、一度だけ渡す。 預かっている発言があるか、渡している途中の間は、待ちが終わっていても新しい発言はその後ろへ並ぶ。端末が受け付けなかった発言は列の先頭へ戻し、捨てない。席を起動し直したときは、預かった発言を新しい起動へ持ち越す。ただし Codex の席の利用上限の解除確定後の再開の指示(#389)と、Claude の席へ解除予定時刻に打つ再開の指示(#401)は、その起動にだけ渡し、別の起動へは持ち越さない。
  • 利用上限では預からない(Claude #401、Codex #403)。 利用上限で止まっている席への発言も、ほかの発言と同じくこの列を通る。解除後の再開の指示を打つときに席が許可待ちであれば、指示もこの列で預かり、待ちの後に打つ。
  • 端末で答えることは変えない(#336)。 預かるのは部屋がアプリから打ち込む発言だけである。人が端末の欄に打つキーは今までどおり届き、許可の画面に答えられる。
  • 記録する。 預かった発言と、渡したときの行き先(typed:端末へ打った、refused:端末が受け付けず列へ戻した)を、上の logs\permission-requests.log に一行ずつ書く(prompt_hold::log_line)。発言の本文は書かず、message_id だけを書く。
  • 預かりはメモリの中だけである。 席を閉じたまま起動し直さなかったとき、Pullcept が落ちたときは、預かっていた発言は端末へは届かない。発言そのものはトピックの記録にあり、read_room_history で読める(「受容したトレードオフ」)。

サイドバーの開閉

部屋を挟む二つの面——左の「トピック」と右の「アカウント」——を、それぞれ畳める。会話面を広く使いたいときに閉じる(#118)。

操作はヘッダに置く。 左の面のボタンはヘッダの左端、右の面のボタンは右端であり、どちらも自分が畳む列の上に立つ(#252 で帯が窓の最上段になってからは、左はアプリのアイコンの右隣、右は窓の三つのボタンの左隣である)。左端はアプリ名「Pullcept」が出ていた位置である。あの語は、その窓を見ている者に対してその窓が何であるかを言うものであり、押しても何も起きなかった。語を残して隣にボタンを足す形は採らない(Master 判断)。

ボタンは畳んだ状態でも残る。 畳んだ面には押すものが残らないため、開く手はヘッダの側にしか無い。診断面の 端末 と ✕ が入口と出口を分けている形(上記「診断面」)とはここが違う——あちらは畳まれた面の外に入口があり、開いた面の中に出口がある。こちらは一つのボタンが両方を兼ねる。

アイコンは同じ柄の鏡像である。 縦線の入った角丸の四角であり、縦線は自分が畳む面の側に立つ。左右で別の絵にはしない——別の絵は、二つのボタンが別のことをすると言う(決定4)。二つのボタンは同じ図形を持ち、それを鏡像にしているのは CSS の一行である。左右で別々に描けば、以後は二つを揃え続けることになる。

名前は状態で変えない。 読み上げの名前は「トピックの開閉」「アカウントの開閉」で固定し、開いているかどうかは aria-expanded が言う。端末 ボタンと同じ形である(上記「診断面」)。一つの問いに二つの答えを置かない。

畳んだ面は幅 0 である。 細い帯は残さない(決定2)。帯は面へ戻る道標になるが、道標はヘッダのボタンとして常に在るため要らない。

畳んでも、その中で起きたことは失われない。 面は隠すだけであり、描画の元になる出来事——名簿、席、トピックの索引——は届き続ける。右の面を閉じている間にセッションが落ちても、開けばそう出ている(制約)。診断面を ✕ で畳んでもセッションが止まらないのと同じ形である(上記「診断面」)。

開閉の状態は config.json に残る(決定1)。 閉じたまま次の起動でも閉じている。文字サイズの二軸(会話面 #60、端末 #68)が localStorage に在るのとは置き場が違う——あちらが答えるのは「読んでいる文が読める大きさか」であって読む側の問いであり、こちらは窓の割り付けである。押すたびに書く。終了時にまとめて書く形は、アプリが行儀よく閉じなかった一回で失われる。この欄を持たない設定は両方とも開いた状態として読む。それがこの欄より前のすべての画面が出していた割り付けである。

キーは Ctrl+B であり、左の面だけを開閉する(決定3、AI 判断6)。 柄の参照元である Claude Desktop がその側に割り当てている。右の面に第二の組み合わせは与えない——左右で非対称になるが、キーを一つ発明するよりは、要ると分かってから足すほうを採る。

端末に焦点がある間、Ctrl+B は端末のものである(AI 判断5)。 attachCustomKeyEventHandler は Ctrl+V 以外のすべてに true を返すため、セッションはこのキーを受け取る。その上で同じキーがウィンドウにも届くため、止めなければ「面が畳まれ、同時にシェルへ Ctrl+B が渡る」形になる。Ctrl+B は端末側で意味を持つ(tmux の prefix、readline の backward-char)。端末ペインはセッションのものである(#84)。会話面の文字サイズのキーが既定動作を止めるのは webview 自身のズームを止めるためであって(上記「会話面の文字サイズ」)、セッションからキーを取り上げる根拠にはならない。面はボタンから開けるため、キーを譲っても失うものが無い。

会話面の文字サイズ

会話面(#room)の文字サイズを、読む側が変えられる。

効く範囲は会話の言葉である。 部屋に並ぶ本文と、それを書き込む入力欄の本文(#input。#354 まではテキストエリア)の二つが従う。同じ一つの文を、読む側と書く側で別の寸法にする理由が無い——一つの行為が二つの寸法に分かれる(#81)。入力欄が動かない形が当初の仕様であり、2026-08-24 に実機で確認もされていたが、それは実装漏れではなく判断であり、#81 がその判断を改めた。

入力欄のうち従うのは本文だけである。 本文の下の操作列(宛先のボタン・キーの案内・送信、#222)と、@ で開く宛先の候補(#204)は動かない。書かれる文ではなく操作であり、UI 全体の軸(#66)に残る。この軸が持つのは「読んでいる文が読める大きさか」であって、操作の押しやすさではない。

参加者パネル・診断面・端末は動かない。 ただし参加者パネルの丸印だけは従う(#260、上記「参加者パネル」)——会話面の丸印との大きさの関係を保つためであり、パネルの文字と操作は動かない。端末は自分自身の寸法を別に持つが、それはこの軸ではない(下記「診断面」)。範囲は宣言ではなく置き場所で成立させる——寸法は #room と入力欄の本文(#input)の二つに直接載るカスタムプロパティであり、他のどの要素もこの二つの子孫ではないため継承経路が無い。参加者パネル(#participants)にも同じプロパティを載せるが(#260)、パネルの中でそれを読むのは丸印だけである。共通の祖先(#conversation)に載せると診断面と状態行にも届き、:root に載せれば画面全体に届く。上から配って個別に打ち消す形は採らない——打ち消しを書き忘れた要素が範囲に入るのであって、範囲が置き場所そのものであれば忘れる先が無い。端末は特に巻き込めない。xterm.js は自前の寸法を持ち、そこから桁数と行数を計算するため、上から降ってきた寸法は表示だけでなく CLI が使える幅を変える。

既定値は現在の見た目のままである。 1rem、つまりブラウザ既定の 16px である。.message .body には font-size が無く、body にも html にも :root にも無かったため、会話面はこれまでずっと既定で出ていた(#194 以後、:root は UI の大きさを持つが、会話面はそれを割り戻す。「表示」)。入力欄も同じである——当時のテキストエリアは font: inherit で、辿った先に寸法が無いため同じ 16px に落ちていた。二つが同じ既定に立っていたことが、この軸へまとめても設定していない画面が変わらない理由である。まわり(タイトルバー 0.85rem / 参加者パネル 0.72rem 〜 0.78rem / 発言者名と時刻 0.78rem / 端末 13px)が寸法を与えられている中で、会話面だけが縮められていない。「チャットの文字が大きい」(実機観察、2026-08-23)はここから来ている。それでも既定は動かさない——設定していない画面が勝手に変わらないことを優先する。面どうしを揃え直すことは別の判断であり、この節が入れるのは変えられる仕組みのほうである。

寸法は行全体に効く。 会話面の側では、発言者名と時刻(.message .meta)は本文と同じ大きさを受け継ぎ(#258 までは本文に対する比 0.78em)、本文と一緒に動く。本文だけが大きくなり、それに付く名前と時刻が据え置かれる形は、一つの行が二つの面から来ているように読める。行の最大幅も同じ理由で em である——rem で固定すると、文字が大きくなるほど一行に入る文字数が減る。最大幅が持つべきものは文字数のほうである。入力欄の側では、枠の高さが rows(行数)で決まるため、寸法を上げれば枠も一緒に伸びる。書ける行数を保ったまま文字だけが大きくなる形にはならない。

段は離散である。 0.7 から 1.6rem までの梯子を持ち、その両端が上限と下限である。連続した値の範囲は、読める大きさかどうかの判断を利用者へ渡す。既定より下は段を細かく、上は粗く取る。観察が「大きい」である以上、実際に使われるのは下方向である。

保持は localStorage(pullcept.room-font-size)である。 再起動しても保たれる。画面の設定であって参加者の属性ではない——同じ会話を二人が読むとき、二人が同じ寸法を望む理由が無い。#40 が画面の設定に定めた形と同じである。参加者クラスで書き分けることもしない。誰の発言でも同じ寸法で表示する(#39)。

梯子に無い値が保存されていた場合は既定へ戻す。落ちる先が黙っているためである——梯子の外の値からは段を進めず、キーもピッカーも理由を出さずに効かなくなる。

操作はキーボードと設定メニューに置く。 Ctrl + = / - / 0(拡大 / 縮小 / 既定へ戻す)と、設定メニューの「表示」のピッカーである(#194、「表示」)。画面上のピッカーは設定メニューの一つだけであり、タイトルバーに置いていたものは #209 で外した。キーボードだけにしないのは既定値の側の事情である。既定を現状維持と決めた以上、設定していない画面に出るのは「大きい」と言われたその状態であり、見つけられない操作の裏に直し方を置けば、知っている者だけが直せる。タイトルバーの「設定」がその直し方への入口である。ピッカーの表示は既定に対する百分率である。選んでいるのは「今より大きいか小さいか」であって、内部で何 rem かではない。

キー操作は既定動作を止める。webview 自身のズームが同じキーに割り当たっており、そちらは画面を丸ごと巻き込む——参加者パネル・診断面・端末と、入力欄の操作(操作列と宛先の候補)である。この節が効かせないと決めたものがそのまま動くため、止めなければ範囲の限定そのものが成立しない。

診断面

会話面ともパネルとも別枠として、CLI の出力を表示する診断面を持つ。既定は折りたたみで、起動した CLI が頼まれずに終了したとき(起動直後に落ちた場合を含む。下記、#121)は自動で開く。

▶ で起動しても面は開かない(#215)。 畳まれていれば畳まれたまま、開いていれば開いたままである。起動した端末は選択中になり(行のハイライト・セッションの値)、面を開けばその端末が出ている。畳まれている間は端末へフォーカスを移さない——打ち込む先が画面に無いためであり、行で選んだときと同じ考え方である(#175)。以前は起動のたびに面を開いていた。起動直後に出るのが開発チャネルの確認プロンプトで、その答えを端末へ打つ必要があったためである。このフラグは #195 で起動行から外れ、起動直後に人が答えるべき問いは出なくなった。アプリが起動を断ったとき(start_session の失敗)も面は開かない。何も起動していないため面に読むべき出力が無く、理由は状況行と行の「起動失敗」にある。頼んでいない終了(下記、#121)で面を開く振る舞いは変えない——落ちる直前の出力が理由を読む材料だからである。画面の再読み込みの後に席から繋ぎ直したときに面を開く振る舞いも変えない(「画面の再読み込みに耐える」)。

畳んだままでも、PTY は面の寸法で始まる(#215)。 CLI の最初の描画が持たない窓の寸法で組まれないよう、端末は起動の前に面の寸法で fit する。隠れた枠には測る寸法が無いため、畳まれていれば同じ処理の中で面を一度だけ開いて測り、すぐ畳み直す。その間に画面は描かれないため、面が開いたようには見えない。起動した後に窓の寸法が変わっていても、面を開いた時点で測り直してセッションへ伝える(下記「端末の文字サイズは端末自身の寸法である」の、隠れている端末を測り直す経路と同じ)。

診断面は起動した CLI をそのまま端末として表示する。CLI はフルスクリーンの TUI であり、カーソル移動・画面消去・部分再描画を前提に出力する。行指向の表示器で映すと再描画の断片が積み上がるだけになるため、端末エミュレータ(xterm.js)で描く。

端末は WebGL の描画で描く(#278)。 xterm.js は既定では DOM で描き、ブロック要素と罫線(█▛▜▐▌ など)もフォントのグリフとしてセルに並べる。グリフはセルの縁で隙間なく接しないため、それらで組まれた図——Claude Code の起動時のマスコットがその一つ——にはマス目ごとの細い線が入る(Master の実機スクリーンショット、2026-10-05)。WebGL の描画(@xterm/addon-webgl)はこれらの文字をフォントを使わずセル単位で塗る(customGlyphs。DOM の描画では効かない)。すべての端末を同じ関数(useWebglRenderer)で開き、端末ごとに描き方が分かれないようにする(TERMINAL_OPTIONS と同じ考え方)。

ブロック要素の四角は整数ピクセルの縁で塗る(#278)。 WebGL の描画に替えた後も、▛ ▜ のように複数の四角でできた文字の中に 1 ピクセル幅の線が残った(Master の実機確認、2026-10-05。マスコットの両目の上の縦線)。アドオン(0.19.0)はこれらをセル幅 / 8 の倍数の座標で、四角ごとに fillRect で塗る(src/CustomGlyphs.ts の drawBlockElementChar)。セル幅が奇数ピクセルだと二枚が接する縁がピクセルの中央に来て、両側から半透明に塗られ、その列が薄い線に見える。上流の最新 beta(0.20.0-beta.300)も同じ書き方である。そこで、インストール時に当てるパッチ(scripts/patch-xterm-webgl.mjs、「開発・テスト・配布」)で、各四角の四辺を先に整数ピクセルへ丸めてから塗るようにした。隣り合う四角は同じ値に丸めた縁で接するため、隙間も重なりも出ない。

DOM の描画は床として残す。WebGL が使えない環境では読み込みが失敗し、端末はそのまま DOM で描く。後からコンテキストを失ったとき(GPU のリセット、開いているコンテキストが多すぎて webview が古いものを取り上げたとき)はアドオンを捨て、端末は空にならず DOM の描画へ戻る。文字サイズの変更・fit・再接続(#84)・畳んだ面の再表示(#215)の経路は描き方に依らず、触っていない。

これは却下された設計とは別のものである。区別すべき二つ:

  • 却下: アプリが CLI の出力を解析して発言を取り出す(liplus-desktop の stream_parser.rs のモデル)。部屋に並ぶものの出所が部屋の発言(画面の入力欄、say_to_room、アプリが受ける通知)ではなくなる。
  • 採用: 起動した CLI をそのまま映す。解析しない。部屋のメッセージ経路には一切関与しない。

部屋に並ぶ発言は画面の入力欄、say_to_room、アプリが受ける通知からのみ来る。端末は表示器であって情報源ではない。アプリは部屋の発言を端末へ入力し(#183 / #195)、端末ペインが送るキーから打ちかけの有無だけを読むが、それは部屋から端末への向きと、人から端末への向きである。端末の出力から部屋へ読むものは今も無い。会話面(#room)に端末を置かないことは維持する。

診断面と参加者パネルは併せて「部屋の下の機構が生きているか」という、会話面が答えられない問いに答える。部屋が沈黙しているとき、その沈黙が正常な待機なのか機構の停止なのかは、会話面だけからは区別できない。パネルが待受と生死を数字で出し、その理由は端末に残る。

端末をパネルへ畳まないのはこのためである。CLI が枠を描くには相応の桁数と行数が要り、名簿と同じ幅の列では TUI の再描画が成立しない。設計キャンバスは右下への同居を描いているが、あれは配置の絵であって寸法の指定ではない。

端末はアカウントごとに持つ。 診断面は一つの枠であり、その中に稼働中のアカウントの数だけ端末が立つ。選択されている一つだけが見え、残りは破棄せず隠す。隠れている端末にも出力は届き続けるため、別のアカウントを見てから戻ると、それまでのスクロールバックは残っている。

端末はトピックごとにも分かれる(#141、決定1)。 端末の単位は(トピック, アカウント)であり、アカウントだけではない。トピックを切り替えると、端末・タブ・セッションの値・参加者のリストがそのトピックのものに替わる。離れたトピックの端末は破棄せず隠し、そのセッションの出力は届き続ける。戻ればそのトピックで最後に見ていた端末が出る。パネルの値の追随先をトピックに替えるだけの形は取らない——端末が一つのままでは、同じアカウントを二つのトピックで同時に動かすこと(決定2)が画面の側で成り立たない。

一つの端末を共有しない。共有すると、稼働中の全セッションの出力が一枚の画面へ書き込まれる。CLI はカーソル移動と部分再描画で出力するため、二つの再描画が重なった後の画面から片方を分離することはできない。「最後に起動したものが見える」のではなく、全部が重なる(#57)。

診断面は窓である。 上端にヘッダを持ち、面全体が角丸の枠として畳まれる(#68)。角丸は面(#diagnostics)が持ち、端末(#terminal)は上側の角を手放す。値は 8px で、ヘッダの上端と面の下端の両方に同じ値が出る。端末が自前の 4px を持っていた形は、角の無い枠の中に角のある矩形を描いていた。ヘッダが載せるのは、窓の名前「端末」と、開いている端末のタブ、面を畳む ✕ である。端末の文字サイズのピッカーも載せていたが、#209 で設定メニューへ一本化した(下記「端末の文字サイズは端末自身の寸法である」)。名前は左端に置き、タブはその後ろに並ぶ。何も走っていないときにヘッダが自分について言うのはこの名前だけになる。

ヘッダに色を敷かない。 窓は全体が一つの地であり、それはページの地(--bg)であってパネルの色味(--surface)ではない(実機観察、2026-08-24)。帯であった頃の名残として面が --surface を持っていたが、それはヘッダを一色の帯として端末の上に置く形だった。ヘッダであることは位置と中身が言う。窓と会話面を分けるのは枠線と角丸である。

枠と端末の間の余白は詰める。 0.15rem である(実機観察、2026-08-24)。ここは余白ではなく端末が使っていない面積であり、CLI の桁数と行数は残った寸法から計算される。削った分は端末の面積になる。0 にはしない——xterm はカーソルと選択を自分の箱の縁まで描くため、枠に密着させると縁が枠線の上へ乗る。

端末の下辺に帯を出さない。 xterm 6 のスクロールは .xterm-scrollable-element が持ち、.xterm-viewport は誰も測らない空の div として残っている。それでも xterm 自身のスタイルシートはこの要素へ二つの指定を当てたままであり、どちらも保守されていない——テーマの届かない #000 の塗りと、ネイティブのスクロールバーを 15px 分確保する overflow-y: scroll である。端末の箱は面いっぱいへ伸ばしてあり、xterm が描くのは行の整数倍であるから、行数 × 行の高さ を引いた余りは誰も描かない帯として下辺に残る。そこをこの空の要素が黒で塗り、右端にはネイティブのスクロールバーの下向き矢印が出ていた(#77)。帯は割り算の余りであるから面の高さとともに動く——実測で 600〜900px の窓に対して 4.44px から 14.44px である。新しく生まれたものではない。#68 が 800px の窓で 6.63px から 14.44px へ動かし、矢印の高さに届かなかったものがちょうど一つ分になった時点で、縁ではなくスクロールバーとして読まれるようになった。

補正の側は壊れていない。fit が測るのは端末の枠(.term)であり、そこから出る桁数と行数は実際に描かれている面と一致する——修正の前後どちらでも 126×23 に対して 960×345 である。直すのは何も描かない要素であって測り方ではないため、.xterm-viewport を描画から外す。塗りや overflow をこちらから上書きする形は採らない。その形では、次の面の高さが作る余りの上に帯がまた立つ。

タブと参加者の行は連動する。 どちらを押しても同じ端末が画面に出る。同じ選択を二つの面が持つ形であり、#59 が名簿から外した重複とは逆を向く判断である(「受容したトレードオフ」)。タブが答えるのは「いまどの端末が開いているか」であり、行はそれを並べて読むことでしか答えない。二つは一つの描画から同時に出す。連動を実装の約束にせず、構造で成立させるためである。

✕ は面を畳む。セッションは終わらない。 端末もリスナもスクロールバックも残り、開き直せば同じタブが並ぶ。ヘッダの 端末 ボタンは残す。畳まれた状態から開く手は ✕ の側に無く、二つは同じ操作の入口と出口である。セッションを終わらせる手は参加者の行の 終了 のままであり、端末側へは移さない。

終了した端末は、タブが閉じられるまで残る。 落ちる直前の出力が、なぜ落ちたかを読む唯一の材料である。「読んだ」の合図は、以前は「別のアカウントを選ぶ」という暗黙の選択だった(#57)。終了したタブに ✕ が付いたことで、その合図は明示の操作へ移る(#68)。合図そのものは失われず、置き場所が変わる。走っているセッションのタブには ✕ を出さない。出せば「セッションの終了」と読まれ、行の 終了 を端末側へ移さないという制約を破る。同じアカウントを再起動した場合は、前回の端末が新しい端末に置き換わる。

終了は、頼んだものと頼んでいないものを分けて言う(#121)。 アプリが自分で止めた終了——行の 終了、トピックの削除、アプリの終了——は、人がダイアログで承知して押した結果である。これを「終了コード不明。端末を確認してください」と事故の文面で言うと、無い問題を探させることになる。無視してよい警告が定常的に出れば、本当に確かめるべき終了が来たときにも無視される。したがって頼んだ終了では、状況行は「◯◯ を終了しました。」だけにし、エラーにせず、端末ペインを開かない。 既に開いていれば閉じもしない——見え方を変えない。セッションの値の行も 終了 だけを出し、エラーの色にしない。頼んでいない終了(CLI が自分で終わった、落ちた)は従来どおりである。 終了コードを添えたエラーの文面を出し、表示中の端末なら面を開く。落ちる直前の出力が、なぜ落ちたかを読む唯一の材料だからである。

印は止めた側が付け、終了のイベントがそれを運ぶ。 画面の側で推し量らない。終了コードが無いことは同じ事実ではない——頼んでいない終了でもコードは取れないことがある。PTY の側では、止める経路(kill_pty / kill_each / kill_all)がどれもセッションを一覧から外してから止め、一覧から外すのはそれ以外にはセッション自身の読み取りスレッドの終わりしかない。したがって読み取りスレッドが終わりに見たとき既に一覧に無いことが、頼んだ終了の記録そのものであり、pty-exit-{id} はこれを { code, requested } として運ぶ。別の記録を並べて持たない——同じ事実の二つ目の置き場は、食い違う場所が一つ増えるだけである。一覧への登録は読み取りスレッドを立てる前に済ませる。 起動直後に死ぬ子(コマンドが無い等)は、登録より先に出力の終わりへ着きうる。そのとき一覧に無いことを頼んだ終了と読めば、頼んでいない終了を頼んだものとして黙らせることになり、後から入った登録は死んだセッションを走っていると言い続ける。

端末の文字サイズは端末自身の寸法である。 寸法の軸は三本あり、いずれも独立している——会話面(#60)、端末(#68)、UI 全体(#66)。互いに相対で乗らない。これは表示だけの話ではない。xterm.js は fontSize から桁数と行数を計算するため、変えるたびに fit を呼び、セッションへ新しい寸法を伝える。

操作は設定メニューの「表示」のピッカーである(#194、「表示」)。端末面のヘッダにも同じ値のピッカーを置いていたが、#209 で外した。保持は localStorage(pullcept.terminal-font-size)、段は離散の梯子(9〜24px)でその両端が上下限である。#60 と同じ形である。既定は 13px——これまで端末が出ていた寸法であり、設定していない画面は変わらない。表示は px である。#60 が百分率で出すのは、会話面が何かに対して読まれるものではないためであり、端末は CLI の描く枠に対して読まれ、何桁入るかを決めるのはこの数そのものだからである。梯子に無い値が保存されていた場合は既定へ戻す(#60 と同じ理由)。

端末のカーソルの形は設定で選ぶ(#448)。 設定メニューの「表示」の「端末のカーソル」で、四角(block)・縦線(bar)・下線(underline)から選ぶ。どれがよいかは好みであり、正解が無い(Master 2026-10-17「完全好みっぽい」)。既定は四角——xterm.js の既定で、これまで端末が出ていた形である。保持は文字サイズと同じく localStorage(pullcept.terminal-cursor-style)で、知らない値が保存されていれば既定へ戻す。変えると、開いている端末すべてにすぐ効き、桁数と行数は変わらない。CLI は形を指定していない。Claude Code は CSI Ps SP q を送らず、Codex は ESC[0 q(xterm.js では「設定の形に戻す」)だけを送るため、ここで選んだ形がそのまま出る。

寸法は画面ごとの設定であって、セッションの属性ではない。画面上のすべての端末が同じ寸法で出る。隠れている端末は次に表示されたときに測り直す——隠れた枠には測る寸法が無いためであり、そのセッションへ新しい桁数が伝わるのもその時点である。

終了はアカウントごとに行う。 参加者のリストのその行から、そのアカウントのセッションを終了する。席の解放は別途要らない——RoomSeats は稼働の有無を PTY へ問うため、終了すれば席は空き、そのまま再起動できる(「アカウント」)。画面が再読み込みされた後もこの行から終了できる。端末は席を見て作り直される(「アカウント」の「画面の再読み込みに耐える」)。

終了は取り消せない操作であり、表示を切り替えるだけの操作の隣に並ぶ。無地のクリック一つでは実行しない。クリックは確認のダイアログを開き、そこで答えるまで何も終わらない。ダイアログは「取消」と「終了」の二つの答えを持ち、焦点は「取消」にある。Escape は取消と同じ答えである——開いたダイアログの上で当てずっぽうに押した鍵が、何も変えない側へ倒れる。

window.confirm を使わない。 ホストが答えなかった場合の失敗の向きがどちらも誤りであるためである。答えを待たない実装ではボタンが黙って何もしない側へ倒れ、答えを既定で肯定と読む実装では一度のクリックで落ちる。window.confirm はこのアプリのどこにも残っていない。

確認は webview の中の <dialog> で出す。 これは上とは別の判断である。window.confirm を断るのはホストに答えを預けないためであり、確認をどの面へ出すかはそこから決まらない。#71 以前は確認をボタン自身に置いていた——一度目のクリックが武装し、ボタンの文言と見た目が変わり、二度目が実行する形である。ダイアログへ移したことで「どちらの状態にいるかをボタン自身が表示する」形は手放した(#71)。ホスト依存の失敗の向きは、この <dialog> がアプリの中にあるため、そもそも起こらない。

アカウントの削除は二度押しのまま残っている(「アカウント」)。同じ形へ揃えるかは #71 では決めず、#72 で扱う。

端末はセッションへの入力路でもある。キー入力はそのまま PTY へ渡り、ウィンドウのリサイズは CLI 側へ伝わる。

入力の宛先は、今見えている端末のセッションである。 キー入力もペーストも、その端末が属するセッションへ送る。「最後に起動したセッション」は宛先にしない。画面に見えている端末と、打ち込まれる端末が一致しない状態は、表示の混線より実害が大きい(#57)。リサイズも同様に、見えている端末のセッションへだけ伝える。

これも会話面とは別の axis にある。CLI はフォルダごとに初回の信頼確認を出すため、この経路が無いとどの作業ディレクトリでもセッションは起動せず、部屋が立ち上がること自体が構造的に不可能になる。

信頼確認や権限確認をアプリが自動で答えることはしない。それらはセキュリティ上の確認であり、判断は人間が行う。アプリが負うのは、人間が答えられる経路を用意することであって、代わりに答えることではない。

アプリの終了

アプリを閉じると、走っているセッションも終わる(#85)。 CLI の子プロセスは job object に入っていないため、アプリのプロセスが消えても道連れにならない。後始末を置かなければ、孤児になった CLI は接続先を失ったまま走り続け——部屋のサーバはアプリと一緒に死ぬ——次の起動では席の台帳が空であるから、同じアカウントをもう一度起動できてしまう。同じ CLI が二つ走る形であり、止める手は Task Manager しか残らない。

「アプリは閉じるが CLI は残す」という使い方は採らない。孤児が生まれる形を既定にしないほうを採る(2026-08-25、Master の判断)。

落とす前に確認を挟む。 閉じる操作は取り消せる操作の隣に在り——ウィンドウの ✕ はどのアプリでも同じ場所に在る——、その一度のクリックが走っているセッションを全部終わらせる。確認の形は行の 終了 と同じ理由に従う(上記「診断面」)。window.confirm は使わない。ホストが答えなかった場合の失敗の向きがどちらも誤りであることは、閉じる操作でも変わらない。確認は webview の中の <dialog> で出し、焦点は「取消」に置き、Escape は取消と同じ答えである。

行の 終了 とは別のダイアログである。 一つの要素に二つの問いを持たせると、開くたびにどちらの顔で開くかを決めることになり、答えもどちらの問いへ戻すかを決めることになる。行の 終了 がアカウントのモーダルの第二の顔にならなかったのと同じ理由である。文言も分ける——問うているのは「アプリの終了」であって「セッションの終了」ではない。

走っているセッションが無ければ確認は出さず、そのまま閉じる。 落とすものが無いのに問う形は、閉じる操作に一段足すだけである。走っているかどうかはアプリの席の台帳へ問う(「アカウント」の「一つのアカウントは一つの部屋に一席まで」)——画面が持つ写しは最後に更新した時点のものであり、閉じてよいかの判定に使えるほど新しいとは限らない。起動が飛んでいる最中の席(セッションがまだ無い席)も数に入れる。残そうとしているのはプロセスであり、まだ見えていないことは無いことではない。

「取消」は閉じる操作の取り消しである。 アプリは閉じず、セッションも落ちない。「閉じないがセッションだけ落とす」答えは置かない——それは行の 終了 が持つ操作であり、この問いはアプリが消えることについてのものである。

終了は既存の kill の経路を通す。 席の解放は別途行わない。RoomSeats は稼働の有無を PTY へ問うため、プロセスが消えれば席は空く(「アカウント」)。ただし一括の終了は行の 終了 の繰り返しではなく、アプリ側で PTY の一覧そのものを一度に掃く。画面が持つ一覧は写しであり、写しに載っていないセッション——たとえば起動が飛んでいる最中のもの——が残るのは、この経路が防ごうとしている当のものである。

実際に木を落としているのは kill ではなく drop である。 PTY の項目を一覧から取り出して捨てると master が閉じ、その ConPTY に繋がったプロセスが道連れになる。TerminateProcess の成否はここでは効いていない。

そのため、一括終了には失敗を報告する経路が無い。 Windows では Child::kill が WinChild::kill へ解決し、自分の TerminateProcess の結果を捨てて常に Ok(()) を返す(portable-pty-patch/src/win/mod.rs)。失敗を集める形を置けば永久に空の集合を集め続けるだけであり、「失敗したら閉じない」という保護が、書かれてはいるが実在しない状態になる。根は do_kill の成否判定が反転していることであり、#96 で扱う。#96 が解ければ kill() は本当に失敗を返せるようになるため、その時点で失敗の経路を足し直す判断ができる。それまでは置かない。

扱わない経路が二つある。 一つはクラッシュで、後始末そのものが走らないため孤児は依然として生まれうる。job object に入れれば OS が面倒を見るが、それは確認を挟む形とは別の軸である(job object は問わずに落とす)。もう一つは孤児の引き取りで、起動時に前回の孤児を見つけて繋ぎ直す形である。pty id が Rust のメモリにしか無いため持続化の話になる。正常終了の経路で孤児が生まれなくなる以上、引き取りの必要性そのものは下がる。

閉じる操作の差し止めはアプリ側に配線を持たない。 Tauri は webview が CloseRequested を購読した時点で自ら閉じるのを差し止め(tauri::manager::window::on_window_event が window.has_js_listener を見て api.prevent_close() を呼ぶ、tauri 2.11.5)、購読側が差し止めずに戻ればウィンドウを破棄する。問いと答えはダイアログの在る側で完結するため、Rust 側に on_window_event を置く必要が無い。

タイトルバーの閉じるも同じ問いを通る(#252)。 窓に標準の枠が無くなり、閉じるはタイトルバーの右端のボタンになった(上記「タイトルバーは窓の最上段である」)。このボタンは窓の close を呼ぶ。close は OS の閉じると同じ CloseRequested を出すため、上の差し止めと問いがそのまま働く。問わずに壊す destroy は呼ばない。close にも ACL の権限が要り、core:window:allow-close を src-tauri/capabilities/default.json に足してある——次の段落と同じ形の取りこぼしを、ボタンを足す変更の中で塞いだ。Alt+F4 は従来どおり OS から同じ経路に入る。

閉じるを完了させるのは webview 側の destroy であり、そのための権限が要る(#97)。 onCloseRequested は購読側が preventDefault せずに戻った後、自分で window.destroy() を呼ぶ(@tauri-apps/api/window)。閉じるを成立させる一手はこれだけである。この呼び出しは Tauri の ACL を通るため、src-tauri/capabilities/default.json に core:window:allow-destroy が要る。core:default の束には入っていない——core:window の既定に並ぶのは allow-title や allow-is-visible のような問い合わせ系だけであり、窓を壊す権限は明示して初めて付く。権限が無ければ destroy は ACL に拒否され、その拒否はリスナの中の promise で捨てられるため画面には何も出ない。差し止めだけが残り、✕ を押しても閉じないアプリになる——#95 が購読を足したとき、購読が要求する権限は権限表へ入っていなかった。購読を足す変更と、その購読が要る権限は、同じ変更に揃える。

UI の方向(2026-08-21 時点、暫定)

設計キャンバス: Pullcept UI 設計

2 枚のアートボードを並べ、その差分をロードマップとして読む形にしてある。

  • 次の一手 — 現在の実装に参加者パネルと宛先を足した、実装可能な範囲。両方とも実装済みである。この 1 枚と実装の差は、操作ボタンのうち未実装のリサイズとミュート(端末と終了は実装済みで、参加者のリストの各行のメニューに載っている)、宛先の見た目(削除可能なチップ)、そして #53 / #57 / #59 / #62 で入ったアカウントまわり(種別ごとのグループ、行の開始・終了・編集、モーダル、ランチャ行の廃止)である。宛先は本文に @名前 と書き、送ると部屋がそれを宛先へ移して本文から消す形であり(#204 / #206)、チップの形の差は追っていない。アカウントの側はキャンバスが実装より大きく遅れており、差は #46 で追う。
  • 全体像 — ルーム一覧 / ダイレクト / 参加者・セッション状態パネル

色と寸法は src/styles.css の実値をそのまま使っている(--accent: #3a6ea5 / --surface: #f4f4f7 / 角丸 4・6px / system-ui + Yu Gothic UI)。当時の参加者ごとの色は accent と同じ彩度・明度で色相だけ変えたもの(現行の色設定は上記「参加者ごとの色」)。絵と実装が同じトークンの上に乗るため、キャンバスの値をそのまま実装へ移せる。

この UI は本番向けではない。 段階的に作り、機能と並行して改善する前提の暫定案である。確定仕様として扱わない。

決まったこと

  • 会話面はメッセージ単位を保つ。端末は診断面側に置き、会話面には置かない。
  • 参加者パネルを持つ(実装済み。上記「参加者パネル」)。セッションの接続方法・起動コマンド・作業ディレクトリ・開始時刻を出す。操作のうち端末と終了は実装済みで、アカウントごとの行に載っている。ミュートは未実装であり、キャンバス側にのみある。
  • 発言に宛先を持たせる(実装済み。本文に参加者の @名前 を書く。入力欄で @ を打てば候補から選べる。複数可。書かなければ全体宛。#204 / #206)。
  • 参加者ごとの色(実装済み。現行の宣言色・導出色はテーマごとの共通の明度・彩度で色相のみ)。色相は宣言でき、宣言が導出に優先する(上記「参加者パネル」の梯子)。
  • 名前と色は「この部屋での自分は誰か」の宣言である(実装済み。人間もセッションも、アカウントの属性として)。
  • アカウントは起動していなくても存在する(実装済み。「アカウント」)。同一性は id であり、名前は表示と宛先のための属性である。
  • 人間はアカウントである(実装済み。種別 admin。#53 が保留にし、#59 が決めた。#192 で user から改名)。画面の本人も他の参加者と同じ行を持ち、名簿と宛先の出所は一本化された。人間の同一性を config のエントリとして持つことは受け入れる——本人であることは localStorage に残り、config に入るのは名前と色だけである。
  • 参加者のリストは一つである(実装済み。上記「参加者パネル」)。種別で分かれ、見出しが件数を示す。種別は作成時の宣言であり、接続の種類から推測しない。
  • アカウントの作成・編集・削除は同一 webview 内のモーダルで行い、明示的な決定と取消を持つ(実装済み。「アカウント」)。

決まっていないこと

いずれも費用が判明していて、判断だけが残っている。

項目 費用
システム発言の出所 エージェントが say_to_room で言うなら可。アプリがファイルや git を監視して言う形は、constraints の一行目の線(部屋のために要るか)で量ることになり、要るとはまだ言えていない
webhook を部屋へ流す 実証中(#169、「webhook の通知」)。アプリ本体が受けて部屋へ出す形であり、#32 が挙げた「参加者として書く形」ではない。constraints との関係は #172 で整理した(部屋に要る機能としてアプリ本体が持つ)
ミュート design/Vision.dc.html にあるが、まだ求められていない。#59 の対象外

ニックネーム表示(#383)

設定の基本欄に任意の「ニックネーム」を置く。参加者・アカウント行は上段に呼称、下段に元の名前を表示する。未設定では名前を一度だけ表示する。会話の話し手欄は新着・履歴とも投稿時の speaker(呼称、未設定なら名前)だけを表示する(#391)。改名・切断・アカウント削除でも過去の話し手を変更しない。投稿の full_name は元の名前のメタデータとして保持し、full_name のない旧投稿も speaker だけを表示する。本人の右寄せ行の話し手欄を持たない既存表示は維持する。宛先候補は呼称だけを挿入し、衝突する候補には全参加者宛てであることを示す。

Clone this wiki locally