Update documentation.

This commit is contained in:
2026-03-28 17:42:37 +05:30
parent 01662f7e0e
commit 79b120ff91
25 changed files with 109 additions and 109 deletions

View File

@@ -1,6 +1,6 @@
# APIClient - Agent
> **AI-first API testing desktop client** built with Python + PyQt6.
> **AI-first API testing desktop client** - built with Python + PyQt6.
> Specialised for the [EKIKA Odoo API Framework](https://apps.odoo.com/apps/modules/19.0/api_framework), but works with any REST, GraphQL, or WebSocket API.
---
@@ -8,31 +8,31 @@
## Features
### Core API Testing
- **Multi-tab request editor** work on multiple requests simultaneously, drag to reorder
- **All HTTP methods** GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
- **Smart params & headers table** per-row enable/disable checkboxes, 36 px comfortable rows, auto-expanding blank row
- **Body editor** raw JSON/XML/text with syntax highlighting, **Format JSON** button, `application/vnd.api+json` support
- **Auth panel** Bearer Token, Basic Auth, API Key (header or query)
- **Pre-request scripts** Python executed before each request; access `pm.environment.get/set`
- **Test scripts** assertions auto-run after every response; `pm.test(...)` / `expect(...)` DSL
- **Response viewer** syntax-highlighted body, headers table, test results, search, copy, save
- **WebSocket client** connect, send, receive, log messages
- **Mock server** local HTTP mock with configurable routes
- **Multi-tab request editor** - work on multiple requests simultaneously, drag to reorder
- **All HTTP methods** - GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
- **Smart params & headers table** - per-row enable/disable checkboxes, 36 px comfortable rows, auto-expanding blank row
- **Body editor** - raw JSON/XML/text with syntax highlighting, **Format JSON** button, `application/vnd.api+json` support
- **Auth panel** - Bearer Token, Basic Auth, API Key (header or query)
- **Pre-request scripts** - Python executed before each request; access `pm.environment.get/set`
- **Test scripts** - assertions auto-run after every response; `pm.test(...)` / `expect(...)` DSL
- **Response viewer** - syntax-highlighted body, headers table, test results, search, copy, save
- **WebSocket client** - connect, send, receive, log messages
- **Mock server** - local HTTP mock with configurable routes
### Collections & Environments
- **Collections sidebar** import/export Postman Collection v2.1 JSON, cURL
- **Environment variables** `{{base_url}}`, `{{api_key}}`, etc. resolved at send time; per-environment values
- **Collection runner** run all requests in a collection, view pass/fail results
- **History** every sent request automatically saved
- **Collections sidebar** - import/export Postman Collection v2.1 JSON, cURL
- **Environment variables** - `{{base_url}}`, `{{api_key}}`, etc. resolved at send time; per-environment values
- **Collection runner** - run all requests in a collection, view pass/fail results
- **History** - every sent request automatically saved
### AI Co-pilot (Claude-powered)
- **Persistent AI chat sidebar** toggle with the `✦ AI` button or `Ctrl+Shift+A`
- **Full context awareness** AI sees your current request (method, URL, headers, body, params, test scripts) and the last response (status, body, errors); secrets are automatically redacted
- **Streaming responses** tokens stream in real time
- **One-click Apply** AI suggestions come with **Apply Body**, **Apply Params**, **Apply Headers**, **Apply Test Script** buttons that set the values directly in the request editor
- **Multi-turn conversation** full history maintained per session; Clear to reset
- **Quick actions** Analyze, Fix Error, Gen Body, Write Tests, Auth Help, Explain Response
- **EKIKA Odoo collection generator** generate complete collections for JSON-API, REST JSON, GraphQL, and Custom REST JSON without spending AI tokens; supports all auth types
- **Persistent AI chat sidebar** - toggle with the `✦ AI` button or `Ctrl+Shift+A`
- **Full context awareness** - AI sees your current request (method, URL, headers, body, params, test scripts) and the last response (status, body, errors); secrets are automatically redacted
- **Streaming responses** - tokens stream in real time
- **One-click Apply** - AI suggestions come with **Apply Body**, **Apply Params**, **Apply Headers**, **Apply Test Script** buttons that set the values directly in the request editor
- **Multi-turn conversation** - full history maintained per session; Clear to reset
- **Quick actions** - Analyze, Fix Error, Gen Body, Write Tests, Auth Help, Explain Response
- **EKIKA Odoo collection generator** - generate complete collections for JSON-API, REST JSON, GraphQL, and Custom REST JSON without spending AI tokens; supports all auth types
### EKIKA Odoo API Framework specialisation
- Generates full CRUD + Execute / Export / Report / Fields / Access-Rights endpoints per model
@@ -73,14 +73,14 @@ pyinstaller>=6.0.0
## Quick Start
### 1 Send your first request
### 1 - Send your first request
1. Launch the app: `python main.py`
2. Type a URL in the bar, e.g. `https://jsonplaceholder.typicode.com/todos/1`
3. Press **Send** (or `Ctrl+Enter`)
4. See the JSON response with syntax highlighting in the bottom panel
### 2 Use environment variables
### 2 - Use environment variables
1. Click **Manage****New Environment** → name it `My API`
2. Add variables:
@@ -93,7 +93,7 @@ pyinstaller>=6.0.0
5. In Headers, add `Authorization: Bearer {{api_key}}`
6. Variables are resolved automatically at send time
### 3 Import a collection
### 3 - Import a collection
**From Postman export:**
1. `File → Import…`
@@ -109,11 +109,11 @@ curl -X POST https://api.example.com/v1/orders \
**From OpenAPI spec:**
1. `Tools → AI Assistant → Import from Docs`
2. Paste the OpenAPI JSON/YAML URL parsed instantly, no AI tokens used
2. Paste the OpenAPI JSON/YAML URL - parsed instantly, no AI tokens used
---
## EKIKA Odoo API Framework Complete Example
## EKIKA Odoo API Framework - Complete Example
### Generate a collection in 30 seconds
@@ -131,8 +131,8 @@ curl -X POST https://api.example.com/v1/orders \
| Models | `sale.order, res.partner, account.move` |
| Operations | ✓ List, Get, Create, Update, Delete |
4. Click **Generate Collection** preview appears instantly
5. Click **Import Both** collection + environment are saved
4. Click **Generate Collection** - preview appears instantly
5. Click **Import Both** - collection + environment are saved
This generates the following requests for each model with zero AI tokens:
@@ -200,7 +200,7 @@ Select **API Kind: GraphQL**. The generator creates:
---
## AI Chat Co-pilot Example Session
## AI Chat Co-pilot - Example Session
Click **✦ AI** in the top bar to open the sidebar. The AI automatically knows what request you have open and the last response.
@@ -227,7 +227,7 @@ AI: A 401 on the EKIKA JSON-API endpoint means the x-api-key header is
[ Apply Headers to Request ]
```
Click **Apply Headers to Request** headers are set immediately and the Headers tab opens.
Click **Apply Headers to Request** - headers are set immediately and the Headers tab opens.
### Generating a body for a complex model
@@ -305,8 +305,8 @@ APIClient-Agent/
│ ├── core/
│ │ ├── storage.py # SQLite persistence (collections, environments, history)
│ │ ├── http_client.py # httpx-based request engine, variable resolution
│ │ ├── ai_client.py # Claude API collection generation from docs
│ │ ├── ai_chat.py # Claude API multi-turn conversational co-pilot
│ │ ├── ai_client.py # Claude API - collection generation from docs
│ │ ├── ai_chat.py # Claude API - multi-turn conversational co-pilot
│ │ ├── openapi_parser.py # OpenAPI 3.x / Swagger 2.0 local parser
│ │ ├── ekika_odoo_generator.py# EKIKA Odoo framework collection generator
│ │ ├── test_runner.py # pm.test / expect assertion engine
@@ -345,7 +345,7 @@ Settings are stored in an SQLite database at `~/.apiclient_agent/data.db` (creat
2. In the app: `Tools → AI Assistant → Settings tab`
3. Paste the key and click **Save API Key**
The key is stored locally in the SQLite database only never transmitted except to the Anthropic API.
The key is stored locally in the SQLite database only - never transmitted except to the Anthropic API.
---
@@ -354,7 +354,7 @@ The key is stored locally in the SQLite database only — never transmitted exce
Some servers (especially demo/development instances) use self-signed certificates or wildcard certificates that don't match the exact hostname. If you see:
```
SSL certificate error could not connect to https://...
SSL certificate error - could not connect to https://...
Tip: disable SSL verification in the request Settings tab.
```
@@ -382,10 +382,10 @@ The executable is produced in `dist/APIClient-Agent`.
3. Commit your changes: `git commit -m "Add my feature"`
4. Push and open a pull request
Please keep UI styling in `theme.py` using `setObjectName()` selectors never inline `setStyleSheet()` for static colors.
Please keep UI styling in `theme.py` using `setObjectName()` selectors - never inline `setStyleSheet()` for static colors.
---
## License
[MIT License](LICENSE) Copyright (c) 2026 EKIKA.co
[MIT License](LICENSE) - Copyright (c) 2026 EKIKA.co

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Conversational AI co-pilot core."""
"""APIClient - Agent - Conversational AI co-pilot core."""
import json
import re
import httpx
@@ -11,9 +11,9 @@ You are APIClient - Agent, an expert AI API testing co-pilot embedded in the API
Your responsibilities:
• Help craft and debug HTTP requests (REST, JSON-API, GraphQL, Odoo APIs)
• Analyze HTTP responses status codes, headers, body structure, errors
• Analyze HTTP responses - status codes, headers, body structure, errors
• Specialize in the EKIKA Odoo API Framework:
- JSON-API (Content-Type: application/vnd.api+json) body format: {"data": {"type": model, "attributes": {...}}}
- JSON-API (Content-Type: application/vnd.api+json) - body format: {"data": {"type": model, "attributes": {...}}}
- REST JSON (Content-Type: application/json)
- GraphQL (POST with {"query": "..."} body)
- Auth: x-api-key header, Basic Auth, OAuth2 Bearer, JWT Bearer
@@ -45,7 +45,7 @@ pm.test('Has data', lambda: expect(pm.response.json()).to_have_key('data'))
```
Rules:
- Be concise and actionable explain WHY, not just WHAT
- Be concise and actionable - explain WHY, not just WHAT
- If you add apply blocks, briefly explain what each block does
- For JSON-API responses: data is in response.data, errors in response.errors
- For SSL cert errors: tell user to uncheck SSL verification in the Settings tab

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Claude AI integration."""
"""APIClient - Agent - Claude AI integration."""
import json
import re
import httpx
@@ -28,7 +28,7 @@ You are an expert API documentation analyzer for APIClient - Agent.
Given API documentation (which may be a spec, a web page, framework docs, or raw text),
extract or infer all useful API endpoints and return structured JSON.
Return ONLY valid JSON no markdown, no commentary, just the JSON object.
Return ONLY valid JSON - no markdown, no commentary, just the JSON object.
Schema:
{
@@ -74,7 +74,7 @@ Rules:
- If it is a GRAPHQL API, generate a POST /graphql endpoint with example query body
- If auth options are shown (API key, OAuth, Basic), include ALL variants as separate
environment variables so the user can choose
- Keep paths clean strip trailing slashes, normalise to lowercase
- Keep paths clean - strip trailing slashes, normalise to lowercase
"""
@@ -202,7 +202,7 @@ def fetch_url_content(url: str) -> str:
ct = resp.headers.get("content-type", "")
text = resp.text
# If HTML page strip tags for cleaner AI input
# If HTML page - strip tags for cleaner AI input
if "html" in ct and not _looks_like_spec(text):
text = _strip_html(text)

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Code snippet generators."""
"""APIClient - Agent - Code snippet generators."""
import json
from urllib.parse import urlencode

View File

@@ -1,4 +1,4 @@
"""EKIKA Odoo API Framework Direct collection generator.
"""EKIKA Odoo API Framework - Direct collection generator.
Generates complete Postman-style collections from the EKIKA api_framework module
without requiring any AI API calls. All URL patterns, body formats, auth headers,
@@ -87,7 +87,7 @@ def _env_vars(instance_url: str, auth_type: str, extra: dict = None) -> dict:
def _clean_endpoint(endpoint: str) -> str:
"""Normalise endpoint slug ensure leading slash, strip trailing slash."""
"""Normalise endpoint slug - ensure leading slash, strip trailing slash."""
ep = endpoint.strip().strip("/")
return f"/{ep}" if ep else "/api"
@@ -281,7 +281,7 @@ def _build_jsonapi_endpoints(base_ep: str, model: str, headers: dict,
if "Get Fields" in operations:
eps.append({
"name": f"Get Fields {model}",
"name": f"Get Fields - {model}",
"method": "GET",
"path": f"{ep_path}/fields_get",
"headers": {**headers, "Accept": ct},
@@ -295,7 +295,7 @@ def _build_jsonapi_endpoints(base_ep: str, model: str, headers: dict,
if "Check Access Rights" in operations:
eps.append({
"name": f"Check Access {model}",
"name": f"Check Access - {model}",
"method": "GET",
"path": f"{ep_path}/check_access_rights",
"headers": {**headers, "Accept": ct},
@@ -405,7 +405,7 @@ def _build_restjson_endpoints(base_ep: str, model: str, headers: dict,
if "Get Fields" in operations:
eps.append({
"name": f"Get Fields {model}",
"name": f"Get Fields - {model}",
"method": "GET",
"path": f"{ep_path}/fields_get",
"headers": {**headers},
@@ -442,7 +442,7 @@ def _build_graphql_endpoints(base_ep: str, model: str, headers: dict,
f"}}"
)
eps.append({
"name": f"GraphQL List {model}",
"name": f"GraphQL - List {model}",
"method": "POST",
"path": path,
"headers": {**headers, "Content-Type": ct},
@@ -465,7 +465,7 @@ def _build_graphql_endpoints(base_ep: str, model: str, headers: dict,
f"}}"
)
eps.append({
"name": f"GraphQL Get {model} by ID",
"name": f"GraphQL - Get {model} by ID",
"method": "POST",
"path": path,
"headers": {**headers, "Content-Type": ct},
@@ -491,7 +491,7 @@ def _build_graphql_endpoints(base_ep: str, model: str, headers: dict,
f"}}"
)
eps.append({
"name": f"GraphQL Create {model}",
"name": f"GraphQL - Create {model}",
"method": "POST",
"path": path,
"headers": {**headers, "Content-Type": ct},
@@ -518,7 +518,7 @@ def _build_graphql_endpoints(base_ep: str, model: str, headers: dict,
f"}}"
)
eps.append({
"name": f"GraphQL Update {model}",
"name": f"GraphQL - Update {model}",
"method": "POST",
"path": path,
"headers": {**headers, "Content-Type": ct},
@@ -539,7 +539,7 @@ def _build_graphql_endpoints(base_ep: str, model: str, headers: dict,
f"}}"
)
eps.append({
"name": f"GraphQL Delete {model}",
"name": f"GraphQL - Delete {model}",
"method": "POST",
"path": path,
"headers": {**headers, "Content-Type": ct},
@@ -587,7 +587,7 @@ def generate_collection(
all_endpoints += _build_restjson_endpoints(base_ep, model, headers, operations)
elif api_kind == "GraphQL":
all_endpoints += _build_graphql_endpoints(base_ep, model, headers, operations)
else: # Custom REST JSON same as REST JSON
else: # Custom REST JSON - same as REST JSON
all_endpoints += _build_restjson_endpoints(base_ep, model, headers, operations)
# Build URLs using {{base_url}} variable
@@ -595,7 +595,7 @@ def generate_collection(
if not ep["path"].startswith("http"):
ep["url"] = f"{{{{base_url}}}}{ep['path']}"
name = collection_name or f"EKIKA Odoo {api_kind} {', '.join(models[:3])}"
name = collection_name or f"EKIKA Odoo - {api_kind} - {', '.join(models[:3])}"
return {
"collection_name": name,

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent HTTP client engine."""
"""APIClient - Agent - HTTP client engine."""
import re
import base64
from copy import deepcopy
@@ -144,18 +144,18 @@ def send_request(req: HttpRequest, variables: dict = None) -> HttpResponse:
detail = str(e)
if "CERTIFICATE_VERIFY_FAILED" in detail or "certificate" in detail.lower() or "SSL" in detail:
return HttpResponse(error=(
f"SSL certificate error could not connect to {r.url}\n\n"
f"SSL certificate error - could not connect to {r.url}\n\n"
f"The server's certificate is not trusted or doesn't match the hostname.\n"
f"Tip: disable SSL verification in the request Settings tab."
))
return HttpResponse(error=f"Connection refused could not reach {r.url}")
return HttpResponse(error=f"Connection refused - could not reach {r.url}")
except httpx.ConnectTimeout:
return HttpResponse(error=f"Connection timed out after {req.timeout}s")
except httpx.ReadTimeout:
return HttpResponse(error=f"Read timed out server took too long to respond")
return HttpResponse(error=f"Read timed out - server took too long to respond")
except httpx.SSLError as e:
return HttpResponse(error=f"SSL error: {e}. Disable SSL verification if using a self-signed cert.")
except httpx.TooManyRedirects:
return HttpResponse(error="Too many redirects possible redirect loop")
return HttpResponse(error="Too many redirects - possible redirect loop")
except Exception as e:
return HttpResponse(error=str(e))

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Lightweight HTTP mock server."""
"""APIClient - Agent - Lightweight HTTP mock server."""
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent OpenAPI / Swagger spec parser.
"""APIClient - Agent - OpenAPI / Swagger spec parser.
Parses OpenAPI 3.x and Swagger 2.0 specs (JSON or YAML) directly,
without needing AI tokens.

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Storage layer (SQLite)."""
"""APIClient - Agent - Storage layer (SQLite)."""
import json
import sqlite3
from pathlib import Path

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Core data models."""
"""APIClient - Agent - Core data models."""
from dataclasses import dataclass, field
from typing import Optional

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent AI chat sidebar panel (persistent, context-aware)."""
"""APIClient - Agent - AI chat sidebar panel (persistent, context-aware)."""
import re
from PyQt6.QtWidgets import (
@@ -124,7 +124,7 @@ class MessageBubble(QFrame):
self._text_lbl.setText(self._full_text)
def finalize(self):
"""Called when streaming ends strip apply blocks and render them."""
"""Called when streaming ends - strip apply blocks and render them."""
if self._finalized:
return
self._finalized = True

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent AI Assistant Dialog."""
"""APIClient - Agent - AI Assistant Dialog."""
import json
from PyQt6.QtWidgets import (
@@ -43,7 +43,7 @@ class AnalysisWorker(QThread):
self.progress.emit("Checking for OpenAPI/Swagger spec…")
spec = openapi_parser.detect_spec(content)
if spec:
self.progress.emit("OpenAPI spec detected parsing directly…")
self.progress.emit("OpenAPI spec detected - parsing directly…")
result = openapi_parser.parse_spec(spec)
if self.base_url:
result["base_url"] = self.base_url
@@ -136,7 +136,7 @@ class AIAssistantDialog(QDialog):
layout.addWidget(footer)
# ══════════════════════════════════════════════════════════════════════════
# Tab 1 EKIKA Odoo API Framework (dedicated, no AI tokens needed)
# Tab 1 - EKIKA Odoo API Framework (dedicated, no AI tokens needed)
# ══════════════════════════════════════════════════════════════════════════
def _build_ekika_tab(self) -> QWidget:
@@ -303,7 +303,7 @@ class AIAssistantDialog(QDialog):
self.ek_preview.setFont(QFont("JetBrains Mono, Fira Code, Consolas", 10))
self.ek_preview.setPlaceholderText(
"Fill in the form above and click Generate Collection to preview.\n\n"
"No API key required collection is generated instantly from the\n"
"No API key required - collection is generated instantly from the\n"
"EKIKA Odoo API Framework documentation."
)
self.ek_preview.setMaximumHeight(180)
@@ -380,7 +380,7 @@ class AIAssistantDialog(QDialog):
self.ek_import_btn.setEnabled(True)
self.ek_env_btn.setEnabled(True)
self.ek_both_btn.setEnabled(True)
self.status_label.setText(f"{len(eps)} endpoint(s) ready click Import to save")
self.status_label.setText(f"{len(eps)} endpoint(s) ready - click Import to save")
def _ekika_import(self):
if not self._result:
@@ -399,7 +399,7 @@ class AIAssistantDialog(QDialog):
self._do_create_env(self._result)
# ══════════════════════════════════════════════════════════════════════════
# Tab 2 Generic AI analysis (OpenAPI / any docs URL)
# Tab 2 - Generic AI analysis (OpenAPI / any docs URL)
# ══════════════════════════════════════════════════════════════════════════
def _build_generic_tab(self) -> QWidget:
@@ -543,10 +543,10 @@ class AIAssistantDialog(QDialog):
lines = [
f"✓ Parsed via: {src_label}",
f"✓ Collection: {result.get('collection_name', 'Unnamed')}",
f"✓ Base URL: {result.get('base_url', '')}",
f"✓ Base URL: {result.get('base_url', '-')}",
f"✓ Auth type: {result.get('auth_type', 'none')}",
f"✓ Endpoints: {len(endpoints)} found",
f"✓ Env vars: {list(env_vars.keys()) or ''}",
f"✓ Env vars: {list(env_vars.keys()) or '-'}",
]
if notes:
lines += ["", "── Notes ─────────────────", notes]
@@ -565,7 +565,7 @@ class AIAssistantDialog(QDialog):
self.analyze_btn.setEnabled(True)
self.progress_bar.setVisible(False)
self.result_view.setPlainText(f"✗ Error:\n\n{msg}")
self.status_label.setText("Error see results panel")
self.status_label.setText("Error - see results panel")
def _set_generic_action_buttons(self, enabled: bool):
self.import_btn.setEnabled(enabled)
@@ -586,7 +586,7 @@ class AIAssistantDialog(QDialog):
self._do_create_env(self._generic_result)
# ══════════════════════════════════════════════════════════════════════════
# Tab 3 Settings
# Tab 3 - Settings
# ══════════════════════════════════════════════════════════════════════════
def _build_settings_tab(self) -> QWidget:
@@ -598,7 +598,7 @@ class AIAssistantDialog(QDialog):
hint = QLabel(
"EKIKA AI Assistant uses Claude by Anthropic to analyze plain-text API documentation.\n"
"OpenAPI/Swagger specs and EKIKA Odoo Framework collections are generated locally "
"OpenAPI/Swagger specs and EKIKA Odoo Framework collections are generated locally - "
"no API key required for those."
)
hint.setObjectName("hintText")
@@ -683,7 +683,7 @@ class AIAssistantDialog(QDialog):
def _do_create_env(self, result: dict):
env_vars = result.get("environment_variables", {})
col_name = result.get("collection_name", "AI Import")
env_name = f"{col_name} Environment"
env_name = f"{col_name} - Environment"
if not env_vars:
QMessageBox.information(self, "No Variables", "No environment variables detected.")

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Code Generation Dialog."""
"""APIClient - Agent - Code Generation Dialog."""
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QComboBox,
QTextEdit, QPushButton, QLabel, QApplication, QWidget

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Collection Runner dialog."""
"""APIClient - Agent - Collection Runner dialog."""
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QTreeWidget, QTreeWidgetItem, QComboBox, QHeaderView, QProgressBar, QWidget
@@ -168,7 +168,7 @@ class CollectionRunnerDialog(QDialog):
status_str = str(result.status)
row_color = Colors.SUCCESS if result.status < 400 else Colors.ERROR
test_str = f"{passed}/{total}" if total > 0 else ""
test_str = f"{passed}/{total}" if total > 0 else "-"
item = QTreeWidgetItem([
f"{result.method} {result.request_name}",
status_str,
@@ -189,6 +189,6 @@ class CollectionRunnerDialog(QDialog):
def _on_finished(self):
self.run_btn.setEnabled(True)
self.summary_label.setText(
f"Completed: {self._done} request(s) "
f"Completed: {self._done} request(s) - "
f"Tests: {self._passed_tests}/{self._total_tests} passed"
)

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Environment Manager Dialog."""
"""APIClient - Agent - Environment Manager Dialog."""
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem,
QPushButton, QTableWidget, QTableWidgetItem, QHeaderView,

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Import Dialog."""
"""APIClient - Agent - Import Dialog."""
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, QWidget,
QTextEdit, QPushButton, QLabel, QFileDialog, QMessageBox
@@ -76,7 +76,7 @@ class ImportDialog(QDialog):
layout.setContentsMargins(16, 12, 16, 12)
layout.setSpacing(8)
hint = QLabel("Paste a cURL command it will open as a new request tab:")
hint = QLabel("Paste a cURL command - it will open as a new request tab:")
hint.setObjectName("hintText")
layout.addWidget(hint)

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Main Window."""
"""APIClient - Agent - Main Window."""
from PyQt6.QtWidgets import (
QMainWindow, QSplitter, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QComboBox, QPushButton, QStatusBar, QTabWidget,
@@ -147,7 +147,7 @@ class MainWindow(QMainWindow):
self.chat_panel = AIChatPanel()
splitter.addWidget(self.chat_panel)
splitter.setSizes([260, 940, 360]) # give chat panel real size first
self.chat_panel.hide() # THEN hide splitter remembers 360
self.chat_panel.hide() # THEN hide - splitter remembers 360
self._main_splitter = splitter
# Wire apply signals
@@ -163,7 +163,7 @@ class MainWindow(QMainWindow):
self._status_bar = QStatusBar()
self._status_bar.setFixedHeight(26)
self.setStatusBar(self._status_bar)
self._status_bar.showMessage(f"Ready {APP_NAME} v{APP_VERSION}")
self._status_bar.showMessage(f"Ready - {APP_NAME} v{APP_VERSION}")
def _build_http_workspace(self) -> QWidget:
w = QWidget()

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Mock Server Panel."""
"""APIClient - Agent - Mock Server Panel."""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
QTableWidget, QTableWidgetItem, QHeaderView, QDialog,

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Request Panel."""
"""APIClient - Agent - Request Panel."""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QComboBox, QLineEdit,
QPushButton, QTabWidget, QTableWidget, QTableWidgetItem,
@@ -227,7 +227,7 @@ class RequestPanel(QWidget):
self.url_input = QLineEdit()
self.url_input.setObjectName("urlBar")
self.url_input.setPlaceholderText("Enter URL e.g. https://api.example.com/v1/users")
self.url_input.setPlaceholderText("Enter URL - e.g. https://api.example.com/v1/users")
self.url_input.returnPressed.connect(self._send)
self.send_btn = QPushButton("Send")
@@ -359,7 +359,7 @@ class RequestPanel(QWidget):
# ── Slots ────────────────────────────────────────────────────────────────
def _on_method_changed(self, method: str):
# Inline style is intentional here color is dynamic per method value
# Inline style is intentional here - color is dynamic per method value
color = method_color(method)
self.method_combo.setStyleSheet(f"QComboBox#methodCombo {{ color: {color}; }}")
@@ -377,7 +377,7 @@ class RequestPanel(QWidget):
parsed = json.loads(text)
self.body_editor.setPlainText(json.dumps(parsed, indent=2, ensure_ascii=False))
except json.JSONDecodeError:
pass # not valid JSON leave as-is
pass # not valid JSON - leave as-is
def _send(self):
self.send_requested.emit(self._build_request())

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Response Panel."""
"""APIClient - Agent - Response Panel."""
import json
from PyQt6.QtWidgets import (
@@ -25,13 +25,13 @@ def _fmt_size(n: int) -> str:
class StatusBadge(QLabel):
def __init__(self, parent=None):
super().__init__("", parent)
super().__init__("-", parent)
self.setFixedHeight(26)
self._apply_style(Colors.TEXT_MUTED)
self.setFont(QFont("Segoe UI", 11, QFont.Weight.Bold))
def _apply_style(self, color: str):
# Inline style intentional badge color is dynamic per status code
# Inline style intentional - badge color is dynamic per status code
self.setStyleSheet(f"""
QLabel {{
color: {color};
@@ -53,7 +53,7 @@ class StatusBadge(QLabel):
self._apply_style(Colors.ERROR)
def clear(self):
self.setText("")
self.setText("-")
self._apply_style(Colors.TEXT_MUTED)
@@ -174,8 +174,8 @@ class ResponsePanel(QWidget):
self._loading_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
ll.addWidget(self._loading_label)
self._stack.addWidget(self.tabs) # index 0 normal view
self._stack.addWidget(loading_widget) # index 1 loading
self._stack.addWidget(self.tabs) # index 0 - normal view
self._stack.addWidget(loading_widget) # index 1 - loading
layout.addWidget(self._stack, 1)
@@ -203,7 +203,7 @@ class ResponsePanel(QWidget):
size = resp.size_bytes or len(resp.body.encode())
self.size_label.setText(_fmt_size(size))
# Body pretty-print JSON if possible
# Body - pretty-print JSON if possible
try:
parsed = json.loads(resp.body)
self.body_view.setPlainText(json.dumps(parsed, indent=2, ensure_ascii=False))

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Request Search Dialog."""
"""APIClient - Agent - Request Search Dialog."""
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLineEdit,
QPushButton, QListWidget, QListWidgetItem, QLabel, QWidget

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Collections Sidebar."""
"""APIClient - Agent - Collections Sidebar."""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QTreeWidget, QTreeWidgetItem,
QPushButton, QInputDialog, QMenu, QLineEdit, QLabel, QMessageBox

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent Multi-tab request manager."""
"""APIClient - Agent - Multi-tab request manager."""
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QTabWidget, QPushButton, QTabBar
from PyQt6.QtCore import pyqtSignal, Qt

View File

@@ -1,7 +1,7 @@
"""
APIClient - Agent Central Theme Engine
APIClient - Agent - Central Theme Engine
All styling lives here in the global QSS.
UI widgets use setObjectName() selectors never inline setStyleSheet() for static colors.
UI widgets use setObjectName() selectors - never inline setStyleSheet() for static colors.
Only truly dynamic values (per-request method color, status badge) stay inline.
"""
from PyQt6.QtGui import QColor, QPalette

View File

@@ -1,4 +1,4 @@
"""APIClient - Agent WebSocket client panel."""
"""APIClient - Agent - WebSocket client panel."""
import asyncio
import queue
import time