Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ src/
├── styling/ # Visual theming and icons
│ ├── theme.py # Palettes, colours, badge geometry, method_color(), status_color()
│ ├── theme_manager.py # ThemeManager — QPalette + QSettings
│ ├── tab_settings_manager.py # TabSettingsManager — request-tab QSettings bridge (preview, limits, activate-on-close, wrap mode)
│ ├── global_qss.py # build_global_qss() — global stylesheet builder
│ └── icons.py # Phosphor font-glyph icon provider (phi())
├── widgets/ # Reusable shared components
Expand Down Expand Up @@ -221,7 +222,7 @@ src/
│ ├── collection_runner.py
│ ├── import_dialog.py
│ ├── save_request_dialog.py # Save draft request to collection
│ └── settings_dialog.py # Settings (theme, colour scheme)
│ └── settings_dialog.py # Settings (theme + request-tab behaviour)
├── environments/ # Environment management widgets
│ ├── environment_editor.py
│ └── environment_selector.py
Expand All @@ -247,15 +248,20 @@ src/
│ └── search_filter.py # _SearchFilterMixin — response search/filter
├── navigation/ # Tab switching and path navigation
│ ├── breadcrumb_bar.py
│ ├── request_tab_bar.py
│ ├── request_tab_bar.py # Compatibility wrapper re-exporting the wrapped deck
│ ├── request_tabs/ # Wrapped multi-row request tab deck sub-package
│ │ ├── __init__.py
│ │ ├── bar.py # RequestTabBar custom wrapped-row deck
│ │ ├── labels.py # TabLabel / FolderTabLabel chip content widgets
│ │ └── tab_button.py # TabButton chip with close + reorder interactions
│ └── tab_manager.py # TabManager + TabContext (with local_overrides, draft_name)
└── popups/ # Response metadata popups
├── status_popup.py # HTTP status code explanation
├── timing_popup.py # Request timing breakdown
├── size_popup.py # Response/request size breakdown
└── network_popup.py # Network/TLS connection details
tests/
├── conftest.py # Autouse fresh-DB fixture + qapp fixture
├── conftest.py # Autouse fresh-DB fixture + qapp fixture + tab-settings reset
├── unit/ # Repository & service layer tests
│ ├── database/ # Repository tests
│ │ ├── test_repository.py
Expand All @@ -277,8 +283,10 @@ tests/
└── ui/ # End-to-end PySide6 widget tests
├── conftest.py # _no_fetch (autouse) + helpers
├── test_main_window.py
├── test_main_window_tabs_navigation.py # Wrapped tab deck shortcuts + search tests
├── test_main_window_save.py # SaveButton + RequestSaveEndToEnd tests
├── test_main_window_draft.py # Draft tab open/save lifecycle tests
├── test_main_window_session.py # Tab session persistence (save/restore) tests
├── styling/ # Theme and icon tests
│ ├── test_theme_manager.py
│ └── test_icons.py
Expand Down
64 changes: 57 additions & 7 deletions .github/instructions/architecture.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ ThemeManager ──QPalette + global QSS──► QApplication
──theme_changed signal──► widgets (refresh dynamic styles)
──QSettings──► persistent user preferences

TabSettingsManager ──QSettings──► persistent request-tab preferences
──settings_changed──► MainWindow / RequestTabBar

