From 69a8701dccd32d6e14073b7b5d90e80a51ac426f Mon Sep 17 00:00:00 2001 From: Claude Lin & Lay Date: Fri, 21 Aug 2026 19:31:25 +0900 Subject: [PATCH] feat(launcher): make the launch options editable and merge the room channel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 起動オプションを画面から編集できるようにし、アプリが付ける channel エントリを 利用者の指定へ統合するようにした。 従来は args を編集する UI が無く、channel の対象も SERVER_NAME 固定だった。 Master の希望する起動形(--dangerously-skip-permissions と、別の channel サーバの指定)を表現する手段が存在しなかった。 統合の形にしたのは、第二のフラグとして足すと同一フラグの二重指定になるため。 liplus-desktop #90 の実測では --channels 併記による二重登録で全体が不通になって おり、同じ形を自分で作ることになる。利用者が channel フラグを書いていればその 直後へ挿入し、書いていなければフラグごと足す。 上書きではなく統合なのは、部屋のエントリを利用者の指定で落とせないようにする ため。落ちれば部屋の入力路そのものが消える。 実際に起動する行を画面に表示する。書いた行と走る行が違う以上、結果を見せるほうが 説明より安い。 引数の分割は mcp-config 側に置いてテストで固定した。TypeScript 側に第二の実装を 書くとテスト済みの側からドリフトするため、フロントエンドはコマンド経由で呼ぶ。 二重引用符によるグループ化のみ解釈する。空白分割だけだと "C:\Program Files\..." のような指定が黙って二つに割れる。バックスラッシュはエスケープではなくそのまま。 未検証として docs に明記: 一つのフラグに server: エントリを複数並べられるかは 確認できていない。#90 が確定しているのは --channels 併記の失敗であり、同一 フラグへの複数エントリは扱っていない。--print での確認は channel の読み込み 自体が走らないため無意味。対話セッションでのみ確かめられる。 #26 --- README.md | 4 +- crates/mcp-config/src/lib.rs | 124 ++++++++++++++++++++++++++++++++++- docs/0-requirements.md | 12 ++++ index.html | 13 ++++ src-tauri/src/lib.rs | 2 + src-tauri/src/session.rs | 19 ++++++ src/main.ts | 42 ++++++++++-- src/styles.css | 40 +++++++++++ 8 files changed, 249 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 41e2c80..c7dadcd 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,9 @@ npm run tauri dev 起動すると Tauri の窓が開き、部屋ソケットが待ち受けを始めます。フロントエンドの dev サーバは `vite.config.ts` で 1420 番に固定しています(`src-tauri/tauri.conf.json` の `devUrl` と一致させる必要があるため)。 -タブの下に作業ディレクトリの入力欄があります。初回はホームディレクトリが入っているので、セッションを動かしたいディレクトリへ変更してください。この値はタブ設定として保存されます。 +タブの下に「起動オプション」の入力欄があります。`--dangerously-skip-permissions` のように、CLI へ渡したいオプションをそのまま書けます。アプリは部屋の channel エントリ(`server:liplus-chat-room`)をここへ統合するので、別の channel サーバを指定しても部屋の入力路は残ります。実際に起動する行は入力欄の右に表示されます。 + +作業ディレクトリの入力欄があります。初回はホームディレクトリが入っているので、セッションを動かしたいディレクトリへ変更してください。この値はタブ設定として保存されます。 「診断」を開くと、部屋ソケットの待受状態、セッションの生死、そして**起動した CLI の端末**が確認できます。端末はそのまま操作できます。CLI はフォルダごとに初回の信頼確認を出すため、最初の一回はここから答えてください。 diff --git a/crates/mcp-config/src/lib.rs b/crates/mcp-config/src/lib.rs index 209c515..ca42612 100644 --- a/crates/mcp-config/src/lib.rs +++ b/crates/mcp-config/src/lib.rs @@ -56,11 +56,70 @@ pub fn reject_incompatible_flags(args: &[String]) -> Result<(), &'static str> { Ok(()) } +/// The flag that loads channel servers into a session. +pub const CHANNEL_FLAG: &str = "--dangerously-load-development-channels"; + /// The launch arguments for a channel-enabled session, given the tab's own. +/// +/// The room's entry is merged into whatever the person wrote rather than added +/// as a second flag: `--channels` alongside this one registers a server twice +/// and takes the whole room down, and two copies of this flag is the same +/// shape. Merging also means the room's input path cannot be dropped by +/// configuring a different server — losing it is losing the room. pub fn channel_launch_args(base: &[String]) -> Vec { + let room = format!("server:{SERVER_NAME}"); let mut args = base.to_vec(); - args.push("--dangerously-load-development-channels".to_string()); - args.push(format!("server:{SERVER_NAME}")); + + if args.iter().any(|arg| *arg == room) { + return args; + } + + match args.iter().position(|arg| arg == CHANNEL_FLAG) { + // Right after the flag: the values are positional, and keeping them + // contiguous means a later argument cannot be captured as one. + Some(index) => args.insert(index + 1, room), + None => { + args.push(CHANNEL_FLAG.to_string()); + args.push(room); + } + } + args +} + +/// Split a launch-options string the way a shell would, minus the parts a +/// shell does that have no place here. +/// +/// Double quotes group, because Windows paths have spaces in them and a bare +/// whitespace split turns one such argument into two without saying so. +/// Nothing else is interpreted: no variable expansion, no globbing, no escape +/// characters — a backslash in a Windows path is a backslash. +pub fn split_launch_options(text: &str) -> Vec { + let mut args = Vec::new(); + let mut current = String::new(); + let mut quoted = false; + let mut has_token = false; + + for ch in text.chars() { + match ch { + '"' => { + quoted = !quoted; + has_token = true; + } + c if c.is_whitespace() && !quoted => { + if has_token { + args.push(std::mem::take(&mut current)); + has_token = false; + } + } + c => { + current.push(c); + has_token = true; + } + } + } + if has_token { + args.push(current); + } args } @@ -292,6 +351,67 @@ mod tests { assert_eq!(reject_incompatible_flags(&args), Ok(())); } + #[test] + fn merges_the_room_into_a_channel_flag_the_person_already_wrote() { + // Master's own launch line, which names a different channel server. + // A second copy of the flag is the `--channels` failure in another + // shape, and dropping the room entry loses the room's input path. + let base: Vec = [ + "--dangerously-skip-permissions", + CHANNEL_FLAG, + "server:github-webhook-mcp", + ] + .iter() + .map(|s| s.to_string()) + .collect(); + + let merged = channel_launch_args(&base); + assert_eq!( + merged, + vec![ + "--dangerously-skip-permissions".to_string(), + CHANNEL_FLAG.to_string(), + format!("server:{SERVER_NAME}"), + "server:github-webhook-mcp".to_string(), + ] + ); + assert_eq!( + merged.iter().filter(|arg| *arg == CHANNEL_FLAG).count(), + 1, + "the flag must not appear twice" + ); + } + + #[test] + fn does_not_add_the_room_twice() { + let base = vec![CHANNEL_FLAG.to_string(), format!("server:{SERVER_NAME}")]; + assert_eq!(channel_launch_args(&base), base); + } + + #[test] + fn splits_launch_options_keeping_quoted_arguments_whole() { + assert_eq!( + split_launch_options(" --a --b=1 "), + vec!["--a".to_string(), "--b=1".to_string()] + ); + // A Windows path with spaces is one argument, and its backslashes are + // literal rather than escapes. + assert_eq!( + split_launch_options(r#"--add-dir "C:\Program Files\x" --flag"#), + vec![ + "--add-dir".to_string(), + r"C:\Program Files\x".to_string(), + "--flag".to_string(), + ] + ); + assert!(split_launch_options(" ").is_empty()); + // An empty quoted argument is an argument, not nothing. + assert_eq!( + split_launch_options("--x \"\""), + vec!["--x".to_string(), String::new()] + ); + } + #[test] fn the_launch_flag_names_the_server_the_config_registers() { // The flag and the `.mcp.json` key are one fact in two places; a drift diff --git a/docs/0-requirements.md b/docs/0-requirements.md index 574e9f1..8de351d 100644 --- a/docs/0-requirements.md +++ b/docs/0-requirements.md @@ -189,6 +189,18 @@ liplus-desktop の `stream_parser.rs` および `spawn_stream_pty` / `spawn_stre 信頼確認や権限確認をアプリが自動で答えることはしない。それらはセキュリティ上の確認であり、判断は人間が行う。アプリが負うのは、人間が答えられる経路を用意することであって、代わりに答えることではない。 +### 起動オプション + +セッションの起動オプションは画面から編集でき、タブ設定として保存する。 + +アプリ自身の channel エントリ(`server:liplus-chat-room`)は、利用者が書いたものへ**統合**する。利用者が `--dangerously-load-development-channels` を書いていればその直後へ挿入し、書いていなければフラグごと足す。第二のフラグとしては足さない。同一フラグの二重指定は `--channels` 併記と同じ形であり、成立条件が壊れる。 + +統合であって上書きではない。利用者が別の channel サーバを指定しても、部屋のエントリは落ちない。落ちれば部屋の入力路そのものが消える。 + +実際に起動する行は画面に表示する。書いた行と走る行が違う以上、結果を見せるほうが説明より安い。 + +**未検証**: 一つのフラグに `server:` エントリを複数並べられるかは確認できていない。#90 の実測が確定しているのは `--channels` 併記による二重登録の失敗であり、同一フラグへの複数エントリは扱っていない。`--print` での確認は無意味である(channel の読み込み自体が走らないため)。対話セッションでのみ確かめられる。 + ### 作業ディレクトリ セッションの作業ディレクトリは画面に表示し、変更でき、タブ設定として保存する。未設定のままアプリのプロセス cwd へ暗黙にフォールバックしない。 diff --git a/index.html b/index.html index 1130c5d..f9a0777 100644 --- a/index.html +++ b/index.html @@ -28,6 +28,19 @@ +
+ + +
+