Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,12 @@ npm run tauri dev

作業ディレクトリの入力欄があります。初回はホームディレクトリが入っているので、セッションを動かしたいディレクトリへ変更してください。

右の参加者パネルに、部屋にいる参加者と、まだ起動していないアカウント(「未起動」)が並びます。その下にセッションの裏の値(部屋ソケット、セッションの生死、接続方法、起動コマンド、作業ディレクトリ、開始時刻、ウィンドウ)が出ます。発言の色は参加者ごとに違い、名簿の点と同じ色です。色は選んだものが優先され、選んでいなければ自分は accent(青)、他の参加者は名前から決まります。名簿では自分の行に「(あなた)」が付きます。宛先に選べるのは部屋に居る参加者だけで、未起動のアカウントは出ません。
右の参加者パネルに、部屋にいる参加者と、まだ起動していないアカウント(「未起動」)が並びます。その下に「端末」の列があり、この画面が端末を持っているアカウントが一行ずつ出ます。行を押すとその端末が診断面に出て、行の「終了」でそのアカウントのセッションを終了できます。終了は二度押しです——一度目でボタンが「本当に終了」に変わり、数秒で戻ります。さらに下にセッションの裏の値(部屋ソケット、セッションの生死、接続方法、起動コマンド、作業ディレクトリ、開始時刻、ウィンドウ)が出ます。部屋ソケット以外は、選んでいる端末のアカウントのものです。発言の色は参加者ごとに違い、名簿の点と同じ色です。色は選んだものが優先され、選んでいなければ自分は accent(青)、他の参加者は名前から決まります。名簿では自分の行に「(あなた)」が付きます。宛先に選べるのは部屋に居る参加者だけで、未起動のアカウントは出ません。

「診断」を開くと**起動した CLI の端末**が出ます。端末はそのまま操作できます。CLI はフォルダごとに初回の信頼確認を出すため、最初の一回はここから答えてください。

端末はアカウントごとに別です。複数のセッションを動かしても出力が混ざることはなく、キー入力とペーストは**今見えている端末**のセッションへ届きます。別のアカウントを見てから戻っても、それまでの出力は残っています。終了したアカウントの端末も、別のアカウントを選ぶまで残ります(なぜ落ちたかは、落ちる直前の出力にしか書かれていないためです)。

この端末は表示と操作のためのものです。ここに映る内容が部屋の発言になることはありません。部屋に並ぶのは channel 経由の発言だけです。

アカウントを選び「セッション参加」を押すと、次の 2 つが行われます。
Expand Down
34 changes: 30 additions & 4 deletions docs/0-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,8 @@ liplus-desktop の `stream_parser.rs` および `spawn_stream_pty` / `spawn_stre
- 宣言色(`hello` の `hue` / `room_join` の `hue`。宣言 > 自分の accent > 名前からの導出)
- チャットルーム UI(メッセージ一覧、発言者表示、入力欄、参加者から選ぶ宛先)
- 参加者パネル(名簿、参加者ごとの色、部屋ソケット、セッションの生死と終了コード、接続方法・起動コマンド・作業ディレクトリ・開始時刻・ウィンドウ)
- 診断面(CLI の端末表示と入力)
- アカウントごとの端末(パネルの端末の列で選ぶ、行からの終了、切り替えで残るスクロールバック)
- 診断面(アカウントごとの CLI の端末表示と入力)
- Windows CI(`npm ci` と Rust コンパイル確認)、Release 公開時の CD

### 実機で確認済み(2026-08-21)
Expand All @@ -262,6 +263,7 @@ liplus-desktop の `stream_parser.rs` および `spawn_stream_pty` / `spawn_stre
- 床を入れた後の同時発話の実機観測。参加者間で発言が届く経路、起動ごとの名乗り、参加者ごとの登録鍵、床の判定はいずれも実装済みである。2026-08-22 の実測(2 セッションが互いの発言を受け取らない / 2 セッションがどちらも `Claude Code` を名乗る)は #39 と #40 の修正前のものであり、2026-08-23 の二重応答(#47 の premise)は床の実装前のものである。いずれも修正後の再計測は済んでいない。
- 順番の付与(弾くだけでなく「あなたは N 人め」を返す形)。戻り値の形は段差を残していないが、二重計算が痛むと測れてから判断する。
- アカウント面の実機確認。作成・改名・削除、オフライン表示、二重起動の拒否、改名後の `.mcp.json` 登録の追随はいずれも実装済みで、CI の型検査とテストは通っているが、実機での操作は未確認である。
- アカウントごとの端末と終了の実機確認。2 セッションでの出力の分離、入力の宛先、切り替え後のスクロールバック、終了後の席の解放と再起動はいずれも実装済みであり、CI の型検査は通っているが、実機での操作は未確認である。
- 会話ログの永続化と観測 UI
- plugin としての allowlist 掲載(配布の第二段階)