RequestEditorWidget ──send_requested──► MainWindow
MainWindow → HttpSendWorker (QThread) → HttpService.send_request()
→ HttpSendWorker.finished(HttpResponseDict) → ResponseViewerWidget.load_response()
Expand All @@ -68,6 +71,14 @@ RequestEditorWidget ──_on_fetch_schema──► SchemaFetchWorker (QThread
and passed to `MainWindow`. It owns the app-wide stylesheet, QPalette,
and QSettings persistence for theme preferences. See
`pyside6.instructions.md` for widget styling rules.
- `TabSettingsManager` (`ui.styling.tab_settings_manager`) is created once
in `main.py` and passed to `MainWindow`. It persists request-tab
behaviour (preview enablement, compact labels, duplicate-name path
disambiguation, wrap mode, tab limit, and close-activation policy)
via QSettings. It also stores the open-tab session (tab list + active
index) for restore-on-launch via `save_open_tabs()` /
`load_open_tabs()` / `clear_open_tabs()`. Session data is a JSON
string under QSettings key `tabs/session`.
- `CollectionService` is instantiated as `self._svc = CollectionService()` in
`CollectionWidget.__init__`, but **every method is `@staticmethod`**.
Do not add instance state without updating every call site.
Expand Down Expand Up @@ -144,7 +155,15 @@ Key signals to know (always-on summary):
- `SavedResponsesPanel` emits `save_current_requested`,
`rename_requested`, `duplicate_requested`, and `delete_requested` — all
handled in `MainWindow` through `CollectionService`.
- `ThemeManager.theme_changed()` → widgets refresh dynamic styles.
- `ThemeManager.theme_changed()` → widgets refresh dynamic styles, including
the wrapped request-tab deck chip styling.
- `TabSettingsManager.settings_changed()` → `MainWindow` / `RequestTabBar`
refresh tab behaviour and label presentation, including switching
between single-row and wrapped-row layouts.
- `MainWindow` View menu exposes `Search Tabs…` (`Ctrl+P`), `Next Tab`
(`Ctrl+Tab`, `Ctrl+PgDown`), and `Previous Tab`
(`Ctrl+Shift+Tab`, `Ctrl+PgUp`) so the wrapped deck keeps editor-style
keyboard navigation even though it is no longer a native `QTabBar`.
- `VariablePopup` uses **class-level callbacks**, not signals — wired once
in `MainWindow.__init__`.

Expand Down Expand Up @@ -281,15 +300,46 @@ into `%Y-%m-%d %H:%M` strings for the UI.
Set to `"Untitled Request"` when a draft tab is opened. Updated when
the user renames via the breadcrumb bar. Used as fallback label in the
save-to-collection dialog. `None` for persisted request tabs.
8. **VariablePopup uses class-level callbacks, not Qt signals** —
8. **Request-tab behaviour is settings-driven** — preview tabs, compact
labels, duplicate-name path suffixes, tab insertion position, wrap
mode, tab limit, and close-activation policy are read from
`TabSettingsManager`.
`RequestTabBar` is a custom wrapped multi-row widget, not a native
`QTabBar`; it keeps a small compatibility API (`currentIndex()`,
`setCurrentIndex()`, `count()`, `tabRect()`, `tabButton()`,
`tabToolTip()`, `select_next_tab()`, `select_previous_tab()`,
`tab_search_text()`, `tab_request_info()`) so `MainWindow` and tests
do not depend on Qt tab-bar internals. `MainWindow` enforces the
limit/promotion policies when opening and closing tabs.
**Session persistence:** `_TabControllerMixin._persist_open_tabs()` saves
the current tab list (type + DB id + method + name for requests) and
active index after every tab open/close/reorder and in `closeEvent`.
**Deferred tab materialisation:** `_restore_tabs()` restores tabs
lazily after `CollectionWidget.load_finished` fires. Request tabs
with `method` and `name` in the session data are created as
lightweight tab-bar chips stored in `_deferred_tabs`; the editor and
viewer widgets are built on first selection via
`_materialise_deferred_tab()`. Old-format entries (without
`method`/`name`) fall back to eager `_open_request()` for backward
compatibility. Deleted requests/collections are silently skipped.
Draft (unsaved) tabs are serialized with `type: "draft"` and an inline
snapshot of the editor state (`get_request_data()` + `draft_name`).
On restore, `_restore_draft()` calls `_open_draft_request()` and
replays the saved state into the editor.
9. **Manual tab reorder changes close-unchanged priority** — when the user
drags tabs into a new visible order, `_TabControllerMixin._on_tab_reordered`
rewrites `TabContext.opened_order` to match that order. The
`close_unchanged` limit policy then evicts the leftmost eligible,
unchanged tab instead of an older pre-drag ordering.
10. **VariablePopup uses class-level callbacks, not Qt signals** —
`VariablePopup` is a **singleton** `QFrame`. Its callbacks
(`set_save_callback`, `set_local_override_callback`,
`set_reset_local_override_callback`, `set_add_variable_callback`,
`set_has_environment`) are classmethods that store callables on the
**class itself**, not on an instance. They are wired once in
`MainWindow.__init__` and survive popup hide/show cycles.
9. **Saved response mutations are MainWindow-owned** —
`SavedResponsesPanel` is a read-only/browser widget. It never imports the
repository or service directly for mutations; it only emits signals to
`MainWindow`, which calls `CollectionService` and then refreshes the
sidebar state.
11. **Saved response mutations are MainWindow-owned** —
`SavedResponsesPanel` is a read-only/browser widget. It never imports the
repository or service directly for mutations; it only emits signals to
`MainWindow`, which calls `CollectionService` and then refreshes the
sidebar state.
14 changes: 14 additions & 0 deletions .github/instructions/pyside6.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,20 @@ standard object names:
| `sidebarSourceDot` | `QLabel` | Colour-coded variable source dot |
| `sidebarSeparator` | `QFrame` | Separator line in sidebar panels |

### QTabBar overflow scroll buttons

When a `QTabWidget` has more tabs than fit, Qt shows left/right
`QToolButton` scroll arrows inside the `QTabBar`. These are styled
globally in `global_qss.py` with:

- `background: input_bg`
- `border: 1px solid border` (sharp corners, `border-radius: 0`)
- `border-color: accent` on hover

Do **not** override or remove the default platform arrows. Do **not**
add `border-radius`, `bg_alt` hover fills, or `image: none` rules.
The global rule is unscoped — it applies to every `QTabBar` in the app.

### When inline setStyleSheet() is still acceptable

Only use `setStyleSheet()` for **dynamic per-instance** styling that
Expand Down
24 changes: 18 additions & 6 deletions .github/instructions/testing.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ applyTo: "tests/**/*.py"
full validation checklist.
3. **Each test gets a fresh SQLite database** — the `_fresh_db` autouse
fixture handles this. Never share DB state between tests.
4. **UI tests need `qapp` and `qtbot` fixtures.** Register widgets with
4. **Each test starts with cleared tab preferences** — the
`_reset_tab_settings` autouse fixture removes the `tabs/*` QSettings
group so preview/tab-limit settings never leak between cases.
5. **UI tests need `qapp` and `qtbot` fixtures.** Register widgets with
`qtbot.addWidget(widget)`.
5. **The `_no_fetch` fixture is autouse in `tests/ui/`** — it prevents
6. **The `_no_fetch` fixture is autouse in `tests/ui/`** — it prevents
`CollectionWidget` from spawning a background thread. You do not need
to apply it manually.
6. **Use bare module imports** (e.g. `from database.database import init_db`)
7. **Use bare module imports** (e.g. `from database.database import init_db`)
— `src/` is on the Python path.
7. **Do not test the session or engine directly** — test through the
8. **Do not test the session or engine directly** — test through the
repository or service layer.

## Fresh database per test (autouse fixture)
Expand All @@ -47,6 +50,13 @@ init_db(tmp_path / "test.db")
single `QApplication` instance. All UI tests must accept `qapp` and use
`qtbot.addWidget(widget)` for cleanup.

## Fresh QSettings tab preferences per test (autouse fixture)

`conftest.py` also provides `_reset_tab_settings`, which removes the
`tabs` QSettings group before every test. Use this when adding persisted
request-tab settings so one test cannot silently change preview or tab-limit
behaviour for the next.

## `_no_fetch` fixture — avoiding background threads in tests

`CollectionWidget.__init__` spawns a `QThread` that queries the database.
Expand Down Expand Up @@ -102,7 +112,7 @@ test file still exceeds 600 lines, split by test class into separate files.

```
tests/
├── conftest.py # Root: _fresh_db (autouse) + qapp (session)
├── conftest.py # Root: _fresh_db + _reset_tab_settings (autouse) + qapp
├── unit/ # Pure logic — no Qt widgets
│ ├── database/ # Repository layer tests
│ │ ├── test_repository.py
Expand All @@ -123,9 +133,11 @@ tests/
│ └── test_oauth2_service.py
└── ui/ # PySide6 widget tests (need qapp + qtbot)
├── conftest.py # _no_fetch (autouse) + helper functions
├── test_main_window.py # Top-level MainWindow smoke tests
├── test_main_window.py # Top-level MainWindow smoke tests
├── test_main_window_tabs_navigation.py # Wrapped tab deck shortcuts + search tests
├── test_main_window_save.py # SaveButton + RequestSaveEndToEnd tests
├── test_main_window_draft.py # Draft tab open/save lifecycle tests
├── test_main_window_session.py # Tab session persistence (save/restore) tests
├── styling/ # Theme and icon tests
│ ├── test_theme_manager.py
│ └── test_icons.py
Expand Down
Loading
Loading