diff --git a/README.md b/README.md index d100bd0..c12b07e 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,92 @@ -# Session Mover TUI +# opencode-session-mover -A Python Textual TUI for easily moving OpenCode sessions between projects and workspaces without writing SQL. +*USE AT YOUR OWN RUSK* -## Quick Start +⚠️ This is entirely vibe coded and mostly untested. It should backup yout database before changing anything, but you should make your own backup first ⚠️ + +A terminal UI for managing [OpenCode](https://opencode.ai) sessions — move, copy, or delete sessions across projects without writing SQL. + +## Requirements + +- Python 3.11+ +- [Textual](https://github.com/Textualize/textual) (`pip install -r requirements.txt`) + +A `mise.toml` is included if you use [mise](https://mise.jdx.dev) — it pins Python 3.14 and SQLite 3.51. Run `mise install` to set up the environment. + +OpenCode stores its data in an SQLite database, usually at `~/.local/share/opencode/opencode.db`. + +## Running ```bash -# Install dependencies +# Install dependencies With mise +mise install + +# Install dependencies and run pip install -r requirements.txt - -# Run (looks for opencode.db in current directory) -python session-mover.py - -# Or specify a database path -python session-mover.py /path/to/opencode.db +python main.py /path/to/opencode.db ``` -## How to Use +If no path is given it looks for `opencode.db` in the current directory. -1. **Browse** - See all sessions organized by project/workspace on the left -2. **Select** - Click or use arrow keys + Enter to select sessions (multiple with Shift+Click) -3. **Filter** - Type in the filter box to search session titles -4. **Move** - Press `m` to move selected sessions to another project -5. **Copy** - Press `c` to duplicate sessions to another project -6. **Backup** - Press `b` to manually create a database backup +## Layout -### Hotkeys (always visible at bottom) +The UI has two panels: -- `b` - Backup database -- `m` - Move selected sessions -- `c` - Copy selected sessions -- `a` - Toggle archived sessions visibility -- `r` - Refresh all data -- `q` - Quit -- `Escape` - Deselect / close dialogs +- **Left — All Projects**: a list of your OpenCode projects with active session counts +- **Right — Sessions**: sessions for the selected project, with a filter bar at the top -### Move/Copy Dialog +## Keybindings -1. Select destination project (dropdown) -2. Optionally select a workspace within that project -3. Review the SQL preview to see what will execute -4. Press `Enter` or click Confirm to execute -5. A backup is automatically created before the first write operation +| Key | Action | +|-----|--------| +| `Tab` | Move focus between project list and session table | +| `f` | Focus the filter input | +| `Enter` (in filter) | Return focus to session table | +| `Escape` | Clear filter if active, otherwise clear selection | +| `Space` | Select / deselect the highlighted session | +| `m` | Move selected sessions to another project | +| `c` | Copy selected sessions to another project | +| `d` | Delete selected sessions (with confirmation) | +| `a` | Toggle visibility of archived sessions | +| `b` | Create a manual database backup | +| `r` | Refresh all data from the database | +| `q` | Quit | -### Safety +## Selecting Sessions -- Before any move/copy, a timestamped backup is created: `opencode-YYYYMMDD-HHMMSS.db` -- Confirmation dialog shows exactly what will happen -- Changes appear immediately in the UI after success +Navigate the session table with the arrow keys. Press `Space` to toggle selection on the highlighted row. The selection count is shown at the bottom of the session panel. Selection is cleared when you switch projects. -## Database Schema Support +Use `f` to filter sessions by title or slug (case-insensitive). Filtering and selection work together — you can filter, select matching sessions, clear the filter, and the selections persist. -Works with OpenCode's `opencode.db` with tables: -- `session` (with project_id, workspace_id, parent_id) -- `project`, `workspace` -- `message` → `part`, `todo`, `session_share` (all cascade automatically) +## Move / Copy -## Tips +1. Select one or more sessions with `Space` +2. Press `m` (move) or `c` (copy) +3. Choose a destination project from the dropdown (sorted alphabetically, current project excluded) +4. Optionally choose a workspace within that project +5. Click Confirm or press `Enter` -- Filter by typing in the search box (searches title and slug) -- Toggle archived sessions with `a` (hidden by default) -- The left panel shows Projects → Workspaces hierarchy -- Selected sessions remain selected when filtering, so you can search, select, then clear filter +A timestamped backup is created automatically before the first write operation in a session: `opencode-YYYYMMDD-HHMMSS.db` + +Copy duplicates the session and all related records (messages, parts, todos). Move updates the session's project/workspace in place. + +## Delete + +1. Select one or more sessions with `Space` +2. Press `d` +3. Review the list of sessions to be deleted in the confirmation dialog +4. Click Delete to confirm, or Cancel / `Escape` to abort + +Deletion is permanent and removes all related records (messages, todos, etc). A backup is created automatically before the first delete. + +## Archived Sessions + +OpenCode can archive sessions. By default they are hidden. Press `a` to show them alongside active sessions — the session panel header will update to indicate archived mode is on. + +## Files + +| File | Purpose | +|------|---------| +| `main.py` | Entry point | +| `app.py` | Textual TUI — layout, keybindings, UI logic | +| `database.py` | SQLite queries — sessions, move, copy, delete, backup | diff --git a/app.py b/app.py index 2ea29ab..54677cf 100644 --- a/app.py +++ b/app.py @@ -381,8 +381,13 @@ class SessionMoverApp(App): def on_mount(self) -> None: """Initialize app.""" - self.db.connect() - self.db.load_reference_data() + try: + self.db.connect() + self.db.load_reference_data() + except FileNotFoundError as e: + self.notify(str(e), severity="error", timeout=10) + self.set_timer(1, self.exit) + return self._project_ids: List[str] = [] self.refresh_project_list() diff --git a/database.py b/database.py index 8876368..0a39fed 100644 --- a/database.py +++ b/database.py @@ -62,6 +62,8 @@ class Database: def connect(self): """Establish database connection.""" + if not self.db_path.exists(): + raise FileNotFoundError(f"Database not found: {self.db_path}") self.conn = sqlite3.connect(self.db_path) self.conn.row_factory = sqlite3.Row diff --git a/main.py b/main.py index 8bf4223..c3075a8 100644 --- a/main.py +++ b/main.py @@ -1,8 +1,16 @@ #!/usr/bin/env python3 """Session Mover TUI - Move OpenCode sessions between projects without SQL.""" +import sys +from pathlib import Path from app import SessionMoverApp if __name__ == "__main__": - app = SessionMoverApp() + db_path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("opencode.db") + + if not db_path.exists(): + print(f"Error: database file not found: {db_path}", file=sys.stderr) + sys.exit(1) + + app = SessionMoverApp(str(db_path)) app.run()