diff --git a/.cz.toml b/.cz.toml index e0d12ea..6dd6b70 100644 --- a/.cz.toml +++ b/.cz.toml @@ -1,10 +1,11 @@ [tool.commitizen] name = "cz_conventional_commits" -version = "0.1.0" +version = "0.0.0" tag_format = "v$version" version_files = [ "package.json:version", "src-tauri/Cargo.toml:version", "src-tauri/tauri.conf.json:version", + "src/components/Settings.tsx:APP_VERSION", ] update_changelog_on_bump = true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f51b16f..35a3029 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,6 @@ name: CI on: pull_request: - branches: [main] jobs: check: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d2ba710..b0f696b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,7 @@ jobs: outputs: bumped: ${{ steps.bump.outputs.bumped }} version: ${{ steps.bump.outputs.version }} + pre_bumped: ${{ steps.bump.outputs.pre_bumped }} steps: - uses: actions/checkout@v4 with: @@ -23,20 +24,53 @@ jobs: - run: pip install commitizen + - run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + - id: bump run: | - if cz bump --dry-run 2>/dev/null; then + CURRENT=$(python3 -c " + import tomllib + with open('.cz.toml', 'rb') as f: + print(tomllib.load(f)['tool']['commitizen']['version']) + ") + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + LATEST_VERSION=${LATEST_TAG#v} + + if [ -z "$LATEST_TAG" ]; then + # First release — no existing tags + if [ "$CURRENT" != "0.0.0" ]; then + echo "First release: $CURRENT (pre-bumped, no existing tags)" + echo "bumped=true" >> "$GITHUB_OUTPUT" + echo "version=$CURRENT" >> "$GITHUB_OUTPUT" + echo "pre_bumped=true" >> "$GITHUB_OUTPUT" + else + cz bump --yes --increment MINOR + VERSION=$(cz version --project) + echo "First release: $VERSION" + echo "bumped=true" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "pre_bumped=false" >> "$GITHUB_OUTPUT" + fi + elif [ "$CURRENT" != "$LATEST_VERSION" ]; then + echo "Version already bumped to $CURRENT (latest tag: $LATEST_TAG)" + echo "bumped=true" >> "$GITHUB_OUTPUT" + echo "version=$CURRENT" >> "$GITHUB_OUTPUT" + echo "pre_bumped=true" >> "$GITHUB_OUTPUT" + elif cz bump --yes --dry-run 2>/dev/null; then cz bump --yes VERSION=$(cz version --project) echo "bumped=true" >> "$GITHUB_OUTPUT" echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "pre_bumped=false" >> "$GITHUB_OUTPUT" else + echo "No version bump needed" echo "bumped=false" >> "$GITHUB_OUTPUT" fi - if: steps.bump.outputs.bumped == 'true' run: | - # Cargo.lock needs regenerating after Cargo.toml version bump cd src-tauri && cargo update --workspace && cd .. - if: steps.bump.outputs.bumped == 'true' @@ -50,6 +84,7 @@ jobs: src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/tauri.conf.json + src/components/Settings.tsx build: needs: bump @@ -122,13 +157,22 @@ jobs: name: bumped-source - name: Commit version bump and tag + if: needs.bump.outputs.pre_bumped != 'true' run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git add -A git commit -m "bump: v${{ needs.bump.outputs.version }}" git tag "v${{ needs.bump.outputs.version }}" - git push origin main --follow-tags + git push origin HEAD --follow-tags + + - name: Tag pre-bumped version + if: needs.bump.outputs.pre_bumped == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag "v${{ needs.bump.outputs.version }}" + git push origin "v${{ needs.bump.outputs.version }}" - uses: actions/download-artifact@v4 with: @@ -169,4 +213,4 @@ jobs: git add -A git commit -m "Update claude-manager to v${VERSION}" || true git push || true - fi \ No newline at end of file + fi diff --git a/CLAUDE.md b/CLAUDE.md index 0e8cf1d..87e697a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,9 +59,12 @@ src-tauri/src/metadata.rs Local metadata store (rename, archive, delete) ## Linting -- Frontend: `bunx biome check` (0 errors, warnings acceptable for exhaustive deps and a11y click handlers) -- Backend: `cargo fmt -- --check && cargo clippy` -- CI runs both on PR +- `bun run check` — runs full CI pipeline: `tsc`, `biome check`, `vitest`, `cargo fmt --check`, `cargo clippy` +- `bun run fmt` — auto-format both frontend (biome) and backend (rustfmt) +- `bun run clippy` — run clippy standalone +- Frontend: `bunx biome check` (0 errors, 0 warnings) +- Backend: `cargo fmt -- --check && cargo clippy -D warnings` +- CI runs on all PRs and must pass before merge ## Documentation diff --git a/README.md b/README.md index d08660e..b9c7288 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ brew install --cask claude-manager curl -fsSL https://raw.githubusercontent.com/JeffreyWardman/claude-manager/main/install.sh | sh ``` +On macOS and Linux, both install methods also create `claude-manager` and `cmanager` CLI commands so you can launch from the terminal. + ## Usage ### Keyboard shortcuts diff --git a/biome.json b/biome.json index 6ee7a72..ae46e59 100644 --- a/biome.json +++ b/biome.json @@ -30,7 +30,7 @@ "a11y": { "useKeyWithClickEvents": "warn", "noStaticElementInteractions": "warn", - "useSemanticElements": "warn", + "useSemanticElements": "off", "useValidAnchor": "warn", "noSvgWithoutTitle": "off" } diff --git a/install.sh b/install.sh index d73ae4c..b96ee7f 100755 --- a/install.sh +++ b/install.sh @@ -42,7 +42,7 @@ if [ "$PLATFORM" = "macos" ]; then curl -fSL -o "$DMG_PATH" "$URL" echo "Installing to /Applications..." - MOUNT=$(hdiutil attach "$DMG_PATH" -nobrowse -quiet | tail -1 | awk '{print $3}') + MOUNT=$(hdiutil attach "$DMG_PATH" -nobrowse -quiet | tail -1 | sed 's/.*\(\/Volumes\/.*\)/\1/') if [ -d "/Applications/$APP_NAME.app" ]; then rm -rf "/Applications/$APP_NAME.app" fi @@ -50,7 +50,13 @@ if [ "$PLATFORM" = "macos" ]; then hdiutil detach "$MOUNT" -quiet rm -rf "$TMPDIR" + # Create CLI symlinks + mkdir -p /usr/local/bin + ln -sf "/Applications/$APP_NAME.app/Contents/MacOS/$APP_NAME" /usr/local/bin/claude-manager + ln -sf "/Applications/$APP_NAME.app/Contents/MacOS/$APP_NAME" /usr/local/bin/cmanager + echo "Installed $APP_NAME $VERSION to /Applications/$APP_NAME.app" + echo "CLI commands available: claude-manager, cmanager" elif [ "$PLATFORM" = "linux" ]; then if [ "$ARCH_TAG" != "x64" ]; then @@ -72,7 +78,9 @@ elif [ "$PLATFORM" = "linux" ]; then sudo dpkg -i "$DEB_PATH" || sudo apt-get install -f -y rm -rf "$TMPDIR" + sudo ln -sf /usr/bin/claude-manager /usr/bin/cmanager echo "Installed $APP_NAME $VERSION" + echo "CLI commands available: claude-manager, cmanager" else APPIMAGE="${APP_NAME}_${VERSION}_amd64.AppImage" URL="$BASE_URL/$APPIMAGE" @@ -83,7 +91,9 @@ elif [ "$PLATFORM" = "linux" ]; then curl -fSL -o "$INSTALL_DIR/$APP_NAME" "$URL" chmod +x "$INSTALL_DIR/$APP_NAME" + ln -sf "$INSTALL_DIR/$APP_NAME" "$INSTALL_DIR/cmanager" echo "Installed $APP_NAME $VERSION to $INSTALL_DIR/$APP_NAME" + echo "CLI commands available: claude-manager, cmanager" echo "Make sure $INSTALL_DIR is in your PATH" fi fi \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5b5285a..a99934b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-manager", - "version": "0.1.0", + "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-manager", - "version": "0.1.0", + "version": "0.0.0", "dependencies": { "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2.7.0", diff --git a/package.json b/package.json index b9d43e1..402dfbe 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "claude-manager", "private": true, - "version": "0.1.0", + "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", @@ -13,7 +13,9 @@ "test:watch": "vitest", "lint": "biome check .", "format": "biome check --write .", - "check": "tsc --noEmit && biome check . && vitest run" + "check": "tsc --noEmit && biome check . && vitest run && cargo fmt --manifest-path src-tauri/Cargo.toml -- --check && cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings", + "fmt": "biome check --write . && cargo fmt --manifest-path src-tauri/Cargo.toml", + "clippy": "cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings" }, "dependencies": { "@tauri-apps/api": "^2", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 10cc432..efb17d9 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -444,6 +444,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "chrono" version = "0.4.44" @@ -458,7 +464,7 @@ dependencies = [ [[package]] name = "claude-manager" -version = "0.1.0" +version = "0.0.0" dependencies = [ "base64 0.22.1", "dirs-next", @@ -1001,7 +1007,7 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" dependencies = [ - "memoffset 0.9.1", + "memoffset", "rustc_version", ] @@ -1823,15 +1829,6 @@ dependencies = [ "cfb", ] -[[package]] -name = "ioctl-rs" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7970510895cee30b3e9128319f2cefd4bde883a39f38baa279567ba3a7eb97d" -dependencies = [ - "libc", -] - [[package]] name = "ipnet" version = "2.12.0" @@ -2139,15 +2136,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] - [[package]] name = "memoffset" version = "0.9.1" @@ -2243,16 +2231,14 @@ checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] name = "nix" -version = "0.25.1" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "autocfg", - "bitflags 1.3.2", + "bitflags 2.11.0", "cfg-if", + "cfg_aliases", "libc", - "memoffset 0.6.5", - "pin-utils", ] [[package]] @@ -2725,12 +2711,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "piper" version = "0.2.5" @@ -2790,9 +2770,9 @@ dependencies = [ [[package]] name = "portable-pty" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806ee80c2a03dbe1a9fb9534f8d19e4c0546b790cde8fd1fea9d6390644cb0be" +checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e" dependencies = [ "anyhow", "bitflags 1.3.2", @@ -2802,7 +2782,7 @@ dependencies = [ "libc", "log", "nix", - "serial", + "serial2", "shared_library", "shell-words", "winapi", @@ -3446,45 +3426,14 @@ dependencies = [ ] [[package]] -name = "serial" -version = "0.4.0" +name = "serial2" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1237a96570fc377c13baa1b88c7589ab66edced652e43ffb17088f003db3e86" -dependencies = [ - "serial-core", - "serial-unix", - "serial-windows", -] - -[[package]] -name = "serial-core" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f46209b345401737ae2125fe5b19a77acce90cd53e1658cda928e4fe9a64581" -dependencies = [ - "libc", -] - -[[package]] -name = "serial-unix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f03fbca4c9d866e24a459cbca71283f545a37f8e3e002ad8c70593871453cab7" -dependencies = [ - "ioctl-rs", - "libc", - "serial-core", - "termios", -] - -[[package]] -name = "serial-windows" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c6d3b776267a75d31bbdfd5d36c0ca051251caafc285827052bc53bcdc8162" +checksum = "fcdbc46aa3882ec3d48ec2b5abcb4f0d863a13d7599265f3faa6d851f23c12f3" dependencies = [ + "cfg-if", "libc", - "serial-core", + "winapi", ] [[package]] @@ -4229,15 +4178,6 @@ dependencies = [ "utf-8", ] -[[package]] -name = "termios" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a" -dependencies = [ - "libc", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -4573,7 +4513,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ - "memoffset 0.9.1", + "memoffset", "tempfile", "windows-sys 0.61.2", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 15033c8..9ce2278 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,8 +1,7 @@ [package] name = "claude-manager" -version = "0.1.0" +version = "0.0.0" description = "A Tauri App" -authors = ["you"] edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -27,7 +26,10 @@ tauri-plugin-shell = "2.3.5" tauri-plugin-fs = "2.5.0" dirs-next = "2.0.0" regex = "1.12.3" -portable-pty = "0.8" +portable-pty = "0.9" base64 = "0.22" shell-escape = "0.1.5" +[profile.release] +strip = true +lto = true diff --git a/src-tauri/src/pty_manager.rs b/src-tauri/src/pty_manager.rs index 079ad97..9a27295 100644 --- a/src-tauri/src/pty_manager.rs +++ b/src-tauri/src/pty_manager.rs @@ -65,6 +65,7 @@ impl PtyState { } #[tauri::command] +#[allow(clippy::too_many_arguments)] pub fn pty_spawn( id: String, cwd: String, diff --git a/src-tauri/src/sessions.rs b/src-tauri/src/sessions.rs index 2d8cb35..1d0ac8a 100644 --- a/src-tauri/src/sessions.rs +++ b/src-tauri/src/sessions.rs @@ -251,7 +251,7 @@ pub fn get_all_sessions(config_dir: &str) -> Vec { // Step 3: Sort by recency. For each cwd with a live process, mark the most // recent JSONL session in that directory as Active (and attach the pid). - candidates.sort_by(|a, b| b.0.cmp(&a.0)); + candidates.sort_by_key(|c| std::cmp::Reverse(c.0)); let mut active_cwds_marked: std::collections::HashSet = std::collections::HashSet::new(); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 70f71d5..0e8b96b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "claude-manager", - "version": "0.1.0", + "version": "0.0.0", "identifier": "com.jeffreywardman.claude-manager", "build": { "beforeDevCommand": "bun run dev", diff --git a/src/App.tsx b/src/App.tsx index e175556..bf2d9b0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -213,7 +213,19 @@ function AppInner() { setActiveGroupId(null); localStorage.removeItem(activeGroupKey(configDir)); } - }, [groups, activeGroupId]); + }, [groups, activeGroupId, configDir]); + + const persistGroups = useCallback((next: PaneGroup[]) => { + setGroups(next); + localStorage.setItem(groupsKey(configDirRef.current), JSON.stringify(next)); + }, []); + + const activateGroup = useCallback((id: string) => { + setActiveGroupId(id); + localStorage.setItem(activeGroupKey(configDirRef.current), id); + setFocusedSlotIdx(0); + setStandaloneSelectedId(null); + }, []); // Keep focusedSlotIdx in bounds useEffect(() => { @@ -239,18 +251,6 @@ function AppInner() { } }, [sessions, groups, persistGroups]); - function persistGroups(next: PaneGroup[]) { - setGroups(next); - localStorage.setItem(groupsKey(configDir), JSON.stringify(next)); - } - - function activateGroup(id: string) { - setActiveGroupId(id); - localStorage.setItem(activeGroupKey(configDir), id); - setFocusedSlotIdx(0); - setStandaloneSelectedId(null); - } - const handleActivateGroupAtSlot = useCallback((groupId: string, slotIdx: number) => { setActiveGroupId(groupId); localStorage.setItem(activeGroupKey(configDirRef.current), groupId); @@ -278,9 +278,9 @@ function AppInner() { const newActive = next[0]?.id ?? null; setActiveGroupId(newActive); if (newActive) { - localStorage.setItem(activeGroupKey(configDir), newActive); + localStorage.setItem(activeGroupKey(configDirRef.current), newActive); } else { - localStorage.removeItem(activeGroupKey(configDir)); + localStorage.removeItem(activeGroupKey(configDirRef.current)); } } }, @@ -420,7 +420,7 @@ function AppInner() { } // Session not in any group — just visually select it setActiveGroupId(null); - localStorage.removeItem(activeGroupKey(configDir)); + localStorage.removeItem(activeGroupKey(configDirRef.current)); setFocusedSlotIdx(0); setStandaloneSelectedId(s.session_id); }, diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx index b942fd3..fe685cd 100644 --- a/src/components/CommandPalette.tsx +++ b/src/components/CommandPalette.tsx @@ -113,9 +113,15 @@ export function CommandPalette({ aria-label="Command palette" style={modalBackdropStyle} onClick={onClose} + onKeyDown={(e) => { + if (e.key === "Escape") { + onClose(); + } + }} >
e.stopPropagation()} onKeyDown={(e) => { if (e.key === "Tab") { diff --git a/src/components/GridLayout.tsx b/src/components/GridLayout.tsx index 6aca120..784f794 100644 --- a/src/components/GridLayout.tsx +++ b/src/components/GridLayout.tsx @@ -121,8 +121,7 @@ export function GridLayout({ const multiPane = group.slots.length > 1; return ( -
); })} -
+ ); } diff --git a/src/components/NewSessionModal.tsx b/src/components/NewSessionModal.tsx index 5a6c19f..d9ac364 100644 --- a/src/components/NewSessionModal.tsx +++ b/src/components/NewSessionModal.tsx @@ -68,8 +68,19 @@ export function NewSessionModal({ cwds, onConfirm, onClose }: Props) { aria-label="New session" style={modalBackdropStyle} onClick={onClose} + onKeyDown={(e) => { + if (e.key === "Escape") { + onClose(); + } + }} > -
e.stopPropagation()} style={modalDialogStyle}> + {/* biome-ignore lint/a11y/useKeyWithClickEvents: stopPropagation prevents backdrop dismiss */} +
e.stopPropagation()} + style={modalDialogStyle} + >
{ + if (e.key === "Escape") { + handleClose(); + } + }} > + {/* biome-ignore lint/a11y/useKeyWithClickEvents: stopPropagation prevents backdrop dismiss */}
Version - 0.1.0 + {APP_VERSION}