Expand Down Expand Up @@ -298,6 +300,16 @@ liplus-desktop の `stream_parser.rs` および `spawn_stream_pty` / `spawn_stre

端末はこのパネルへ畳まない。設計キャンバスは右下へ同居させているが、パネルの幅では CLI が枠を描けない(下記「診断面」)。

**名簿の下に、端末の列を持つ。** この画面が端末を持っているアカウントが一行ずつ並び、行をクリックすると診断面の表示がその端末へ切り替わる。行には終了が付く。

名簿の第二の一覧ではない。答える問いが違う。名簿は「誰が部屋に居るか」であり、出所は部屋の名簿で、鍵は参加者が接続してきたコネクションである。端末の列は「この画面がどのセッションの端末を持っているか」であり、鍵はアカウント id である。集合も一致しない——別の場所から参加した参加者はここに端末を持たず、終了した直後のセッションは端末を持っていて席を持たない。

行の鍵をアカウント id に置くのは、行に操作を載せるための条件でもある。端末の表示・入力・終了はいずれも一つのアカウントに対する操作であり、名前で突き合わせると、二つのアカウントが同じ名前を名乗った時点で別の行に当たる(上記「アカウント」)。

端末を持たないアカウントは、この列に行を持たない。未起動のアカウントに出せる端末の操作は無い。

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

### 診断面

会話面ともパネルとも別枠として、CLI の出力を表示する診断面を持つ。既定は折りたたみで、セッションの起動失敗や異常終了を検出したときは自動で開く。
Expand All @@ -315,8 +327,22 @@ liplus-desktop の `stream_parser.rs` および `spawn_stream_pty` / `spawn_stre

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

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

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

終了した端末は、その場では消さない。落ちる直前の出力が、なぜ落ちたかを読む唯一の材料である。別のアカウントが選ばれた時点で捨てる。同じアカウントを再起動した場合も、前回の端末は新しい端末に置き換わる。

**終了はアカウントごとに行う。** 端末の列の行から、そのアカウントのセッションを終了する。席の解放は別途要らない——`RoomSeats` は稼働の有無を PTY へ問うため、終了すれば席は空き、そのまま再起動できる(上記「アカウント」)。

終了は取り消せない操作であり、表示を切り替えるだけの操作の隣に並ぶ。無地のクリック一つでは実行しない。一度目のクリックは武装するだけで、ボタンの文言と見た目が変わり、数秒で武装は解ける。二度目のクリックが実行する。

`window.confirm` を使わないのは、ホストが答えなかった場合の失敗の向きがどちらも誤りであるためである。答えを待たない実装ではボタンが黙って何もしない側へ倒れ、答えを既定で肯定と読む実装(アカウント削除が取っている側)では一度のクリックで落ちる。画面側で武装状態を持てばどちらも起きず、どちらの状態にいるかをボタン自身が表示する。

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

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

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

信頼確認や権限確認をアプリが自動で答えることはしない。それらはセキュリティ上の確認であり、判断は人間が行う。アプリが負うのは、人間が答えられる経路を用意することであって、代わりに答えることではない。
Expand Down Expand Up @@ -411,7 +437,7 @@ CI が実行するもの:
上流から順に確認する。各段は下流の前提であるため、順序を飛ばさない。

1. **部屋ソケットが待ち受けているか。** 右の参加者パネルの部屋ソケットの行を見る。`127.0.0.1:<port>` が accent 色で出ていなければ以降はすべて不成立。
2. **セッションが生きているか。** 同じパネルのセッション行を見る。`終了(終了コード N)` なら CLI が落ちている。理由はタイトルバーの「診断」を開いた先の端末に残る。CLI が確認プロンプトで止まっている場合は、端末へ直接答える。
2. **セッションが生きているか。** 同じパネルのセッション行を見る。`終了(終了コード N)` なら CLI が落ちている。この行は端末の列で選択されているアカウントのものであるため、複数動かしている場合はまず対象の行を選ぶ。理由はタイトルバーの「診断」を開いた先の端末に残る。CLI が確認プロンプトで止まっている場合は、端末へ直接答える。
3. **サイドカーが接続したか。** パネルの名簿で、そのアカウントの行から「未起動」が消える。名簿にはアカウントが起動していなくても並ぶため、名前が出ていること自体は接続の根拠にならない。自分(人間)も発言前から載っている。接続していない場合は端末に `room socket: connected as "<name>"` があるかを見る。
4. **サイドカー単体が壊れていないか。** `npm run sidecar:test` を実行する。偽の部屋ソケットを立てて両面を駆動するため、通ればサイドカーではなくアプリ側の問題に絞れる。
5. **`.mcp.json` の登録が効いているか。** セッションの作業ディレクトリの `.mcp.json` に `pullcept-room-<slug>-<hash>` が入っているかを見る。鍵はアカウント id から決まるため名前では引けない。どのアカウントのものかはエントリ内の `PULLCEPT_AGENT_NAME` で読む。固定の `pullcept-room` は前回起動の残りとして掃除される側である。`--mcp-config` によるファイル渡しでは channel 側が名前を解決できないため、正式登録以外の経路は成立しない。
Expand All @@ -424,7 +450,7 @@ CI が実行するもの:

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

