From 456b68cb8f72810128e782200d9cb8ae9210c988 Mon Sep 17 00:00:00 2001 From: kjgbot Date: Sat, 31 Oct 2026 07:20:01 -0700 Subject: [PATCH 1/5] fix(probe): let T3 Code's Claude chats join Relay T3 Code runs Claude as a stream-json process with no terminal and an sdk-ts entrypoint, so the core took its chats for automation. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + crates/probe/src/relay/claude/app.rs | 132 +++++++++++++++++++++++++-- 2 files changed, 125 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index adcd61f62f..bede8b4407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- T3 Code chats running Claude now join Relay by themselves. The core took them for automation, so they never appeared in `relay agent list`. - Grok CLI sessions now join Relay after `relay init`. The core refused every open Grok session with "A signed-in owner is required for managed sessions." because release builds never attach a Cloud install; a Grok session now belongs to the signed-in account, or to this computer's OS user when nobody is signed in. - Broker: `spawn:` capabilities now declare `metadata.configured.node_kind: "broker"`, so Cloud keeps placing spawns on unclaimed CLI brokers instead of refusing them until `agent-relay node claim`. An already-declared kind (e.g. `desktop`) is preserved. - `relay init` downloads the standalone Relaycast server (`relaycast-engine`) for your platform into the install directory's `bin` when it is missing, verifying its SHA-256, so the local server now starts from an npm install on any Node.js version and with npm install scripts disabled. It falls back to the `@relaycast/engine` npm package only if the download fails. diff --git a/crates/probe/src/relay/claude/app.rs b/crates/probe/src/relay/claude/app.rs index 519862d9d9..26e6f0c64d 100644 --- a/crates/probe/src/relay/claude/app.rs +++ b/crates/probe/src/relay/claude/app.rs @@ -1,5 +1,5 @@ -//! Claude chats an app hosts as a structured run: Paseo's top-level chats -//! and the chats in Claude's desktop app. The app runs each Code chat as a local +//! Claude chats an app hosts as a structured run: Paseo's top-level chats, +//! T3 Code's chats and the chats in Claude's desktop app. The app runs each Code chat as a local //! Claude Code process that publishes the same session record, inbox and //! socket as a terminal session, so delivery reaches it the same way. The //! app drives that process as a structured `stream-json` run with no @@ -22,6 +22,15 @@ pub fn is_app(executable: &Path) -> bool { && path.ends_with("/Claude.app/Contents/MacOS/Claude") } +/// T3 Code's main executable, installed for all users or for this one. +pub fn is_t3_app(executable: &Path) -> bool { + let path = executable.to_string_lossy(); + (path.starts_with("/Applications/") + || (path.starts_with("/Users/") && path.contains("/Applications/"))) + && (path.ends_with("/T3 Code (Alpha).app/Contents/MacOS/T3 Code (Alpha)") + || path.ends_with("/T3 Code.app/Contents/MacOS/T3 Code")) +} + /// Whether `directory` is a folder the app made for a chat with no project /// (`…/Claude/scratch-workspaces/…`), whose generated name says nothing /// about the chat. @@ -39,9 +48,29 @@ pub fn is_app_chat( session: &TerminalSession, sessions: &[TerminalSession], ) -> bool { - if session.entrypoint.as_deref() != Some(ENTRYPOINT) - || session.kind.as_deref() != Some("interactive") - { + session.entrypoint.as_deref() == Some(ENTRYPOINT) && hosted_by(table, session, sessions, is_app) +} + +/// Whether a live registry entry is a chat a person runs in T3 Code, which +/// records its chats as `sdk-ts` like any SDK run, so only the process tree +/// tells them apart. A Claude session a chat starts stays automation. +pub fn is_t3_chat( + table: &dyn ProcessTable, + session: &TerminalSession, + sessions: &[TerminalSession], +) -> bool { + hosted_by(table, session, sessions, is_t3_app) +} + +/// Whether an interactive registry entry was started by this user's `host` +/// executable, with no other Claude Code session between them. +fn hosted_by( + table: &dyn ProcessTable, + session: &TerminalSession, + sessions: &[TerminalSession], + host: fn(&Path) -> bool, +) -> bool { + if session.kind.as_deref() != Some("interactive") { return false; } let Some(uid) = table.process(session.pid).map(|process| process.uid) else { @@ -53,7 +82,7 @@ pub fn is_app_chat( return false; }; if current != session.pid { - if table.executable(current).is_some_and(|exe| is_app(&exe)) { + if table.executable(current).is_some_and(|exe| host(&exe)) { return true; } if sessions.iter().any(|other| other.pid == current) { @@ -86,18 +115,22 @@ pub fn is_paseo_chat( } /// Whether a live registry entry is a person's chat that an app hosts as a -/// structured run: a top-level Paseo chat or a Claude desktop app chat. +/// structured run: a top-level Paseo chat, a T3 Code chat or a Claude desktop +/// app chat. pub fn is_app_hosted_chat( table: &dyn ProcessTable, session: &TerminalSession, sessions: &[TerminalSession], ) -> bool { - is_paseo_chat(table, session, sessions) || is_app_chat(table, session, sessions) + is_paseo_chat(table, session, sessions) + || is_t3_chat(table, session, sessions) + || is_app_chat(table, session, sessions) } #[cfg(test)] mod tests { use super::*; + use crate::relay::process::{FakeProcess, FakeProcesses}; #[test] fn only_the_claude_app_bundle_is_the_app() { @@ -116,6 +149,89 @@ mod tests { } } + #[test] + fn only_the_t3_code_bundles_are_t3_code() { + for app in [ + "/Applications/T3 Code (Alpha).app/Contents/MacOS/T3 Code (Alpha)", + "/Applications/T3 Code.app/Contents/MacOS/T3 Code", + "/Users/me/Applications/T3 Code (Alpha).app/Contents/MacOS/T3 Code (Alpha)", + ] { + assert!(is_t3_app(Path::new(app)), "{app}"); + } + for other in [ + "/Applications/T3 Code (Alpha).app/Contents/Resources/resource-monitor/t3-resource-monitor", + "/tmp/T3 Code.app/Contents/MacOS/T3 Code", + "/Applications/Claude.app/Contents/MacOS/Claude", + ] { + assert!(!is_t3_app(Path::new(other)), "{other}"); + } + } + + const T3: &str = "/Applications/T3 Code (Alpha).app/Contents/MacOS/T3 Code (Alpha)"; + + fn process(parent: i32, executable: Option<&str>) -> FakeProcess { + FakeProcess { + parent, + uid: 501, + executable: executable.map(Into::into), + ..FakeProcess::default() + } + } + + fn session(pid: i32, kind: Option<&str>) -> TerminalSession { + TerminalSession { + session_id: "7f2a4cab-5d9e-4e3a-9b8c-3c4d5e6f7a01".into(), + pid, + socket_path: "/s".into(), + peer_protocol: 1, + proc_start: None, + cwd: None, + name: None, + status: None, + entrypoint: Some("sdk-ts".into()), + kind: kind.map(Into::into), + updated_at_ms: None, + registry: None, + } + } + + #[test] + fn a_t3_code_chat_is_a_persons_chat_but_what_it_starts_is_not() { + let table = FakeProcesses::default(); + table.insert(10, process(1, Some(T3))); + table.insert(11, process(10, Some(T3))); + table.insert(12, process(11, Some("/usr/local/bin/claude"))); + table.insert(13, process(12, Some("/bin/zsh"))); + table.insert(14, process(13, Some("/usr/local/bin/claude"))); + + let chat = session(12, Some("interactive")); + assert!(is_t3_chat(&table, &chat, std::slice::from_ref(&chat))); + assert!(is_app_hosted_chat( + &table, + &chat, + std::slice::from_ref(&chat) + )); + + let nested = session(14, Some("interactive")); + assert!(!is_t3_chat( + &table, + &nested, + &[chat.clone(), nested.clone()] + )); + + let run = session(12, Some("sdk")); + assert!(!is_t3_chat(&table, &run, std::slice::from_ref(&run))); + } + + #[test] + fn a_sdk_run_outside_t3_code_stays_automation() { + let table = FakeProcesses::default(); + table.insert(20, process(1, Some("/usr/local/bin/node"))); + table.insert(21, process(20, Some("/usr/local/bin/claude"))); + let run = session(21, Some("interactive")); + assert!(!is_t3_chat(&table, &run, std::slice::from_ref(&run))); + } + #[test] fn scratch_workspaces_are_the_apps_own_folders() { assert!(is_scratch_workspace( From 0c013dc77156b23f05ec111eaee24611923d381f Mon Sep 17 00:00:00 2001 From: kjgbot Date: Sat, 31 Oct 2026 07:29:26 -0700 Subject: [PATCH 2/5] fix(probe): stop the hosted chat walk at an unreadable Claude session A nested run whose parent's registry file cannot be read walked through that parent to the app and auto-joined. Activation now passes the whole registry, so a known unreadable pid stops the walk too. Co-Authored-By: Claude Opus 5.5 --- crates/probe/src/relay/claude/app.rs | 64 +++++++++++-------- .../probe/src/relay/local/directory_trait.rs | 6 +- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/crates/probe/src/relay/claude/app.rs b/crates/probe/src/relay/claude/app.rs index 26e6f0c64d..b51a45c029 100644 --- a/crates/probe/src/relay/claude/app.rs +++ b/crates/probe/src/relay/claude/app.rs @@ -8,7 +8,7 @@ use std::path::Path; -use super::TerminalSession; +use super::{Registry, TerminalSession}; use crate::relay::process::{ProcessTable, MAX_ANCESTRY}; /// The `entrypoint` the app records for its chats. @@ -46,9 +46,9 @@ pub fn is_scratch_workspace(directory: &str) -> bool { pub fn is_app_chat( table: &dyn ProcessTable, session: &TerminalSession, - sessions: &[TerminalSession], + registry: &Registry, ) -> bool { - session.entrypoint.as_deref() == Some(ENTRYPOINT) && hosted_by(table, session, sessions, is_app) + session.entrypoint.as_deref() == Some(ENTRYPOINT) && hosted_by(table, session, registry, is_app) } /// Whether a live registry entry is a chat a person runs in T3 Code, which @@ -57,17 +57,18 @@ pub fn is_app_chat( pub fn is_t3_chat( table: &dyn ProcessTable, session: &TerminalSession, - sessions: &[TerminalSession], + registry: &Registry, ) -> bool { - hosted_by(table, session, sessions, is_t3_app) + hosted_by(table, session, registry, is_t3_app) } /// Whether an interactive registry entry was started by this user's `host` -/// executable, with no other Claude Code session between them. +/// executable, with no other Claude Code session between them, not even one +/// whose registry file cannot be read. fn hosted_by( table: &dyn ProcessTable, session: &TerminalSession, - sessions: &[TerminalSession], + registry: &Registry, host: fn(&Path) -> bool, ) -> bool { if session.kind.as_deref() != Some("interactive") { @@ -85,7 +86,9 @@ fn hosted_by( if table.executable(current).is_some_and(|exe| host(&exe)) { return true; } - if sessions.iter().any(|other| other.pid == current) { + if registry.sessions.iter().any(|other| other.pid == current) + || registry.unreadable.iter().any(|file| file.pid == current) + { return false; } } @@ -120,17 +123,18 @@ pub fn is_paseo_chat( pub fn is_app_hosted_chat( table: &dyn ProcessTable, session: &TerminalSession, - sessions: &[TerminalSession], + registry: &Registry, ) -> bool { - is_paseo_chat(table, session, sessions) - || is_t3_chat(table, session, sessions) - || is_app_chat(table, session, sessions) + is_paseo_chat(table, session, ®istry.sessions) + || is_t3_chat(table, session, registry) + || is_app_chat(table, session, registry) } #[cfg(test)] mod tests { use super::*; use crate::relay::process::{FakeProcess, FakeProcesses}; + use crate::relay::sessions::UnreadableSessionFile; #[test] fn only_the_claude_app_bundle_is_the_app() { @@ -195,6 +199,13 @@ mod tests { } } + fn registry(sessions: &[&TerminalSession]) -> Registry { + Registry { + sessions: sessions.iter().map(|&session| session.clone()).collect(), + unreadable: Vec::new(), + } + } + #[test] fn a_t3_code_chat_is_a_persons_chat_but_what_it_starts_is_not() { let table = FakeProcesses::default(); @@ -205,22 +216,25 @@ mod tests { table.insert(14, process(13, Some("/usr/local/bin/claude"))); let chat = session(12, Some("interactive")); - assert!(is_t3_chat(&table, &chat, std::slice::from_ref(&chat))); - assert!(is_app_hosted_chat( - &table, - &chat, - std::slice::from_ref(&chat) - )); + assert!(is_t3_chat(&table, &chat, ®istry(&[&chat]))); + assert!(is_app_hosted_chat(&table, &chat, ®istry(&[&chat]))); let nested = session(14, Some("interactive")); - assert!(!is_t3_chat( - &table, - &nested, - &[chat.clone(), nested.clone()] - )); + assert!(!is_t3_chat(&table, &nested, ®istry(&[&chat, &nested]))); + + // The parent chat's record cannot be read: its pid still stops the walk. + let unreadable = Registry { + sessions: vec![nested.clone()], + unreadable: vec![UnreadableSessionFile { + path: "/sessions/12.json".into(), + pid: 12, + error: "expected value".into(), + }], + }; + assert!(!is_t3_chat(&table, &nested, &unreadable)); let run = session(12, Some("sdk")); - assert!(!is_t3_chat(&table, &run, std::slice::from_ref(&run))); + assert!(!is_t3_chat(&table, &run, ®istry(&[&run]))); } #[test] @@ -229,7 +243,7 @@ mod tests { table.insert(20, process(1, Some("/usr/local/bin/node"))); table.insert(21, process(20, Some("/usr/local/bin/claude"))); let run = session(21, Some("interactive")); - assert!(!is_t3_chat(&table, &run, std::slice::from_ref(&run))); + assert!(!is_t3_chat(&table, &run, ®istry(&[&run]))); } #[test] diff --git a/crates/probe/src/relay/local/directory_trait.rs b/crates/probe/src/relay/local/directory_trait.rs index 9e0527bd19..a82c2376cf 100644 --- a/crates/probe/src/relay/local/directory_trait.rs +++ b/crates/probe/src/relay/local/directory_trait.rs @@ -349,7 +349,7 @@ impl LocalSessions { /// `auto_activates`, so the SessionStart hook never promises a join /// activation would not make. fn activation_check(&self) -> impl Fn(&DiscoveredSession) -> bool + '_ { - let claude_sessions = claude::registry(&self.config.claude, &*self.table).sessions; + let claude_registry = claude::registry(&self.config.claude, &*self.table); let grok_reachable = move |session_id: &str| self.grok_open_reachable(session_id); let automated = std::cell::OnceCell::new(); let started_by_automation = move |session_id: &str| { @@ -396,12 +396,12 @@ impl LocalSessions { &*self.table, self.config.uid, &terminal, - &claude_sessions, + &claude_registry.sessions, ) || claude::is_app_hosted_chat( &*self.table, &terminal, - &claude_sessions, + &claude_registry, ) }) } From 95f576ee928db3331a1eddbbea91dcfd86df8606 Mon Sep 17 00:00:00 2001 From: kjgbot Date: Sat, 31 Oct 2026 07:31:31 -0700 Subject: [PATCH 3/5] fix(probe): keep claude -p in T3 Code's terminal out of T3 chat detection Co-Authored-By: Claude Opus 5.5 --- crates/probe/src/relay/claude/app.rs | 81 ++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 5 deletions(-) diff --git a/crates/probe/src/relay/claude/app.rs b/crates/probe/src/relay/claude/app.rs index b51a45c029..4b47ad5e4a 100644 --- a/crates/probe/src/relay/claude/app.rs +++ b/crates/probe/src/relay/claude/app.rs @@ -53,13 +53,29 @@ pub fn is_app_chat( /// Whether a live registry entry is a chat a person runs in T3 Code, which /// records its chats as `sdk-ts` like any SDK run, so only the process tree -/// tells them apart. A Claude session a chat starts stays automation. +/// tells them apart. A Claude session a chat starts stays automation, and so +/// does `claude -p` in T3 Code's terminal, which can also record `interactive`. pub fn is_t3_chat( table: &dyn ProcessTable, session: &TerminalSession, registry: &Registry, ) -> bool { - hosted_by(table, session, registry, is_t3_app) + is_structured_chat_process(table, session.pid) && hosted_by(table, session, registry, is_t3_app) +} + +/// Whether `pid` runs the way an app drives a chat: a `stream-json` pipe with +/// no terminal and no `-p`/`--print`. +fn is_structured_chat_process(table: &dyn ProcessTable, pid: i32) -> bool { + let arguments = table.arguments(pid); + let printing = arguments + .iter() + .any(|arg| arg == "-p" || arg == "--print" || arg.starts_with("--print=")); + let structured = arguments.iter().enumerate().any(|(index, argument)| { + argument == "--input-format=stream-json" + || (argument == "--input-format" + && arguments.get(index + 1).map(String::as_str) == Some("stream-json")) + }); + structured && !printing && !table.has_terminal(pid) } /// Whether an interactive registry entry was started by this user's `host` @@ -182,6 +198,19 @@ mod tests { } } + fn chat_process(parent: i32) -> FakeProcess { + FakeProcess { + arguments: vec![ + "claude".into(), + "--output-format".into(), + "stream-json".into(), + "--input-format".into(), + "stream-json".into(), + ], + ..process(parent, Some("/usr/local/bin/claude")) + } + } + fn session(pid: i32, kind: Option<&str>) -> TerminalSession { TerminalSession { session_id: "7f2a4cab-5d9e-4e3a-9b8c-3c4d5e6f7a01".into(), @@ -211,9 +240,9 @@ mod tests { let table = FakeProcesses::default(); table.insert(10, process(1, Some(T3))); table.insert(11, process(10, Some(T3))); - table.insert(12, process(11, Some("/usr/local/bin/claude"))); + table.insert(12, chat_process(11)); table.insert(13, process(12, Some("/bin/zsh"))); - table.insert(14, process(13, Some("/usr/local/bin/claude"))); + table.insert(14, chat_process(13)); let chat = session(12, Some("interactive")); assert!(is_t3_chat(&table, &chat, ®istry(&[&chat]))); @@ -237,11 +266,53 @@ mod tests { assert!(!is_t3_chat(&table, &run, ®istry(&[&run]))); } + #[test] + fn claude_print_in_t3_codes_terminal_stays_automation() { + let table = FakeProcesses::default(); + table.insert(30, process(1, Some(T3))); + table.insert(31, process(30, Some("/bin/zsh"))); + table.insert( + 32, + FakeProcess { + arguments: vec!["claude".into(), "-p".into(), "hello".into()], + has_terminal: true, + ..process(31, Some("/usr/local/bin/claude")) + }, + ); + let run = session(32, Some("interactive")); + assert!(!is_t3_chat(&table, &run, ®istry(&[&run]))); + + // A structured pipe in that terminal is a script, not the app's chat. + table.insert( + 33, + FakeProcess { + has_terminal: true, + ..chat_process(31) + }, + ); + let piped = session(33, Some("interactive")); + assert!(!is_t3_chat(&table, &piped, ®istry(&[&piped]))); + + table.insert( + 34, + FakeProcess { + arguments: vec![ + "claude".into(), + "-p".into(), + "--input-format=stream-json".into(), + ], + ..process(30, Some("/usr/local/bin/claude")) + }, + ); + let printing = session(34, Some("interactive")); + assert!(!is_t3_chat(&table, &printing, ®istry(&[&printing]))); + } + #[test] fn a_sdk_run_outside_t3_code_stays_automation() { let table = FakeProcesses::default(); table.insert(20, process(1, Some("/usr/local/bin/node"))); - table.insert(21, process(20, Some("/usr/local/bin/claude"))); + table.insert(21, chat_process(20)); let run = session(21, Some("interactive")); assert!(!is_t3_chat(&table, &run, ®istry(&[&run]))); } From 5cd24c9d0c0ddd7cfd2ce97801bac41e88ae24f2 Mon Sep 17 00:00:00 2001 From: kjgbot Date: Sat, 31 Oct 2026 07:43:07 -0700 Subject: [PATCH 4/5] fix(probe): recognize T3 Code's Beta and Nightly bundles Co-Authored-By: Claude Opus 5.5 --- crates/probe/src/relay/claude/app.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/probe/src/relay/claude/app.rs b/crates/probe/src/relay/claude/app.rs index 4b47ad5e4a..d92edab57e 100644 --- a/crates/probe/src/relay/claude/app.rs +++ b/crates/probe/src/relay/claude/app.rs @@ -27,8 +27,14 @@ pub fn is_t3_app(executable: &Path) -> bool { let path = executable.to_string_lossy(); (path.starts_with("/Applications/") || (path.starts_with("/Users/") && path.contains("/Applications/"))) - && (path.ends_with("/T3 Code (Alpha).app/Contents/MacOS/T3 Code (Alpha)") - || path.ends_with("/T3 Code.app/Contents/MacOS/T3 Code")) + && [ + "/T3 Code.app/Contents/MacOS/T3 Code", + "/T3 Code (Alpha).app/Contents/MacOS/T3 Code (Alpha)", + "/T3 Code (Beta).app/Contents/MacOS/T3 Code (Beta)", + "/T3 Code (Nightly).app/Contents/MacOS/T3 Code (Nightly)", + ] + .iter() + .any(|bundle| path.ends_with(bundle)) } /// Whether `directory` is a folder the app made for a chat with no project @@ -174,6 +180,8 @@ mod tests { for app in [ "/Applications/T3 Code (Alpha).app/Contents/MacOS/T3 Code (Alpha)", "/Applications/T3 Code.app/Contents/MacOS/T3 Code", + "/Applications/T3 Code (Beta).app/Contents/MacOS/T3 Code (Beta)", + "/Applications/T3 Code (Nightly).app/Contents/MacOS/T3 Code (Nightly)", "/Users/me/Applications/T3 Code (Alpha).app/Contents/MacOS/T3 Code (Alpha)", ] { assert!(is_t3_app(Path::new(app)), "{app}"); From 04de1ce7e2b3068a12ba5ff41bc12b38ad50bec1 Mon Sep 17 00:00:00 2001 From: kjgbot Date: Sat, 31 Oct 2026 07:44:34 -0700 Subject: [PATCH 5/5] fix(probe): match T3 Code channels by bundle name, not a list Co-Authored-By: Claude Opus 5.5 --- crates/probe/src/relay/claude/app.rs | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/crates/probe/src/relay/claude/app.rs b/crates/probe/src/relay/claude/app.rs index d92edab57e..02d70ce501 100644 --- a/crates/probe/src/relay/claude/app.rs +++ b/crates/probe/src/relay/claude/app.rs @@ -22,19 +22,21 @@ pub fn is_app(executable: &Path) -> bool { && path.ends_with("/Claude.app/Contents/MacOS/Claude") } -/// T3 Code's main executable, installed for all users or for this one. +/// T3 Code's main executable on any release channel (`T3 Code` or +/// `T3 Code ()`), installed for all users or for this one. pub fn is_t3_app(executable: &Path) -> bool { + let Some(name) = executable.file_name().and_then(|name| name.to_str()) else { + return false; + }; + let channel_build = name + .strip_prefix("T3 Code (") + .and_then(|rest| rest.strip_suffix(')')) + .is_some_and(|channel| !channel.is_empty() && channel.chars().all(char::is_alphanumeric)); let path = executable.to_string_lossy(); - (path.starts_with("/Applications/") - || (path.starts_with("/Users/") && path.contains("/Applications/"))) - && [ - "/T3 Code.app/Contents/MacOS/T3 Code", - "/T3 Code (Alpha).app/Contents/MacOS/T3 Code (Alpha)", - "/T3 Code (Beta).app/Contents/MacOS/T3 Code (Beta)", - "/T3 Code (Nightly).app/Contents/MacOS/T3 Code (Nightly)", - ] - .iter() - .any(|bundle| path.ends_with(bundle)) + (name == "T3 Code" || channel_build) + && (path.starts_with("/Applications/") + || (path.starts_with("/Users/") && path.contains("/Applications/"))) + && path.ends_with(&format!("/{name}.app/Contents/MacOS/{name}")) } /// Whether `directory` is a folder the app made for a chat with no project @@ -188,6 +190,9 @@ mod tests { } for other in [ "/Applications/T3 Code (Alpha).app/Contents/Resources/resource-monitor/t3-resource-monitor", + "/Applications/T3 Code (Alpha).app/Contents/Frameworks/T3 Code (Alpha) Helper (Renderer).app/Contents/MacOS/T3 Code (Alpha) Helper (Renderer)", + "/Applications/T3 Code (Beta).app/Contents/MacOS/T3 Code", + "/Applications/T3 Code ().app/Contents/MacOS/T3 Code ()", "/tmp/T3 Code.app/Contents/MacOS/T3 Code", "/Applications/Claude.app/Contents/MacOS/Claude", ] {