A desktop app for managing multiple Claude Code sessions side by side. diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index beb4781..02140ee 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,5 +1,5 @@ import { invoke } from "@tauri-apps/api/core"; -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { SLOT_COUNTS } from "../groupOps"; import type { ActivityState } from "../hooks/usePtyActivity"; import type { SortMode } from "../sidebarUtils"; @@ -391,6 +391,12 @@ export function Sidebar({ : filteredSessions.filter((s) => matchSession(s, searchQuery)) : filteredSessions; + const startRename = useCallback((session: ClaudeSession) => { + setRenamingId(session.session_id); + setRenameValue(session.display_name ?? session.project_name); + setContextMenu(null); + }, []); + useEffect(() => { const handleKey = (e: KeyboardEvent) => { if (renamingId || renamingGroupId) { @@ -432,12 +438,6 @@ export function Sidebar({ return () => window.removeEventListener("keydown", handleKey); }, [sessions, selectedId, onSelect, renamingId, renamingGroupId, filteredSessions, startRename]); - function startRename(session: ClaudeSession) { - setRenamingId(session.session_id); - setRenameValue(session.display_name ?? session.project_name); - setContextMenu(null); - } - async function commitRename(sessionId: string) { try { const trimmed = renameValue.trim(); @@ -638,6 +638,7 @@ export function Sidebar({ {filterDropdownOpen && (

e.stopPropagation()} style={{ position: "absolute", @@ -928,6 +929,8 @@ export function Sidebar({
{/* Group header */}
{ + if ( + (e.key === "Enter" || e.key === " ") && + !isRenamingGroup && + !groupContextMenu + ) { + e.preventDefault(); + onActivateGroup(group.id); + } + }} onMouseEnter={(e) => { if (!isActive) { e.currentTarget.style.background = "var(--item-hover)"; @@ -1078,6 +1091,7 @@ export function Sidebar({ {showLayoutPicker && (
e.stopPropagation()} style={{ position: "absolute", @@ -1194,6 +1208,8 @@ export function Sidebar({ return (
{ + if ((e.key === "Enter" || e.key === " ") && session) { + e.preventDefault(); + onActivateGroupAtSlot(group.id, slotIdx); + } + }} onContextMenu={ session ? (e) => { @@ -1432,6 +1454,8 @@ export function Sidebar({ return (
{ if (el) { itemRefs.current.set(session.session_id, el); @@ -1476,6 +1500,12 @@ export function Sidebar({ onSelect(session); } }} + onKeyDown={(e) => { + if ((e.key === "Enter" || e.key === " ") && !isRenaming) { + e.preventDefault(); + onSelect(session); + } + }} onDoubleClick={() => { if (!isRenaming) { startRename(session); @@ -1655,6 +1685,7 @@ export function Sidebar({ {profileDropdownOpen && (
e.stopPropagation()} style={{ position: "absolute", diff --git a/src/components/TerminalPane.tsx b/src/components/TerminalPane.tsx index a482745..0f83274 100644 --- a/src/components/TerminalPane.tsx +++ b/src/components/TerminalPane.tsx @@ -33,6 +33,7 @@ export function TerminalPane({ ptyId, cwd, cmd, configDir }: Props) { } }, [theme]); + // biome-ignore lint/correctness/useExhaustiveDependencies: configDir excluded by design — profile switch unmounts/remounts component useEffect(() => { const container = containerRef.current; if (!container) { @@ -123,7 +124,7 @@ export function TerminalPane({ ptyId, cwd, cmd, configDir }: Props) { observer.disconnect(); term.dispose(); }; - }, [ptyId, cwd, theme.terminal, cmd]); // eslint-disable-line react-hooks/exhaustive-deps + }, [ptyId, cwd, theme.terminal, cmd]); return
; } diff --git a/src/hooks/useFocusTrap.ts b/src/hooks/useFocusTrap.ts index a04d3fe..5319fe3 100644 --- a/src/hooks/useFocusTrap.ts +++ b/src/hooks/useFocusTrap.ts @@ -4,6 +4,7 @@ export function useFocusTrap( dialogRef: React.RefObject, onEscape?: () => void, ) { + // biome-ignore lint/correctness/useExhaustiveDependencies: dialogRef is a stable ref useEffect(() => { const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape" && onEscape) {