- **次の一手** — 現在の実装に参加者パネルと宛先を足した、実装可能な範囲。両方とも実装済みである。この 1 枚と実装の差は、操作ボタン(端末 / リサイズ / 終了)、宛先の見た目(削除可能なチップ)、#40 でランチャとタイトルバーに入った名乗り(名前欄と色の選択)、そして #53 のアカウント(作成・編集・削除、名簿のオフライン表示)である。宛先は `<select>` で機能的に等価であり、形の差は追っていない。名乗りとアカウントの側はキャンバスが実装より古く、差は #46 で追う。
- **次の一手** — 現在の実装に参加者パネルと宛先を足した、実装可能な範囲。両方とも実装済みである。この 1 枚と実装の差は、操作ボタンのうち未実装のリサイズ(端末と終了は実装済み。ただしキャンバスは 3 つを一組の行として描き、実装は端末の列の各行に置いている)、宛先の見た目(削除可能なチップ)、#40 でランチャとタイトルバーに入った名乗り(名前欄と色の選択)、そして #53 のアカウント(作成・編集・削除、名簿のオフライン表示)である。宛先は `<select>` で機能的に等価であり、形の差は追っていない。名乗りとアカウントの側はキャンバスが実装より古く、差は #46 で追う。
- **全体像** — ルーム一覧 / ダイレクト / 参加者・セッション状態パネル

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

- 会話面はメッセージ単位を保つ。端末は診断面側に置き、会話面には置かない。
- 参加者パネルを持つ(実装済み。上記「参加者パネル」)。セッションの接続方法・起動コマンド・作業ディレクトリ・開始時刻を出す。操作(端末 / ミュート / 終了)は未実装であり、キャンバス側にのみある。
- 参加者パネルを持つ(実装済み。上記「参加者パネル」)。セッションの接続方法・起動コマンド・作業ディレクトリ・開始時刻を出す。操作のうち端末と終了は実装済みで、アカウントごとの行に載っている。ミュートは未実装であり、キャンバス側にのみある。
- 発言に宛先を持たせる(実装済み。入力欄の左で参加者から選ぶ。既定は全体宛)。
- 参加者ごとの色(実装済み。accent と同じ明度・彩度で色相のみ)。色相は宣言でき、宣言が導出に優先する(上記「参加者パネル」の梯子)。
- 名前と色は「この部屋での自分は誰か」の宣言である(実装済み。人間はタイトルバーで参加の時点に、セッションはアカウントの属性として)。
Expand Down
27 changes: 27 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@
than watched — the socket, the session, how it was launched —
moved to the participant panel, where they are on screen without
opening anything. -->
<!-- One frame, holding a terminal per running account. All but the
selected one are hidden rather than closed, so a session keeps
running and keeps filling its own scrollback while another is
being watched (#57). -->
<aside id="diagnostics" hidden>
<div id="terminal" aria-label="セッションの端末"></div>
</aside>
Expand Down Expand Up @@ -126,6 +130,29 @@ <h2 class="panel-title">参加者</h2>
<li class="empty">参加者なし</li>
</ul>

<!-- The terminals this screen is holding, one row per account. Not
the roster above and not a second copy of it: that list answers
who is in the room and is keyed on each participant's
connection, while this one answers which sessions have a pane
here and is keyed on the account id. The memberships differ — a
participant who joined from elsewhere has no terminal here, and a
session that has just exited has a terminal and no seat.

Keying on the account id is what lets a row carry an operation:
the pane, the input and the 終了 all act on one account, and a
name match would tie the wrong one as soon as two accounts
answer to one name (#53). Hidden entirely until something has
been launched — an account with no session has no terminal to
show or end (#57). -->
<section id="terminals-section" class="terminals-section" hidden>
<h2 class="panel-title">端末</h2>
<ul id="terminals" class="terminals"></ul>
</section>

<!-- What the terminal on the glass was launched from. The values
follow the row that is selected above, so they answer for the
pane being looked at rather than for whichever session started
last. 部屋ソケット is the exception and is the room's own. -->
<dl class="facts">
<dt>部屋ソケット</dt>
<dd id="socket-state">確認中</dd>
Expand Down
Loading