···99- Preserves existing tags and other metadata
1010- Emits `session_exec` event with previous and new command
1111- Interactive TUI shows "Create new session..." for spawn-enabled remote hosts
1212+- Interactive filter hides "Create new session..." items when filter doesn't match "new"
1313+- `host/session` filter syntax: type `prod/api` to filter by host then session
1414+- Extracted `buildFilteredGroups` as a pure function for unit testing
12151316## 0.8.0
1417
+1-1
README.md
···4040 ↑↓ select ⏎ attach q quit
4141```
42424343-Arrow keys to navigate, type to filter, Enter to attach, `q` to quit. Creating a new session walks through a directory picker and name/command prompt.
4343+Arrow keys to navigate, type to filter, Enter to attach, `q` to quit. When pty-relay is installed, remote sessions appear grouped by host. Use `host/session` syntax to filter by host (e.g., `prod/api`). Creating a new session walks through a directory picker and name/command prompt.
44444545When you detach from a session entered via the interactive list (`Ctrl+\`), you return to the list. The session keeps running in the background.
4646
+41-16
src/tui/interactive.ts
···9696// Relay integration
9797// ============================================================
98989999-interface RemoteSession {
9999+export interface RemoteSession {
100100 name: string;
101101 status: string;
102102 command?: string;
103103 cwd?: string;
104104}
105105106106-interface RelayHost {
106106+export interface RelayHost {
107107 label: string;
108108 url: string;
109109 sessions: RemoteSession[];
···141141// Computed values
142142// ============================================================
143143144144-interface ListItem {
144144+export interface ListItem {
145145 type: "session" | "create" | "remote" | "remote-create";
146146 session?: SessionInfo;
147147 remote?: { host: RelayHost; session: RemoteSession };
···184184 return matches.map(m => m.item);
185185}
186186187187-const filteredGroups = computed<SelectableGroup<ListItem>[]>(() => {
188188- const filter = filterText.get();
187187+/** Build filtered groups from local sessions and relay hosts.
188188+ * Exported for unit testing. */
189189+export function buildFilteredGroups(
190190+ filter: string,
191191+ localSessions: SessionInfo[],
192192+ hosts: RelayHost[],
193193+): SelectableGroup<ListItem>[] {
194194+ // Parse "host/session" filter syntax
195195+ let hostFilter = "";
196196+ let sessionFilter = filter;
197197+ if (filter.includes("/")) {
198198+ const slashIdx = filter.indexOf("/");
199199+ hostFilter = filter.slice(0, slashIdx).trim();
200200+ sessionFilter = filter.slice(slashIdx + 1).trim();
201201+ }
189202190190- // Local group
191191- const localItems: ListItem[] = sortedSessions.get().map(s => ({ type: "session" as const, session: s }));
192192- const filteredLocal = filter ? filterAndSort(filter, localItems) : localItems;
193193- // Always add "Create new session" at the end of local group
194194- const localWithCreate: ListItem[] = [...filteredLocal, { type: "create" }];
203203+ const showCreate = !filter || "new".startsWith(filter.toLowerCase());
195204196196- const groups: SelectableGroup<ListItem>[] = [
197197- { title: "Local", items: localWithCreate },
198198- ];
205205+ // Local group — skip if host filter is set (user is filtering by remote host)
206206+ const groups: SelectableGroup<ListItem>[] = [];
207207+ if (!hostFilter || fuzzyMatch(hostFilter, "local").match) {
208208+ const localItems: ListItem[] = localSessions.map(s => ({ type: "session" as const, session: s }));
209209+ const filteredLocal = sessionFilter ? filterAndSort(sessionFilter, localItems) : localItems;
210210+ const localWithCreate: ListItem[] = showCreate ? [...filteredLocal, { type: "create" }] : filteredLocal;
211211+ groups.push({ title: "Local", items: localWithCreate });
212212+ }
199213200214 // Remote groups from relay
201201- for (const host of relayHosts.get()) {
215215+ for (const host of hosts) {
202216 if (host.error) continue;
217217+ // If host filter is set, only include matching hosts
218218+ if (hostFilter && !fuzzyMatch(hostFilter, host.label).match) continue;
219219+203220 const remoteItems: ListItem[] = host.sessions.map(s => ({
204221 type: "remote" as const,
205222 remote: { host, session: s },
206223 }));
207207- const filtered = filter ? filterAndSort(filter, remoteItems) : remoteItems;
224224+ const filtered = sessionFilter ? filterAndSort(sessionFilter, remoteItems) : remoteItems;
208225 const items: ListItem[] = [...filtered];
209209- if (host.spawn_enabled) {
226226+ if (host.spawn_enabled && showCreate) {
210227 items.push({ type: "remote-create", remoteHost: host });
211228 }
212229 if (items.length > 0 || !filter) {
···215232 }
216233217234 return groups;
235235+}
236236+237237+const filteredGroups = computed<SelectableGroup<ListItem>[]>(() => {
238238+ return buildFilteredGroups(
239239+ filterText.get(),
240240+ sortedSessions.get(),
241241+ relayHosts.get(),
242242+ );
218243});
219244220245// Total item count across all groups (for scroll region)
···329329330330 let ss = tui.screenshot();
331331 expect(ss.text).toContain(name1);
332332- expect(ss.text).toContain("Create new session...");
332332+ // "Create new session..." should be hidden when filter doesn't match "new"
333333+ expect(ss.text).not.toContain("Create new session...");
333334334335 // Backspace to remove filter
335336 for (let i = 0; i < filterText.length; i++) {