Initial release — APIClient - Agent v2.0.0

AI-first API testing desktop client built with Python + PyQt6.

Features:
- Multi-tab HTTP request editor with params/headers/body/auth/tests
- KeyValueTable with per-row enable/disable checkboxes and 36px rows
- Format JSON button, syntax highlighting, pre-request & test scripts
- Collections, environments, history, import/export (Postman v2.1, cURL)
- OpenAPI 3.x / Swagger 2.0 local parser (no AI tokens)
- EKIKA Odoo API Framework generator — JSON-API, REST JSON, GraphQL,
  Custom REST JSON with all auth types (instant, no AI tokens)
- Persistent AI chat sidebar (Claude-powered co-pilot) with streaming,
  context-aware suggestions, and one-click Apply to request editor
- AI collection generator from any docs URL or pasted spec
- WebSocket client, Mock server, Collection runner, Code generator
- Dark/light theme engine (global QSS, object-name selectors)
- SSL error detection with actionable hints
- MIT License

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-28 17:34:18 +05:30
parent 1dbbb4320b
commit 01662f7e0e
37 changed files with 7822 additions and 1 deletions

97
app/ui/tabs_manager.py Normal file
View File

@@ -0,0 +1,97 @@
"""APIClient - Agent — Multi-tab request manager."""
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QTabWidget, QPushButton, QTabBar
from PyQt6.QtCore import pyqtSignal, Qt
from app.ui.request_panel import RequestPanel
from app.models import HttpRequest
class RequestTab(QWidget):
send_requested = pyqtSignal(object)
def __init__(self, parent=None):
super().__init__(parent)
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
self.request_panel = RequestPanel()
self.request_panel.send_requested.connect(self.send_requested)
layout.addWidget(self.request_panel)
def load_request(self, req: HttpRequest):
self.request_panel.load_request(req)
def get_request(self) -> HttpRequest:
return self.request_panel.get_request()
class TabsManager(QWidget):
send_requested = pyqtSignal(object)
current_tab_changed = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
self.tab_widget = QTabWidget()
self.tab_widget.setTabsClosable(False) # we manage close buttons ourselves
self.tab_widget.setMovable(True)
self.tab_widget.currentChanged.connect(lambda _: self.current_tab_changed.emit())
# "+" new tab button in the corner
new_btn = QPushButton("+")
new_btn.setObjectName("ghost")
new_btn.setFixedSize(28, 28)
new_btn.setToolTip("New Tab (Ctrl+T)")
new_btn.clicked.connect(lambda: self.new_tab())
self.tab_widget.setCornerWidget(new_btn, Qt.Corner.TopRightCorner)
layout.addWidget(self.tab_widget)
self._tab_counter = 0
self.new_tab()
# ── Public API ────────────────────────────────────────────────────────────
def new_tab(self, req: HttpRequest = None) -> RequestTab:
tab = RequestTab()
tab.send_requested.connect(self.send_requested)
if req:
tab.load_request(req)
self._tab_counter += 1
label = req.name if (req and req.name) else f"Request {self._tab_counter}"
idx = self.tab_widget.addTab(tab, label)
self.tab_widget.setCurrentIndex(idx)
self._add_close_button(idx, tab)
return tab
def _add_close_button(self, idx: int, tab: RequestTab):
btn = QPushButton("×")
btn.setObjectName("tabCloseBtn")
btn.setFixedSize(18, 18)
btn.setToolTip("Close Tab")
btn.clicked.connect(lambda: self._close_tab(self.tab_widget.indexOf(tab)))
self.tab_widget.tabBar().setTabButton(idx, QTabBar.ButtonPosition.RightSide, btn)
def close_current_tab(self):
self._close_tab(self.tab_widget.currentIndex())
def _close_tab(self, index: int):
if self.tab_widget.count() > 1:
self.tab_widget.removeTab(index)
def current_tab(self) -> RequestTab | None:
w = self.tab_widget.currentWidget()
return w if isinstance(w, RequestTab) else None
def load_request_in_new_tab(self, req: HttpRequest):
self.new_tab(req)
def load_request_in_current_tab(self, req: HttpRequest):
tab = self.current_tab()
if tab:
tab.load_request(req)
def rename_current_tab(self, name: str):
self.tab_widget.setTabText(self.tab_widget.currentIndex(), name)