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
24 changes: 24 additions & 0 deletions docs/0-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ liplus-desktop の `stream_parser.rs` および `spawn_stream_pty` / `spawn_stre
- 画面の再読み込みに耐えるセッション(席の台帳が起動の記録を持ち、画面が席を見て端末を購読し直す。スクロールバックは戻らず、繋ぎ直した端末は切れた位置を 1 行書いてから開く)
- 行のライフサイクル操作の記号表示(開始 ▶️ / 終了 ❌ / 編集 ⚙️、語は `aria-label` と `title`、幅を固定した列)と、終了の確認ダイアログ(webview 内の `<dialog>`、取消に焦点、Escape は取消)
- アプリの終了時のセッションの後始末(走っているセッションがあれば確認のダイアログ、承認で一括終了してから閉じる、取消で閉じない、走っているものが無ければ確認を出さずに閉じる)
- サイドバーの開閉(ヘッダ左右のボタン、同じ図形を CSS で裏返した鏡像のアイコン、幅 0 で畳む形、`config.json` への保持、`Ctrl+B` は左の面のみ、端末に焦点がある間はキーを端末へ渡す)
- 診断面(アカウントごとの CLI の端末表示と入力)
- 診断面のヘッダ(窓の名前「端末」、開いている端末のタブ、行との連動、面を畳む `✕`、端末の文字サイズ)と、面全体の角丸・一つの地・詰めた枠の余白。終了した端末はタブの `✕` で捨てる
- Windows CI(`npm ci` と Rust コンパイル確認)、Release 公開時の CD
Expand Down Expand Up @@ -448,6 +449,7 @@ liplus-desktop の `stream_parser.rs` および `spawn_stream_pty` / `spawn_stre
- 画面の再読み込みに耐えるところの実機確認。繋ぎ直した端末へ出力と入力が戻ること、繋ぎ直した端末の開始時刻・起動コマンド・作業ディレクトリが起動時の値であることは実装済みで、CI の型検査と Rust のコンパイルは通っているが、実機での操作は未確認である。席を持つ行が `❌` を出すことは 2026-08-24 に確認済みであり、端末の 1 行目の告知は同日に確認して**読めないことが分かった**(上記「画面の再読み込みに耐える」)。
- 走っているアカウントの様子のうち、パネル 16.5rem の中で本人の行の名前が押し出されないことの実機確認(#82、上記「走っているアカウントが何をしているか」)。実装済みで CI の型検査は通っているが、寸法の側は未確認である。「考え中…」「出力中」と沈黙 1 秒での消失は 2026-08-24 に確認済みである。Claude Code が確認プロンプトで待っている間に出力を止めるかどうかは依然として未計測である——止めないのであれば、待っている行に「出力中」が立ち続ける。「出力中」がその場合でも literal に正しい語であることがこの語を選んだ理由だが、それは待機と稼働を見分けられるという意味ではない。
- 起動から部屋に入るまでの行の語(#89)の窓の計測。**不具合そのものは修正済みである**(上記「参加者パネル」)。実機で見つかった窓であり(2026-08-24、AI が操作)、`▶️` を押した後、CLI が `--dangerously-load-development-channels` の確認プロンプトで止まっている間、行の note は「未起動」のままで、同じ行のライフサイクルの列は `❌` を出していた——終了できるものが未起動だと言っている状態である。行の「走っている」の定義が**部屋の名簿に居ること**であってプロセスが在ることではないために起きる窓であり、`row.participant` が null の間は上記「走っているアカウントが何をしているか」の二語も出ない。窓の長さは CLI の起動時間そのもので、確認プロンプトで止まれば数分続く。#82 より前から在る経路であり、#82 は空だった枝を埋めただけである。行の語そのものは 2026-08-25 に入口から出口まで確認済みである(上記「実機で確認済み」)——押す前の「未起動」、押した後の「起動中」と `❌` の並存、名簿に現れた後の消失という三段階を、起動していない行が据え置かれたまま辿っている。残る未計測は二つ。一つは窓の時間で、語が出るまでの時間も窓の長さも取っていない——窓の長さが CLI の起動時間であることは上の導出であって、実機で計った値ではない。もう一つは、一度部屋に入った CLI が接続を失った場合に note が「起動中」へ戻るかである。この経路は通っておらず、実機では見ていない。
- サイドバーの開閉の実機確認。ヘッダ左右のボタン、鏡像のアイコンが Windows でその向きに出ること、畳んだときに帯が残らないこと、再起動後の保持、`Ctrl+B`、端末に焦点があるときにキーが端末へ渡ることはいずれも実装済みで CI の型検査と Rust のコンパイルは通っているが、実機での操作は未確認である。`Ctrl+B` を端末へ譲る側は特に未計測である——譲っていることは「アプリの面が動かない」という不在でしか見えず、シェル側がその一打をどう受けたかまで見て初めて確かめられる。
- クラッシュした場合の後始末(#85 の範囲外)。job object へ入れれば OS が落とすが、確認を挟む形とは別の軸であり、実装していない。正常終了の経路を閉じた後も、この経路からは孤児が生まれうる。
- 孤児の引き取り(#85 の範囲外、#84 関連)。起動時に前回の孤児を見つけて繋ぎ直す形。pty id が Rust のメモリにしか無いため持続化の話になる。
- CLI の出力から状態を読む形(「ツール使用中」の粒度)。#82 は段階 1 として出力を読まない二つの合図だけを採り、この形は実測してから別 issue で判断すると決めた。
Expand Down Expand Up @@ -627,6 +629,28 @@ liplus-desktop の `stream_parser.rs` および `spawn_stream_pty` / `spawn_stre

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

### サイドバーの開閉

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

**操作はヘッダに置く。** 左の面のボタンはヘッダの左端、右の面のボタンは右端であり、どちらも自分が畳む列の上に立つ。左端はアプリ名「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`)の文字サイズを、読む側が変えられる。
Expand Down
54 changes: 53 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,42 @@
<body>
<div id="app">
<header id="titlebar">
<span class="title">Pullcept</span>
<!-- Folding the left panel away. Where the app's own name used to sit:
that word said what this window is to someone already looking at
it, and pressing it did nothing (#118). The control that reaches
the panel below it stands over that panel's column instead, which
is the position it is read from.

Its pair is at the far right of this bar, over the column it
folds, and the two are the same drawing mirrored. Both buttons
carry that drawing; what makes them a mirror rather than a pair of
pictures is one rule in src/styles.css, which turns
`#toggle-participants` over. A second picture for the second panel
would say the two controls do different things (#118, decision 4).

`Ctrl+B` reaches this one and not the one opposite. The key is
the terminal's while the terminal has focus — a session's pane is
the session's (#84) — so the shortcut is not the only way in, and
both buttons stay on the bar whether a panel is open or folded.

`aria-expanded` and a name that does not change with it, the way
`#toggle-diagnostics` below carries the same pair. The state is
the attribute's to say; a label that said it too would be two
answers to one question. -->
<button
id="toggle-history"
class="panel-toggle"
type="button"
aria-expanded="true"
aria-controls="history"
aria-label="トピックの開閉"
title="トピックの開閉(Ctrl+B)"
>
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<rect x="1.7" y="2.7" width="12.6" height="10.6" rx="2.4" />
<line x1="6.2" y1="2.7" x2="6.2" y2="13.3" />
</svg>
</button>
<!-- The roster used to be echoed here as one line. It is the
participant panel's list now: two renderings of one roster is one
surface too many, and the panel is always on screen. -->
Expand Down Expand Up @@ -39,6 +74,23 @@
title="会話の文字サイズ(Ctrl + = / - / 0)"
></select>
<button id="toggle-diagnostics" type="button" aria-expanded="false">端末</button>
<!-- Folding the right panel away. Last on the bar, so it sits over the
column it reaches — the same reason its pair is first. See that
one for the shape, the mirror and why this one has no shortcut. -->
<button
id="toggle-participants"
class="panel-toggle"
type="button"
aria-expanded="true"
aria-controls="participants"
aria-label="アカウントの開閉"
title="アカウントの開閉"
>
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<rect x="1.7" y="2.7" width="12.6" height="10.6" rx="2.4" />
<line x1="6.2" y1="2.7" x2="6.2" y2="13.3" />
</svg>
</button>
</header>

<div id="body">
Expand Down
39 changes: 39 additions & 0 deletions src-tauri/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,35 @@ pub struct Account {
pub resume_command: Option<String>,
}

/// Which of the two panels flanking the room are open.
///
/// Here rather than in the webview's own storage, which is where the two text
/// sizes live (#60 / #68). Those are properties of the screen reading the
/// conversation — two people reading one room have no reason to want the same
/// size — and this is a property of the window's layout, which the person who
/// folded a panel away expects to find folded when they open the app again
/// (#118, decision 1).
///
/// Both open, for a screen that has never folded either. That is what every
/// screen showed before this field existed, so a config saved then opens the
/// way it closed.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct PanelState {
/// The topic list, on the left (`#history`).
pub history: bool,
/// The account list, on the right (`#participants`).
pub participants: bool,
}

impl Default for PanelState {
fn default() -> Self {
PanelState {
history: true,
participants: true,
}
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
/// Also read from `tabs`, the name this key had while an account was a
Expand All @@ -130,6 +159,11 @@ pub struct AppConfig {
/// and a hue is simply not declared yet. The next save writes `accounts`.
#[serde(alias = "tabs")]
pub accounts: Vec<Account>,
/// Absent in a config written before this field existed, which is every
/// config saved so far. `Default` fills it with both panels open — the
/// layout those screens have been showing.
#[serde(default)]
pub panels: PanelState,
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -187,6 +221,7 @@ impl Default for AppConfig {
// field they never filled in.
resume_command: None,
}],
panels: PanelState::default(),
}
}
}
Expand Down Expand Up @@ -237,6 +272,10 @@ pub fn load_config(app: AppHandle) -> Result<AppConfig, String> {
// before it existed declares no way of resuming, which is what every
// account did then — there was no topic for a session to be resumed into.
//
// `panels` is the same again: a config saved before it existed says nothing
// about which panels are folded, and both of them were open on every screen
// then (#118).
//
// `character` is the same again, and its absence is the state it means:
// an account saved before it existed declared no character, so its launch
// reads whatever its working directory's own `settings.json` names — which
Expand Down
Loading