What I built
For Hacktoberfest 2026 Week 2's Side Hustle challenge, I built SecureLocal AI: an offline-first desktop tool for a first-pass review of Python source code.
The scanner uses Python's built-in AST to flag seven heuristic patterns: dynamic execution, unsafe pickle deserialization, shell-enabled subprocess calls, possible hardcoded credentials, weak hashes, disabled TLS verification in recognized HTTP clients, and dynamically constructed SQL. It scans selected .py files without importing or executing the project, and its findings remain deterministic whether or not a language model is installed.
I tested the application end to end during development. This is an early working tool—not a claim of customer adoption or proven commercial demand.
Who might pay for it?
The plausible buyer is someone who needs a low-friction first review but cannot or does not want to upload source code to a hosted scanner: an independent Python consultant handling client repositories, or a small development team that wants a local triage step before human review. A paid offering might eventually be useful as a supported desktop workflow or team setup, but that is a market hypothesis, not a validated business. I have no paying customers, customer feedback, or commercial validation to report.
Why open innovation matters here
The static scanner is useful on its own. When a developer wants more context, SecureLocal AI can ask a locally running Ollama service to explain one selected finding using the open-weight Qwen2.5-Coder 3B model (qwen2.5-coder:3b). The model is optional; the app does not download it automatically. Install Ollama, start its local service, and pull the model yourself:
ollama pull qwen2.5-coder:3b
python app.py
The client defaults to http://127.0.0.1:11434 and rejects non-loopback endpoints. A normal scan makes no HTTP/API requests. Only after the user explicitly asks for an explanation does the app send a small, redacted finding context to local Ollama—not the project, source file, file path, or report. The model adds advice; it does not set severity, change the deterministic finding, edit files, or apply a fix. Its explanation is unverified and should be checked independently.
Running inference locally avoids per-request cloud-model charges and means source files do not need to leave the machine for this workflow. It is not cost-free: the user supplies the compute, electricity, storage, and initial model download, and local performance depends on their hardware. I have not measured or benchmarked it. Keeping the model choice and inference on the developer's machine also gives users more control than making a hosted model API a required dependency.
How the app works
The Tkinter interface lets me choose a project folder, start a scan, filter results by severity or text, and inspect a finding's rule, severity, confidence, location, redacted evidence, limitations, and remediation guidance. Findings can be exported as JSON or Markdown. Reports include the findings and relative paths, with credential-like evidence redacted; they do not include complete source files.
File discovery and scanning have safety limits and skip symbolic links, generated directories, oversized files, malformed source, and files that cannot be safely read. The app does not write to the scanned project. These controls reduce accidental exposure and unbounded work, but they do not protect against a compromised operating system or malicious local software. Review any report before sharing it.
Demo: screenshots from the repository
SecureLocal AI is a desktop application, so these existing screenshots are the demo rather than a hosted web instance.
Vulnerable scan and finding list
Clean fixture scan
Finding inspection
Optional local-model explanation
The image files are committed under docs/screenshots/. The image links above use their raw GitHub URLs so DEV can render the existing assets.
Testing and limitations
A recorded development result was 51 tests passing, exercising the scanner, rules and redaction, file discovery, exports, Ollama client behavior, and UI paths. The suite includes synthetic fixtures and mocked model responses; passing tests is evidence that those tested behaviors worked, not proof of complete security coverage or real-world detection accuracy.
The rules are heuristics: they can miss vulnerabilities and flag safe code, and they do not establish whether a value is attacker-controlled. This is Python-only first-pass triage, not full data-flow analysis, dependency scanning, a clean bill of health, or a replacement for review by a security professional.
Project and challenge
- Repository: github.com/kushal-pandey/securelocal-ai (MIT licensed)
-
Run locally: Python 3.11+ and Tkinter;
python app.py. Ollama and Qwen2.5-Coder 3B are optional and only needed for AI explanations. The application itself uses Python's standard library. - Challenge: Hacktoberfest Open-Source AI Challenge: Week 2 — Side Hustle
- Agent session:
attachment:Pasted text #1
{
"path": "c:\\Users\\Kushal\\AppData\\Roaming\\Code\\agentSessionData\\d44184ed-dfb2-4d6f-af7c-6d9b6e7fa524\\attachments\\8ce6204b-ebe2-4657-8262-bb48d712d66f\\Pasted text #1.txt",
"view_range": [
1,
260
]
}
diff --git a/c:/Users/Kushal/AppData/Roaming/Code/agentSessionData/d44184ed-dfb2-4d6f-af7c-6d9b6e7fa524/attachments/8ce6204b-ebe2-4657-8262-bb48d712d66f/Pasted text #1.txt b/c:/Users/Kushal/AppData/Roaming/Code/agentSessionData/d44184ed-dfb2-4d6f-af7c-6d9b6e7fa524/attachments/8ce6204b-ebe2-4657-8262-bb48d712d66f/Pasted text #1.txt
index 0000000..0000000 100644
--- a/c:/Users/Kushal/AppData/Roaming/Code/agentSessionData/d44184ed-dfb2-4d6f-af7c-6d9b6e7fa524/attachments/8ce6204b-ebe2-4657-8262-bb48d712d66f/Pasted text #1.txt
+++ b/c:/Users/Kushal/AppData/Roaming/Code/agentSessionData/d44184ed-dfb2-4d6f-af7c-6d9b6e7fa524/attachments/8ce6204b-ebe2-4657-8262-bb48d712d66f/Pasted text #1.txt
@@ -1,260 +1,260 @@
# Task: Professionally redesign the existing SecureLocal AI Tkinter frontend
You are working on my existing Python project, **SecureLocal AI — Offline-First Code Security Auditor & Remediation Assistant**.
I want you to upgrade the visual design of the existing Tkinter desktop application so that it looks like a polished, professional developer security product suitable for a project challenge submission and portfolio demonstration.
## 1. First, inspect the existing project
Before editing anything:
1. Inspect the project structure, especially `app.py`, everything under `ui/`, and the existing tests.
2. Identify which files implement:
- The main application window and layout.
- Project-folder selection and scanning controls.
- The findings table, severity filters, and text search.
- Finding details and remediation guidance.
- The optional local Ollama model selector and explanation dialog.
- JSON and Markdown report exports.
3. Understand how these components communicate with the scanner, reports, and Ollama client.
4. Identify any existing shared styling helpers and reusable widgets.
5. Report the files you intend to modify and the planned changes before implementing the redesign.
Do not perform a broad rewrite. Reuse the existing architecture and working components.
## 2. Current problems to address
The existing interface works, but it looks like a basic default Tkinter utility:
- Plain gray background and default-looking controls.
- Weak visual hierarchy and inconsistent spacing.
- Project selection, scan controls, filters, AI controls, and export actions lack clear grouping.
- Severity levels are displayed as plain text.
- Findings table is visually dense.
- Finding details use a plain monospaced text area with insufficient visual organization.
- The local AI experience looks disconnected from the main application.
- The overall interface needs a consistent, professional design.
## 3. Target design
Create a modern, light-themed security workbench using a restrained palette:
- Deep navy for branding and important structural elements.
- White or very light gray for application surfaces.
- A subtle blue accent for primary actions and selection.
- Muted gray for secondary text and borders.
- Carefully chosen semantic colors for severity levels.
- Consistent typography, spacing, padding, and alignment.
The result should look intentional and professional rather than flashy.
Use the existing Tkinter stack. You may use `ttk` styles and reusable styling helpers. Do not introduce a web frontend or migrate to another GUI framework.
Do not add third-party Python dependencies unless absolutely necessary. Prefer Python's standard library and existing project dependencies.
## 4. Implement these improvements
### A. Application header
- Give the application a clear, polished title area.
- Retain the SecureLocal AI name and a short description of its purpose.
- Use consistent padding and typography.
- Organize the interface into visually distinct sections.
- Make the main window resize gracefully.
### B. Project selection and scanning
- Group the project path, folder-selection button, and Scan Project action into a coherent section.
- Make the primary scan action visually prominent.
- Keep the selected path readable and avoid unnecessary horizontal clutter.
- Preserve existing folder-selection and scanning behavior.
- Retain appropriate disabled/loading states if already implemented.
### C. Severity summary cards
After a scan, display compact summary cards for:
- Total findings.
- High severity.
- Medium severity.
- Low severity.
If informational findings are represented in the existing data model, preserve and display their count appropriately without overcrowding the interface.
Requirements:
- Calculate counts from the actual current scan results.
- Update the cards after every scan.
- Handle clean scans and empty results correctly.
- Do not invent or duplicate metrics.
- Keep counts consistent with the active scan results, not merely the filtered table.
### D. Findings toolbar and table
- Place severity filtering and text search in a clean toolbar.
- Improve the table's row height, column widths, alignment, selection colors, and readability.
- Use subtle alternating row backgrounds if compatible with the existing Treeview implementation.
- Make severity easy to distinguish using restrained color styling or clear visual indicators.
- Keep rule IDs, filenames, line numbers, and finding titles readable.
- Preserve existing filtering, selection, sorting (if implemented), and finding-detail behavior.
- Ensure the table handles long filenames and finding titles gracefully.
- Make the table resize sensibly when the window is resized.
- Avoid adding decorative columns that reduce useful information.
### E. Finding details and remediation
Redesign the finding-details section into a clear, readable panel.
Where the existing finding data supports it, distinguish:
- Finding title.
- Severity and rule ID.
- File location and line number.
- Why the issue matters.
- Redacted evidence.
- Confidence.
- Remediation guidance.
- Rule limitations.
Use readable typography, consistent spacing, sensible text wrapping, and appropriate scrolling.
Do not expose unredacted secrets or raw source code that the current implementation intentionally excludes.
Preserve the existing finding-selection behavior and all existing detail information.
### F. Local AI experience
Make the local AI controls feel integrated with the rest of the interface.
- Clearly label the model selector.
- Style the Explain Selected with Local AI action consistently with other buttons.
- Preserve the existing model configuration and request flow.
- Keep AI explanations clearly separate from deterministic scanner findings.
- Retain a visible warning that AI-generated advice is unverified.
- Ensure long responses remain readable and scrollable.
- Preserve loading, timeout, unavailable-service, missing-model, and malformed-response error handling.
- Do not suppress genuine errors or make the interface appear successful when the request fails.
The Ollama client currently uses a 120-second request timeout. Preserve this configuration unless you discover a specific technical reason to change it.
Do not send additional source code, project paths, or secrets to Ollama as part of the redesign.
### G. Report exports
- Visually group the JSON and Markdown export actions.
- Preserve the existing export behavior, file dialogs, output formats, and redaction safeguards.
- Ensure the buttons remain accessible at different window sizes.
### H. Consistency and usability
- Apply a consistent styling system across the application.
- Use clear button states and readable text contrast.
- Preserve keyboard and mouse usability.
- Ensure all important controls remain visible or reachable when resizing the window.
- Use scrollable containers where needed.
- Avoid excessive nested frames, tiny fonts, excessive colors, and unnecessary decoration.
- Do not rely on emoji as the primary visual language of a security tool.
## 5. Strict preservation requirements
This is a visual redesign, not a scanner rewrite.
**Do not:**
- Change the seven existing security detection rules or their semantics.
- Change finding IDs, severity logic, confidence logic, or evidence redaction.
- Weaken endpoint validation or Ollama security controls.
- Change scanner limits, file-discovery behavior, or symlink protections.
- Modify report schemas or remove report metadata.
- Execute or import scanned source files.
- Remove existing functionality to simplify the layout.
- Break asynchronous scanning or AI execution.
- Introduce network calls, cloud AI services, automatic model downloads, or telemetry.
- Modify project files outside the scope of the redesign without a clear reason.
- Replace real functionality with mock data.
- claim tests pass unless you actually run them and see the result.
Preserve the current offline-first privacy model.
## 6. Implementation approach
Work incrementally:
1. Inspect and map the existing UI implementation.
2. Explain the proposed file changes.
3. Implement the shared styles and main layou
{
"path": "c:\\Projects\\securelocal-ai\\README.md",
"view_range": [
1,
260
]
}
diff --git a/c:/Projects/securelocal-ai/README.md b/c:/Projects/securelocal-ai/README.md
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/README.md
+++ b/c:/Projects/securelocal-ai/README.md
@@ -1,260 +1,260 @@
# SecureLocal AI
**An offline-first Python code security auditor and remediation assistant.**
SecureLocal AI is a desktop application that helps developers identify common security issues in Python source code without sending their source files to a cloud-based scanning service. It combines deterministic Abstract Syntax Tree (AST) analysis with optional, explicitly requested explanations from a locally running Ollama model.
> **Disclaimer:** SecureLocal AI is an early-stage, heuristic security scanner. A clean scan does not prove that code is secure, and this tool does not replace manual code review or a professional security audit.
## Screenshots
### 1. Vulnerability Scan
Displays detected security findings with rule IDs, severity levels, and source locations.

### 2. Clean Scan
Demonstrates the scanner's behavior on a clean test fixture.

### 3. Finding Details and Remediation
Shows finding evidence, explanations of potential risks, and recommended remediation steps.

### 4. Local AI Explanation
Uses a locally running Ollama model to provide additional context and remediation advice for a selected finding.

## Features
- **Static Python analysis:** Uses the standard library and AST-based rules to inspect source code without importing or executing the target code.
- **Seven security rules:** Detects potentially unsafe dynamic execution, insecure deserialization, risky shell execution, possible hardcoded credentials, weak hash algorithms, disabled HTTP TLS verification, and dynamically constructed SQL.
- **Redacted evidence:** Masks recognized credential-like assignments, token formats, authorization values, and credentials embedded in common service URLs.
- **Severity and filtering:** Presents findings with rule IDs, severity, locations, confidence, limitations, and remediation guidance, with severity and text filters in the desktop interface.
- **Local AI assistance:** Offers optional explanations using an Ollama model running on the same machine.
- **Report export:** Exports scan results to JSON and Markdown.
- **Resource limits:** Applies file-count, file-size, total-byte, and finding limits to help control scanning workloads.
- **Safer file discovery:** Skips common generated directories and symbolic links.
- **Asynchronous desktop interface:** Uses Tkinter to keep scanning and AI explanation tasks from unnecessarily blocking the interface.
- **No third-party Python dependencies:** The scanner, desktop interface, report exporters, and automated tests use Python's standard library.
## Technology Stack
| Component | Technology |
|---|---|
| Language | Python 3.11+ |
| Static analysis | Python AST |
| Desktop GUI | Tkinter |
| AI inference | Ollama with a locally installed model |
| Report formats | JSON and Markdown |
| Automated testing | `unittest` |
| External Python dependencies | None |
## Requirements and Setup
### Requirements
- Python 3.11 or newer.
- A working Tkinter installation.
- Windows is the primary documented setup environment.
- Ollama and a locally installed model are optional and required only for AI explanations.
### Run the application
Clone or download the repository and open a terminal in the project root.
Check your Python installation:
```powershell
python --version
```
Launch the desktop application:
```powershell
python app.py
```
No `pip install` step is required for the scanner, GUI, exports, or tests.
### Run a scan
1. Launch the application.
2. Select the project directory you want to inspect.
3. Choose **Scan project**.
4. Review the findings, severity levels, and remediation guidance.
5. Select a finding to inspect its details.
6. Export the results to JSON or Markdown if needed.
The current implementation scans Python (`.py`) files only.
### Run the automated tests
From the project root:
```powershell
python -m unittest discover -s tests -v
```
The current test suite contains **46 tests**, covering scanner behavior, security rules, redaction, report generation, Ollama client behavior, and GUI error handling.
To run a syntax compilation check:
```powershell
python -m compileall -q .
```
A successful compilation check produces no output.
### Run scanner smoke tests without the GUI
Scan the included synthetic fixtures directly:
```powershell
python -m scanner tests/fixtures/vulnerable
python -m scanner tests/fixtures/clean
```
These fixtures help demonstrate expected behavior; they are not a comprehensive security benchmark.
## Architecture
```text
securelocal-ai/
├── ai/ Optional local Ollama client
├── scanner/ File discovery, AST rules, and scan engine
├── reports/ JSON and Markdown report exporters
├── ui/ Tkinter interface and finding details
├── tests/ Automated tests and synthetic fixtures
├── docs/
│ ├── SECURITY.md Threat model and data-flow notes
│ └── screenshots/ Application screenshots
├── app.py Application entry point
└── README.md
```
The scanner is responsible for producing findings independently of the AI component. The optional model provides supplementary explanations only; it does not determine severity, alter scan results, apply patches, or execute code.
## Security Rules
| Rule ID | Detection | Important limitation |
|---|---|---|
| SLA001 | `eval()` and `exec()` | Does not establish whether the input is attacker-controlled. |
| SLA002 | `pickle.load()`, `pickle.loads()`, and `Unpickler.load()` | Risk depends on whether serialized input is untrusted. |
| SLA003 | `shell=True` in subprocess calls and dynamic `os.system`/`os.popen` use | Does not trace untrusted data into command arguments. |
| SLA004 | Credential-like variable names assigned non-placeholder string literals | Heuristic; may miss secrets or flag non-secret values. Recognized evidence is redacted. |
| SLA005 | MD5/SHA-1 hash calls | May flag legitimate non-security checksums. Password storage requires an appropriate password-hashing algorithm. |
| SLA006 | `verify=False` in recognized `requests`/`httpx` calls | Other HTTP clients and verification patterns may not be recognized. |
| SLA007 | SQL-looking text dynamically interpolated, formatted, or concatenated in execute calls | Cannot prove that input is untrusted or understand every database API. |
These rules provide an initial set of checks, not comprehensive Static Application Security Testing (SAST) coverage. Findings must be reviewed in context.
## Optional Local AI Explanations
SecureLocal AI can perform deterministic scans without Ollama. Local AI explanations are an optional enhancement.
### Setup
1. Install Ollama using its official installation instructions.
2. Ensure the Ollama service is running.
3. Download a model yourself if it is not already installed. For example:
```powershell
ollama pull qwen2.5-coder:3b
```
This command is an optional manual setup step. SecureLocal AI does not download models automatically.
4. Launch SecureLocal AI, select a finding, and choose **Explain selected with local AI**.
The default Ollama endpoint is:
```text
http://127.0.0.1:11434
```
The client restricts connections to the IPv4 loopback address `127.0.0.1` and rejects remote endpoints and redirects.
Only a limited, redacted finding context is sent to Ollama after the user explicitly requests an explanation. The application does not send the entire project or source file to the model.
AI-generated explanations are **unverified advice**. They can be incomplete, inaccurate, or unsuitable for the specific code and must be reviewed by a developer.
## Privacy and Threat Model
SecureLocal AI is designed to minimiz
{
"path": "c:\\Projects\\securelocal-ai\\app.py",
"view_range": [
1,
220
]
}
diff --git a/c:/Projects/securelocal-ai/app.py b/c:/Projects/securelocal-ai/app.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/app.py
+++ b/c:/Projects/securelocal-ai/app.py
@@ -1,220 +1,220 @@
from ui.main_window import run_app
if __name__ == "__main__":
run_app()
{
"path": "c:\\Projects\\securelocal-ai\\ui\\main_window.py",
"view_range": [
1,
420
]
}
diff --git a/c:/Projects/securelocal-ai/ui/main_window.py b/c:/Projects/securelocal-ai/ui/main_window.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/main_window.py
+++ b/c:/Projects/securelocal-ai/ui/main_window.py
@@ -1,420 +1,420 @@
from __future__ import annotations
import queue
import threading
import tkinter as tk
from pathlib import Path
from tkinter import filedialog, messagebox, ttk
from ai.ollama_client import DEFAULT_MODEL, OllamaClient, OllamaError
from reports.exporter import export_json, export_markdown
from scanner.engine import scan_project
from scanner.models import Finding, ScanResult, Severity
from ui.finding_details import FindingDetails
class SecureLocalApp:
def __init__(self, root: tk.Tk) -> None:
self.root = root
self.root.title("SecureLocal AI — Code Security Auditor")
self.root.geometry("1120x780")
self.root.minsize(860, 600)
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
self.events: queue.Queue[tuple[str, object]] = queue.Queue()
self.result: ScanResult | None = None
self.findings_by_id: dict[str, Finding] = {}
self._configure_style()
self._build_ui()
self.root.after(100, self._process_events)
def _configure_style(self) -> None:
style = ttk.Style(self.root)
style.configure("AppTitle.TLabel", font=("Segoe UI", 19, "bold"))
style.configure("Section.TLabel", font=("Segoe UI", 11, "bold"))
style.configure("Privacy.TLabel", foreground="#245c3b")
style.configure("Treeview", rowheight=25)
def _build_ui(self) -> None:
container = ttk.Frame(self.root, padding=16)
container.grid(row=0, column=0, sticky="nsew")
container.columnconfigure(0, weight=1)
container.rowconfigure(4, weight=1)
header = ttk.Frame(container)
header.grid(row=0, column=0, sticky="ew")
header.columnconfigure(0, weight=1)
ttk.Label(header, text="SecureLocal AI", style="AppTitle.TLabel").grid(
row=0, column=0, sticky="w"
)
ttk.Label(
header,
text="Python code security checks that run on this device.",
style="Privacy.TLabel",
).grid(row=1, column=0, sticky="w", pady=(3, 0))
folder_frame = ttk.LabelFrame(container, text="Project folder", padding=10)
folder_frame.grid(row=1, column=0, sticky="ew", pady=(14, 8))
folder_frame.columnconfigure(0, weight=1)
self.project_path = tk.StringVar()
ttk.Entry(folder_frame, textvariable=self.project_path).grid(
row=0, column=0, sticky="ew", padx=(0, 8)
)
ttk.Button(folder_frame, text="Choose folder…", command=self._choose_folder).grid(
row=0, column=1, padx=(0, 8)
)
self.scan_button = ttk.Button(folder_frame, text="Scan project", command=self._start_scan)
self.scan_button.grid(row=0, column=2)
toolbar = ttk.Frame(container)
toolbar.grid(row=2, column=0, sticky="ew", pady=6)
ttk.Label(toolbar, text="Filter severity").pack(side="left")
self.severity_filter = tk.StringVar(value="All")
self.severity_combo = ttk.Combobox(
toolbar,
textvariable=self.severity_filter,
state="readonly",
values=("All", *(severity.value for severity in Severity)),
width=12,
)
self.severity_combo.pack(side="left", padx=(6, 14))
self.severity_combo.bind("<<ComboboxSelected>>", self._apply_filters)
ttk.Label(toolbar, text="Search").pack(side="left")
self.search_text = tk.StringVar()
search = ttk.Entry(toolbar, textvariable=self.search_text, width=24)
search.pack(side="left", padx=6)
self.search_text.trace_add("write", self._apply_filters)
self.summary = ttk.Label(toolbar, text="Choose a project folder to begin.")
self.summary.pack(side="left", padx=(10, 0))
actions = ttk.Frame(container)
actions.grid(row=3, column=0, sticky="ew", pady=(0, 8))
self.ai_model = tk.StringVar(value=DEFAULT_MODEL)
ttk.Label(actions, text="Local model").pack(side="left")
ttk.Entry(actions, textvariable=self.ai_model, width=22).pack(side="left", padx=6)
self.explain_button = ttk.Button(
actions,
text="Explain selected with local AI",
command=self._explain_selected,
state="disabled",
)
self.explain_button.pack(side="left", padx=(2, 12))
self.export_json_button = ttk.Button(
actions, text="Export JSON…", command=self._export_json, state="disabled"
)
self.export_json_button.pack(side="right", padx=(6, 0))
self.export_md_button = ttk.Button(
actions, text="Export Markdown…", command=self._export_markdown, state="disabled"
)
self.export_md_button.pack(side="right")
content = ttk.Panedwindow(container, orient="vertical")
content.grid(row=4, column=0, sticky="nsew")
table_frame = ttk.Frame(content)
table_frame.columnconfigure(0, weight=1)
table_frame.rowconfigure(0, weight=1)
columns = ("severity", "rule", "file", "line", "title")
self.table = ttk.Treeview(table_frame, columns=columns, show="headings", selectmode="browse")
for column, heading, width in (
("severity", "Severity", 100),
("rule", "Rule", 85),
("file", "File", 280),
("line", "Line", 65),
("title", "Finding", 340),
):
self.table.heading(column, text=heading)
self.table.column(column, width=width, anchor="w")
self.table.grid(row=0, column=0, sticky="nsew")
self.table.bind("<<TreeviewSelect>>", self._selection_changed)
vertical = ttk.Scrollbar(table_frame, orient="vertical", command=self.table.yview)
vertical.grid(row=0, column=1, sticky="ns")
self.table.configure(yscrollcommand=vertical.set)
horizontal = ttk.Scrollbar(table_frame, orient="horizontal", command=self.table.xview)
horizontal.grid(row=1, column=0, sticky="ew")
self.table.configure(xscrollcommand=horizontal.set)
self.details = FindingDetails(content)
content.add(table_frame, weight=3)
content.add(self.details, weight=2)
self.status = tk.StringVar(
value="Scanning uses local deterministic rules. Source files are not changed or uploaded."
)
ttk.Label(container, textvariable=self.status, wraplength=1050).grid(
row=5, column=0, sticky="ew", pady=(8, 0)
)
def _choose_folder(self) -> None:
selected = filedialog.askdirectory(title="Choose a Python project folder")
if selected:
self.project_path.set(selected)
def _start_scan(self) -> None:
selected = self.project_path.get().strip()
if not selected or not Path(selected).is_dir():
messagebox.showerror("Choose a project", "Select an existing project folder first.")
return
self.scan_button.configure(state="disabled")
self.status.set("Scanning Python files locally…")
self.summary.configure(text="Scan in progress")
self._clear_findings()
thread = threading.Thread(target=self._scan_worker, args=(selected,), daemon=True)
thread.start()
def _scan_worker(self, selected: str) -> None:
try:
self.events.put(("scan", scan_project(selected)))
except (OSError, ValueError, RecursionError) as error:
self.events.put(("error", str(error)))
def _process_events(self) -> None:
try:
while True:
event, payload = self.events.get_nowait()
if event == "scan":
if isinstance(payload, ScanResult):
self._show_result(payload)
elif event == "explanation":
if isinstance(payload, tuple) and len(payload) == 2:
self._show_explanation(payload)
elif event == "error":
self
{
"path": "c:\\Projects\\securelocal-ai\\ui\\finding_details.py",
"view_range": [
1,
260
]
}
diff --git a/c:/Projects/securelocal-ai/ui/finding_details.py b/c:/Projects/securelocal-ai/ui/finding_details.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/finding_details.py
+++ b/c:/Projects/securelocal-ai/ui/finding_details.py
@@ -1,260 +1,260 @@
from __future__ import annotations
import tkinter as tk
from tkinter import ttk
from scanner.models import Finding
class FindingDetails(ttk.Frame):
def __init__(self, master: tk.Misc) -> None:
super().__init__(master, padding=(8, 6))
self.columnconfigure(0, weight=1)
ttk.Label(self, text="Finding details", style="Section.TLabel").grid(
row=0, column=0, sticky="w"
)
self.content = tk.Text(self, height=10, wrap="word", state="disabled", borderwidth=0)
self.content.grid(row=1, column=0, sticky="nsew", pady=(6, 0))
self.rowconfigure(1, weight=1)
scrollbar = ttk.Scrollbar(self, orient="vertical", command=self.content.yview)
scrollbar.grid(row=1, column=1, sticky="ns", pady=(6, 0))
self.content.configure(yscrollcommand=scrollbar.set)
def show_finding(self, finding: Finding | None) -> None:
if finding is None:
text = "Select a finding to see its explanation, evidence, limitations, and remediation."
else:
text = (
f"{finding.title} · {finding.severity.value} · {finding.rule_id}\n"
f"{finding.file_path}:{finding.line}:{finding.column}\n\n"
f"Why it matters\n{finding.description}\n\n"
f"Redacted evidence\n{finding.evidence}\n\n"
f"Confidence: {finding.confidence}\n"
f"Limitations\n{finding.limitations}\n\n"
f"Remediation\n{finding.remediation}"
)
self.content.configure(state="normal")
self.content.delete("1.0", "end")
self.content.insert("1.0", text)
self.content.configure(state="disabled")
{
"path": "c:\\Projects\\securelocal-ai\\ui\\main_window.py",
"view_range": [
181,
420
]
}
diff --git a/c:/Projects/securelocal-ai/ui/main_window.py b/c:/Projects/securelocal-ai/ui/main_window.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/main_window.py
+++ b/c:/Projects/securelocal-ai/ui/main_window.py
@@ -181,240 +181,240 @@
self.status.set("The operation could not be completed.")
messagebox.showerror("SecureLocal AI", str(payload))
except queue.Empty:
pass
self.root.after(100, self._process_events)
def _show_result(self, result: ScanResult) -> None:
self.result = result
self.scan_button.configure(state="normal")
self.export_json_button.configure(state="normal")
self.export_md_button.configure(state="normal")
self._populate_findings()
counts = result.counts_by_severity
self.summary.configure(
text=(
f"{result.files_scanned} files scanned · "
f"{counts['Critical']} critical · {counts['High']} high · "
f"{counts['Medium']} medium · {counts['Low']} low · {counts['Info']} info"
)
)
skipped_note = f" {len(result.skipped_files)} file(s) skipped." if result.skipped_files else ""
limit_note = " A safety limit was reached; this scan may be incomplete." if result.truncated else ""
self.status.set(
f"Scan complete. Findings are deterministic; no files were changed or uploaded."
f"{skipped_note}{limit_note}"
)
def _populate_findings(self) -> None:
for item in self.table.get_children():
self.table.delete(item)
self.findings_by_id.clear()
if self.result is None:
return
for index, finding in enumerate(self.result.findings):
item_id = f"finding-{index}"
self.findings_by_id[item_id] = finding
self.table.insert(
"",
"end",
iid=item_id,
values=(
finding.severity.value,
finding.rule_id,
finding.file_path,
finding.line,
finding.title,
),
)
self._apply_filters()
def _apply_filters(self, *_: object) -> None:
query = self.search_text.get().strip().casefold()
severity = self.severity_filter.get()
for item_id, finding in self.findings_by_id.items():
matches_severity = severity == "All" or finding.severity.value == severity
haystack = " ".join(
(finding.rule_id, finding.title, finding.file_path, finding.evidence)
).casefold()
visible = matches_severity and query in haystack
if visible and not self.table.exists(item_id):
self.table.reattach(item_id, "", "end")
elif not visible and self.table.exists(item_id):
self.table.detach(item_id)
def _selection_changed(self, _event: tk.Event) -> None:
selected = self.table.selection()
finding = self.findings_by_id.get(selected[0]) if selected else None
self.details.show_finding(finding)
self.explain_button.configure(state="normal" if finding else "disabled")
def _clear_findings(self) -> None:
self.result = None
self.findings_by_id.clear()
for item in self.table.get_children():
self.table.delete(item)
self.details.show_finding(None)
self.explain_button.configure(state="disabled")
self.export_json_button.configure(state="disabled")
self.export_md_button.configure(state="disabled")
def _explain_selected(self) -> None:
selected = self.table.selection()
if not selected:
return
finding = self.findings_by_id[selected[0]]
model = self.ai_model.get().strip()
self.explain_button.configure(state="disabled")
self.status.set("Checking the local Ollama service and installed model…")
thread = threading.Thread(
target=self._explanation_worker, args=(finding, model), daemon=True
)
thread.start()
def _explanation_worker(self, finding: Finding, model: str) -> None:
try:
explanation = OllamaClient().explain_finding(finding, model)
self.events.put(("explanation", (True, explanation)))
except OllamaError as error:
self.events.put(("explanation", (False, str(error))))
def _show_explanation(self, payload: tuple[bool, str]) -> None:
success, text = payload
self.explain_button.configure(state="normal" if self.table.selection() else "disabled")
if success:
dialog = tk.Toplevel(self.root)
dialog.title("Local AI explanation — unverified advice")
dialog.geometry("600x440")
dialog.transient(self.root)
frame = ttk.Frame(dialog, padding=12)
frame.pack(fill="both", expand=True)
ttk.Label(
frame,
text="AI-generated local explanation. It may be incorrect; confirm findings independently.",
wraplength=560,
).pack(anchor="w", pady=(0, 8))
content = tk.Text(frame, wrap="word")
content.pack(fill="both", expand=True)
content.insert("1.0", text)
content.configure(state="disabled")
scrollbar = ttk.Scrollbar(frame, orient="vertical", command=content.yview)
scrollbar.pack(side="right", fill="y")
content.configure(yscrollcommand=scrollbar.set)
ttk.Button(dialog, text="Close", command=dialog.destroy).pack(pady=8)
self.status.set("Local AI explanation received. Deterministic findings were not changed.")
else:
self.status.set("Local AI explanation unavailable; built-in remediation remains available.")
messagebox.showinfo("Local AI unavailable", str(text))
def _export_json(self) -> None:
self._export("json")
def _export_markdown(self) -> None:
self._export("markdown")
def _export(self, format_name: str) -> None:
if self.result is None:
return
extension = ".json" if format_name == "json" else ".md"
destination = filedialog.asksaveasfilename(
title=f"Export {format_name.upper()} report",
defaultextension=extension,
filetypes=[(f"{format_name.upper()} report", f"*{extension}")],
)
if not destination:
return
try:
exporter = export_json if format_name == "json" else export_markdown
path = exporter(self.result, destination)
except OSError as error:
messagebox.showerror("Report export failed", f"The report could not be saved: {error}")
return
self.status.set(f"Report exported to {path}. The report contains findings, not source files.")
def run_app() -> None:
root = tk.Tk()
SecureLocalApp(root)
root.mainloop()
{
"path": "c:\\Projects\\securelocal-ai\\tests\\test_ui_ollama.py",
"view_range": [
1,
260
]
}
diff --git a/c:/Projects/securelocal-ai/tests/test_ui_ollama.py b/c:/Projects/securelocal-ai/tests/test_ui_ollama.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/tests/test_ui_ollama.py
+++ b/c:/Projects/securelocal-ai/tests/test_ui_ollama.py
@@ -1,260 +1,260 @@
from __future__ import annotations
import queue
import unittest
from unittest.mock import patch
from ai.ollama_client import OllamaError
from scanner.models import Finding, Severity
from ui.main_window import SecureLocalApp
class OllamaUiTests(unittest.TestCase):
def test_explanation_error_is_queued_for_ui_without_escaping_worker(self):
app = SecureLocalApp.__new__(SecureLocalApp)
app.events = queue.Queue()
finding = Finding(
"SLA001", "Dynamic code execution", Severity.HIGH, "x.py", 1, 1,
"eval(value)", "description", "remediation", "High", "limitations"
)
with patch(
"ui.main_window.OllamaClient.explain_finding",
side_effect=OllamaError("Ollama is unavailable at the configured loopback address."),
):
app._explanation_worker(finding, "qwen2.5-coder:3b")
self.assertEqual(
app.events.get_nowait(),
(
"explanation",
(False, "Ollama is unavailable at the configured loopback address."),
),
)
if __name__ == "__main__":
unittest.main()
{
"path": "c:\\Projects\\securelocal-ai\\tests\\test_reports.py",
"view_range": [
1,
220
]
}
diff --git a/c:/Projects/securelocal-ai/tests/test_reports.py b/c:/Projects/securelocal-ai/tests/test_reports.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/tests/test_reports.py
+++ b/c:/Projects/securelocal-ai/tests/test_reports.py
@@ -1,220 +1,220 @@
from __future__ import annotations
import json
import tempfile
import unittest
from pathlib import Path
from reports.exporter import export_json, export_markdown
from scanner.engine import scan_project
from scanner.models import ScanResult
FIXTURES = Path(__file__).parent / "fixtures"
SECRET = "ghp_abcdefghijklmnopqrstuvwxyz012345"
class ReportTests(unittest.TestCase):
def test_json_and_markdown_reports_redact_credentials(self):
result = scan_project(FIXTURES / "vulnerable")
with tempfile.TemporaryDirectory() as directory:
json_path = export_json(result, Path(directory) / "report.json")
markdown_path = export_markdown(result, Path(directory) / "report.md")
json_text = json_path.read_text(encoding="utf-8")
markdown_text = markdown_path.read_text(encoding="utf-8")
self.assertNotIn(SECRET, json_text)
self.assertNotIn(SECRET, markdown_text)
self.assertIn("[REDACTED]", json_text)
self.assertIn("SLA007", json_text)
self.assertIn("professional audit", markdown_text)
def test_zero_findings_report_contains_summary_and_disclaimer(self):
result = scan_project(FIXTURES / "clean")
with tempfile.TemporaryDirectory() as directory:
path = export_markdown(result, Path(directory) / "clean.md")
content = path.read_text(encoding="utf-8")
self.assertIn("No findings were reported", content)
self.assertIn("Severity summary", content)
self.assertIn("Disclaimer", content)
def test_reports_redact_service_credentials_from_finding_evidence(self):
database_password = "db-password-value"
with tempfile.TemporaryDirectory() as directory:
root = Path(directory) / "project"
root.mkdir()
source = root / "query.py"
source.write_text(
"cursor.execute(f'SELECT * FROM users WHERE id = {user_id}') "
f"# [REDACTED]",
encoding="utf-8",
)
result = scan_project(root)
json_path = export_json(result, Path(directory) / "report.json")
markdown_path = export_markdown(result, Path(directory) / "report.md")
json_text = json_path.read_text(encoding="utf-8")
markdown_text = markdown_path.read_text(encoding="utf-8")
self.assertNotIn(database_password, json_text)
self.assertNotIn(database_password, markdown_text)
self.assertIn("[REDACTED]", json_text)
def test_reports_redact_bracketed_credentials_and_escape_markdown_evidence(self):
secret = "bracket-secret-value"
with tempfile.TemporaryDirectory() as directory:
root = Path(directory) / "project"
root.mkdir()
(root / "query.py").write_text(
f'eval(user_input); config["api_key"] = "{secret}" # ` <img src=x>\\n',
encoding="utf-8",
)
result = scan_project(root)
json_path = export_json(result, Path(directory) / "report.json")
markdown_path = export_markdown(result, Path(directory) / "report.md")
json_text = json_path.read_text(encoding="utf-8")
markdown_text = markdown_path.read_text(encoding="utf-8")
self.assertNotIn(secret, json_text)
self.assertNotIn(secret, markdown_text)
data = json.loads(json_text)
self.assertIn(
'config["api_key"] = "[REDACTED]"',
data["findings"][0]["evidence"],
)
self.assertIn("``", markdown_text)
self.assertNotIn("\\` <img src=x>", markdown_text)
def test_json_has_scanner_metadata_counts_and_multiple_findings(self):
result = scan_project(FIXTURES / "vulnerable")
with tempfile.TemporaryDirectory() as directory:
path = export_json(result, Path(directory) / "report.json")
data = json.loads(path.read_text(encoding="utf-8"))
self.assertEqual(data["scanner_version"], "0.1.0")
self.assertEqual(data["files_scanned"], 1)
self.assertGreater(len(data["findings"]), 1)
self.assertEqual(data["counts_by_severity"]["High"], 8)
def test_report_with_no_scanned_files_is_supported(self):
result = ScanResult("empty", "2026-01-01T00:00:00+00:00", 0, (), (), False)
with tempfile.TemporaryDirectory() as directory:
path = export_json(result, Path(directory) / "empty.json")
data = json.loads(path.read_text(encoding="utf-8"))
self.assertEqual(data["findings"], [])
self.assertEqual(data["counts_by_severity"]["Critical"], 0)
if __name__ == "__main__":
unittest.main()
{
"path": "c:\\Projects\\securelocal-ai\\scanner\\models.py",
"view_range": [
1,
260
]
}
diff --git a/c:/Projects/securelocal-ai/scanner/models.py b/c:/Projects/securelocal-ai/scanner/models.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/scanner/models.py
+++ b/c:/Projects/securelocal-ai/scanner/models.py
@@ -1,260 +1,260 @@
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
class Severity(str, Enum):
CRITICAL = "Critical"
HIGH = "High"
MEDIUM = "Medium"
LOW = "Low"
INFO = "Info"
@dataclass(frozen=True)
class Finding:
rule_id: str
title: str
severity: Severity
file_path: str
line: int
column: int
evidence: str
description: str
remediation: str
confidence: str
limitations: str
@dataclass(frozen=True)
class SkippedFile:
file_path: str
reason: str
@dataclass(frozen=True)
class ScanResult:
project_name: str
scanned_at: str
files_scanned: int
skipped_files: tuple[SkippedFile, ...]
findings: tuple[Finding, ...]
truncated: bool
@property
def counts_by_severity(self) -> dict[str, int]:
counts = {severity.value: 0 for severity in Severity}
for finding in self.findings:
counts[finding.severity.value] += 1
return counts
I’ve mapped the existing UI to the main app shell and detail panel. The planned edits are confined to ui/main_window.py and ui/finding_details.py: introduce a shared light-theme styling layer, add summary cards for scan totals, modernize the findings table and filter toolbar, and refresh the detail panel while keeping the scanner logic, redaction, and Ollama flow intact.
{"path":"c:\\Projects\\securelocal-ai\\ui\\main_window.py","old_str":"from __future__ import annotations\n\nimport queue\nimport threading\nimport tkinter as tk\nfrom pathlib import Path\nfrom tkinter...
{"command":"@\"\nfrom __future__ import annotations\n\nimport queue\nimport threading\nimport tkinter as tk\nfrom pathlib import Path\nfrom tkinter import filedialog, messagebox, ttk\n\nfrom ai.ollama...
<shellId: 0 completed with exit code 0>
{
"path": "c:\\Projects\\securelocal-ai\\ui\\main_window.py",
"view_range": [
1,
260
]
}
diff --git a/c:/Projects/securelocal-ai/ui/main_window.py b/c:/Projects/securelocal-ai/ui/main_window.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/main_window.py
+++ b/c:/Projects/securelocal-ai/ui/main_window.py
@@ -1,260 +1,260 @@
from __future__ import annotations
import queue
import threading
import tkinter as tk
from pathlib import Path
from tkinter import filedialog, messagebox, ttk
from ai.ollama_client import DEFAULT_MODEL, OllamaClient, OllamaError
from reports.exporter import export_json, export_markdown
from scanner.engine import scan_project
from scanner.models import Finding, ScanResult, Severity
from ui.finding_details import FindingDetails
class SecureLocalApp:
def __init__(self, root: tk.Tk) -> None:
self.root = root
self.root.title("SecureLocal AI — Code Security Auditor")
self.root.geometry("1120x780")
self.root.minsize(860, 600)
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
self.root.configure(bg="#F3F6FB")
self.events: queue.Queue[tuple[str, object]] = queue.Queue()
self.result: ScanResult | None = None
self.findings_by_id: dict[str, Finding] = {}
self.summary_cards: dict[str, tuple[ttk.Label, tk.StringVar]] = {}
self._configure_style()
self._build_ui()
self.root.after(100, self._process_events)
def _configure_style(self) -> None:
style = ttk.Style(self.root)
style.theme_use("clam")
style.configure(".", font=("Segoe UI", 10), background="#F3F6FB", foreground="#0F172A")
style.configure("Header.TFrame", background="#112A46")
style.configure("Header.TLabel", font=("Segoe UI", 20, "bold"), background="#112A46", foreground="#FFFFFF")
style.configure("Subtitle.TLabel", font=("Segoe UI", 10), background="#112A46", foreground="#D7E2F3")
style.configure("Section.TLabel", font=("Segoe UI", 10, "bold"), background="#F3F6FB", foreground="#1E293B")
style.configure("Section.TLabelframe", background="#F3F6FB", bordercolor="#D4DDEB", lightcolor="#D4DDEB", darkcolor="#D4DDEB")
style.configure("Section.TLabelframe.Label", font=("Segoe UI", 10, "bold"), background="#F3F6FB", foreground="#1E293B")
style.configure("Card.TFrame", background="#FFFFFF", relief="solid", borderwidth=1)
style.configure("SummaryLabel.TLabel", font=("Segoe UI", 9, "bold"), background="#FFFFFF", foreground="#475467")
style.configure("SummaryValue.TLabel", font=("Segoe UI", 18, "bold"), background="#FFFFFF", foreground="#0F172A")
style.configure("Toolbar.TFrame", background="#EEF3FA")
style.configure("Toolbar.TLabel", background="#EEF3FA", foreground="#475467", font=("Segoe UI", 9, "bold"))
style.configure("Status.TLabel", background="#F3F6FB", foreground="#475467", wraplength=1100)
style.configure("Primary.TButton", font=("Segoe UI", 10, "bold"), background="#2F6FED", foreground="#FFFFFF")
style.map("Primary.TButton", background=[("active", "#255ad6"), ("pressed", "#1f4bb7")], foreground=[("disabled", "#E2E8F0")])
style.configure("Secondary.TButton", background="#FFFFFF", foreground="#0F172A")
style.map("Secondary.TButton", background=[("active", "#EEF3FA"), ("pressed", "#E2E8F0")])
style.configure("Treeview", rowheight=26, background="#FFFFFF", fieldbackground="#FFFFFF", borderwidth=0)
style.map("Treeview", background=[("selected", "#D9E7FF")], foreground=[("selected", "#0F172A")])
style.configure("Treeview.Heading", font=("Segoe UI", 10, "bold"), background="#E2E8F0", foreground="#0F172A")
style.map("Treeview.Heading", background=[("active", "#D9E7FF")])
style.configure("Panel.TFrame", background="#FFFFFF")
def _build_ui(self) -> None:
container = ttk.Frame(self.root, padding=16, style="Panel.TFrame")
container.grid(row=0, column=0, sticky="nsew")
container.columnconfigure(0, weight=1)
container.rowconfigure(5, weight=1)
header = ttk.Frame(container, style="Header.TFrame", padding=(18, 16))
header.grid(row=0, column=0, sticky="ew")
header.columnconfigure(0, weight=1)
ttk.Label(header, text="SecureLocal AI", style="Header.TLabel").grid(row=0, column=0, sticky="w")
ttk.Label(
header,
text="Offline-first Python code security checks for local review and remediation.",
style="Subtitle.TLabel",
).grid(row=1, column=0, sticky="w", pady=(4, 0))
folder_frame = ttk.LabelFrame(container, text="Project selection", padding=12)
folder_frame.grid(row=1, column=0, sticky="ew", pady=(16, 10))
folder_frame.columnconfigure(0, weight=1)
self.project_path = tk.StringVar()
ttk.Entry(folder_frame, textvariable=self.project_path, width=60).grid(row=0, column=0, sticky="ew", padx=(0, 10))
ttk.Button(
folder_frame,
text="Choose folder…",
command=self._choose_folder,
style="Secondary.TButton",
).grid(row=0, column=1, padx=(0, 10))
self.scan_button = ttk.Button(
folder_frame,
text="Scan project",
command=self._start_scan,
style="Primary.TButton",
)
self.scan_button.grid(row=0, column=2)
self.summary_cards_frame = ttk.Frame(container, padding=(0, 0, 0, 8))
self.summary_cards_frame.grid(row=2, column=0, sticky="ew")
self.summary_cards_frame.columnconfigure(0, weight=1)
self.summary_cards_frame.columnconfigure(1, weight=1)
self.summary_cards_frame.columnconfigure(2, weight=1)
self.summary_cards_frame.columnconfigure(3, weight=1)
self._build_summary_cards()
toolbar = ttk.Frame(container, style="Toolbar.TFrame", padding=(12, 10))
toolbar.grid(row=3, column=0, sticky="ew", pady=(0, 10))
toolbar.columnconfigure(4, weight=1)
ttk.Label(toolbar, text="Severity", style="Toolbar.TLabel").grid(row=0, column=0, sticky="w")
self.severity_filter = tk.StringVar(value="All")
self.severity_combo = ttk.Combobox(
toolbar,
textvariable=self.severity_filter,
state="readonly",
values=("All", *(severity.value for severity in Severity)),
width=12,
)
self.severity_combo.grid(row=0, column=1, sticky="w", padx=(6, 18))
self.severity_combo.bind("<<ComboboxSelected>>", self._apply_filters)
ttk.Label(toolbar, text="Search", style="Toolbar.TLabel").grid(row=0, column=2, sticky="w")
self.search_text = tk.StringVar()
search = ttk.Entry(toolbar, textvariable=self.search_text, width=30)
search.grid(row=0, column=3, sticky="ew", padx=(6, 12))
self.search_text.trace_add("write", self._apply_filters)
self.summary = ttk.Label(toolbar, text="Choose a project folder to begin.", style="Toolbar.TLabel")
self.summary.grid(row=0, column=4, sticky="e")
actions = ttk.Frame(container, padding=(0, 0, 0, 8))
actions.grid(row=4, column=0, sticky="ew")
self.ai_model = tk.StringVar(value=DEFAULT_MODEL)
ttk.Label(actions, text="Local model", style="Section.TLabel").pack(side="left")
ttk.Entry(actions, textvariable=self.ai_model, width=22).pack(side="left", padx=(6, 8))
self.explain_button = ttk.Button(
actions,
text="Explain selected with local AI",
command=self._explain_selected,
state="disabled",
style="Primary.TButton",
)
self.explain_button.pack(side="left", padx=(0, 12))
self.export_json_button = ttk.Button(
actions,
text="Export JSON…",
command=self._export_json,
state="disabled",
style="Secondary.TButton",
)
self.export_json_button.pack(side="right", padx=(8, 0))
self.export_md_button = ttk.Button(
actions,
text="Export Markdown…",
command=self._export_markdown,
state="disabled",
style="Secondary.TButton",
)
self.export_md_button.pack(side="right")
content = ttk.Panedwindow(container, orient="vertical")
content.grid(row=5, column=0, sticky="nsew")
table_frame = ttk.Fra
{
"path": "c:\\Projects\\securelocal-ai\\ui\\main_window.py",
"view_range": [
150,
420
]
}
diff --git a/c:/Projects/securelocal-ai/ui/main_window.py b/c:/Projects/securelocal-ai/ui/main_window.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/main_window.py
+++ b/c:/Projects/securelocal-ai/ui/main_window.py
@@ -150,271 +150,271 @@
style="Secondary.TButton",
)
self.export_md_button.pack(side="right")
content = ttk.Panedwindow(container, orient="vertical")
content.grid(row=5, column=0, sticky="nsew")
table_frame = ttk.Frame(content, style="Panel.TFrame")
table_frame.columnconfigure(0, weight=1)
table_frame.rowconfigure(0, weight=1)
columns = ("severity", "rule", "file", "line", "title")
self.table = ttk.Treeview(table_frame, columns=columns, show="headings", selectmode="browse")
for column, heading, width in (
("severity", "Severity", 100),
("rule", "Rule", 86),
("file", "File", 260),
("line", "Line", 62),
("title", "Finding", 360),
):
self.table.heading(column, text=heading)
self.table.column(column, width=width, anchor="w")
self.table.grid(row=0, column=0, sticky="nsew")
self.table.bind("<<TreeviewSelect>>", self._selection_changed)
vertical = ttk.Scrollbar(table_frame, orient="vertical", command=self.table.yview)
vertical.grid(row=0, column=1, sticky="ns")
self.table.configure(yscrollcommand=vertical.set)
horizontal = ttk.Scrollbar(table_frame, orient="horizontal", command=self.table.xview)
horizontal.grid(row=1, column=0, sticky="ew")
self.table.configure(xscrollcommand=horizontal.set)
self.details = FindingDetails(content)
content.add(table_frame, weight=3)
content.add(self.details, weight=2)
self.status = tk.StringVar(value="Scanning uses local deterministic rules. Source files are not changed or uploaded.")
ttk.Label(container, textvariable=self.status, style="Status.TLabel").grid(row=6, column=0, sticky="ew", pady=(8, 0))
def _build_summary_cards(self) -> None:
self.summary_cards = {}
labels = [
("Total findings", "total"),
("High", "high"),
("Medium", "medium"),
("Low", "low"),
]
for index, (label_text, key) in enumerate(labels):
card = ttk.Frame(self.summary_cards_frame, padding=(12, 10), style="Card.TFrame")
card.grid(row=0, column=index, sticky="nsew", padx=(0 if index == 0 else 8, 0))
ttk.Label(card, text=label_text, style="SummaryLabel.TLabel").pack(anchor="w")
value = tk.StringVar(value="0")
ttk.Label(card, textvariable=value, style="SummaryValue.TLabel").pack(anchor="w", pady=(4, 0))
self.summary_cards[key] = (card, value)
self._update_summary_cards(None)
def _update_summary_cards(self, result: ScanResult | None) -> None:
cards = {
"total": "0",
"high": "0",
"medium": "0",
"low": "0",
}
if result is not None:
counts = result.counts_by_severity
cards["total"] = str(len(result.findings))
cards["high"] = str(counts["High"])
cards["medium"] = str(counts["Medium"])
cards["low"] = str(counts["Low"])
for key, value in cards.items():
if key in self.summary_cards:
self.summary_cards[key][1].set(value)
def _choose_folder(self) -> None:
selected = filedialog.askdirectory(title="Choose a Python project folder")
if selected:
self.project_path.set(selected)
def _start_scan(self) -> None:
selected = self.project_path.get().strip()
if not selected or not Path(selected).is_dir():
messagebox.showerror("Choose a project", "Select an existing project folder first.")
return
self.scan_button.configure(state="disabled")
self.status.set("Scanning Python files locally…")
self.summary.configure(text="Scan in progress")
self._clear_findings()
thread = threading.Thread(target=self._scan_worker, args=(selected,), daemon=True)
thread.start()
def _scan_worker(self, selected: str) -> None:
try:
self.events.put(("scan", scan_project(selected)))
except (OSError, ValueError, RecursionError) as error:
self.events.put(("error", str(error)))
def _process_events(self) -> None:
try:
while True:
event, payload = self.events.get_nowait()
if event == "scan":
if isinstance(payload, ScanResult):
self._show_result(payload)
elif event == "explanation":
if isinstance(payload, tuple) and len(payload) == 2:
self._show_explanation(payload)
elif event == "error":
self._handle_error(payload)
except queue.Empty:
pass
self.root.after(100, self._process_events)
def _show_result(self, result: ScanResult) -> None:
self.result = result
self.scan_button.configure(state="normal")
self.export_json_button.configure(state="normal")
self.export_md_button.configure(state="normal")
self._populate_findings()
self._update_summary_cards(result)
counts = result.counts_by_severity
self.summary.configure(
text=(
f"{result.files_scanned} files scanned · "
f"{counts['Critical']} critical · {counts['High']} high · "
f"{counts['Medium']} medium · {counts['Low']} low"
)
)
skipped_note = f" {len(result.skipped_files)} file(s) skipped." if result.skipped_files else ""
limit_note = " A safety limit was reached; this scan may be incomplete." if result.truncated else ""
self.status.set(
f"Scan complete. Findings are deterministic; no files were changed or uploaded."
f"{skipped_note}{limit_note}"
)
def _populate_findings(self) -> None:
for item in self.table.get_children():
self.table.delete(item)
self.findings_by_id.clear()
if self.result is None:
return
for index, finding in enumerate(self.result.findings):
item_id = f"finding-{index}"
self.findings_by_id[item_id] = finding
tags = (finding.severity.value,)
if index % 2 == 1:
tags = tags + ("oddrow",)
self.table.insert(
"",
"end",
iid=item_id,
tags=tags,
values=(
finding.severity.value,
finding.rule_id,
finding.file_path,
finding.line,
finding.title,
),
)
self._apply_filters()
def _apply_filters(self, *_: object) -> None:
query = self.search_text.get().strip().casefold()
severity = self.severity_filter.get()
for item_id, finding in self.findings_by_id.items():
matches_severity = severity == "All" or finding.severity.value == severity
haystack = " ".join((finding.rule_id, finding.title, finding.file_path, finding.evidence)).casefold()
visible = matches_severity and query in haystack
if visible and not self.table.exists(item_id):
self.table.reattach(item_id, "", "end")
elif not visible and self.table.exists(item_id):
self.table.detach(item_id)
def _selection_changed(self, _event: tk.Event) -> None:
selected = self.table.selection()
finding = self.findings_by_id.get(selected[0]) if selected else None
self.details.show_finding(finding)
self.explain_button.configure(state="normal" if finding else "disabled")
def _clear_findings(self) -> None:
self.result = None
self.findings_by_id.clear()
for item in self.table.get_children():
self.table.delete(item)
self._update_summary_cards(None)
self.summary.configure(text="Choose a project folder to begin.")
self.details.show_finding(None)
self.explain_button.conf
{"command":"python -c \"from pathlib import Path; p=Path(r'c:\\Projects\\securelocal-ai\\ui\\main_window.py'); text=p.read_text(encoding='utf-8-sig'); old=' self.table = ttk.Treeview(table_fram...
File "<string>", line 1
from pathlib import Path; p=Path(r'c:\Projects\securelocal-ai\ui\main_window.py'); text=p.read_text(encoding='utf-8-sig'); old=' self.table = ttk.Treeview(table_frame, columns=columns, show=" headings\,
^
SyntaxError: unterminated string literal (detected at line 1)
<shellId: 1 completed with exit code 1>
{"command":"@'\nfrom pathlib import Path\np = Path(r'c:\\Projects\\securelocal-ai\\ui\\main_window.py')\ntext = p.read_text(encoding='utf-8-sig')\nold = ''' self.table = ttk.Treeview(table_fram...
<shellId: 2 completed with exit code 0>
{
"path": "c:\\Projects\\securelocal-ai\\ui\\main_window.py",
"view_range": [
130,
220
]
}
diff --git a/c:/Projects/securelocal-ai/ui/main_window.py b/c:/Projects/securelocal-ai/ui/main_window.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/main_window.py
+++ b/c:/Projects/securelocal-ai/ui/main_window.py
@@ -130,91 +130,91 @@
actions,
text="Explain selected with local AI",
command=self._explain_selected,
state="disabled",
style="Primary.TButton",
)
self.explain_button.pack(side="left", padx=(0, 12))
self.export_json_button = ttk.Button(
actions,
text="Export JSON…",
command=self._export_json,
state="disabled",
style="Secondary.TButton",
)
self.export_json_button.pack(side="right", padx=(8, 0))
self.export_md_button = ttk.Button(
actions,
text="Export Markdown…",
command=self._export_markdown,
state="disabled",
style="Secondary.TButton",
)
self.export_md_button.pack(side="right")
content = ttk.Panedwindow(container, orient="vertical")
content.grid(row=5, column=0, sticky="nsew")
table_frame = ttk.Frame(content, style="Panel.TFrame")
table_frame.columnconfigure(0, weight=1)
table_frame.rowconfigure(0, weight=1)
columns = ("severity", "rule", "file", "line", "title")
self.table = ttk.Treeview(table_frame, columns=columns, show="headings", selectmode="browse")
self.table.tag_configure("oddrow", background="#F8FAFC")
self.table.tag_configure("Critical", foreground="#B42318")
self.table.tag_configure("High", foreground="#D97706")
self.table.tag_configure("Medium", foreground="#AC6B0C")
self.table.tag_configure("Low", foreground="#2F6FED")
self.table.tag_configure("Info", foreground="#475467")
for column, heading, width in (
("severity", "Severity", 100),
("rule", "Rule", 86),
("file", "File", 260),
("line", "Line", 62),
("title", "Finding", 360),
):
self.table.heading(column, text=heading)
self.table.column(column, width=width, anchor="w")
self.table.grid(row=0, column=0, sticky="nsew")
self.table.bind("<<TreeviewSelect>>", self._selection_changed)
vertical = ttk.Scrollbar(table_frame, orient="vertical", command=self.table.yview)
vertical.grid(row=0, column=1, sticky="ns")
self.table.configure(yscrollcommand=vertical.set)
horizontal = ttk.Scrollbar(table_frame, orient="horizontal", command=self.table.xview)
horizontal.grid(row=1, column=0, sticky="ew")
self.table.configure(xscrollcommand=horizontal.set)
self.details = FindingDetails(content)
content.add(table_frame, weight=3)
content.add(self.details, weight=2)
self.status = tk.StringVar(value="Scanning uses local deterministic rules. Source files are not changed or uploaded.")
ttk.Label(container, textvariable=self.status, style="Status.TLabel").grid(row=6, column=0, sticky="ew", pady=(8, 0))
def _build_summary_cards(self) -> None:
self.summary_cards = {}
labels = [
("Total findings", "total"),
("High", "high"),
("Medium", "medium"),
("Low", "low"),
]
for index, (label_text, key) in enumerate(labels):
card = ttk.Frame(self.summary_cards_frame, padding=(12, 10), style="Card.TFrame")
card.grid(row=0, column=index, sticky="nsew", padx=(0 if index == 0 else 8, 0))
ttk.Label(card, text=label_text, style="SummaryLabel.TLabel").pack(anchor="w")
value = tk.StringVar(value="0")
ttk.Label(card, textvariable=value, style="SummaryValue.TLabel").pack(anchor="w", pady=(4, 0))
self.summary_cards[key] = (card, value)
self._update_summary_cards(None)
def _update_summary_cards(self, result: ScanResult | None) -> None:
cards = {
"total": "0",
"high": "0",
"medium": "0",
"low": "0",
}
if result is not None:
counts = result.counts_by_severity
cards["total"] = str(len(result.findings))
cards["high"] = str(counts["High"])
cards["medium"] = str(counts["Medium"])
cards["low"] = str(counts["Low"])
{
"path": "c:\\Projects\\securelocal-ai\\ui\\finding_details.py",
"view_range": [
1,
220
]
}
diff --git a/c:/Projects/securelocal-ai/ui/finding_details.py b/c:/Projects/securelocal-ai/ui/finding_details.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/finding_details.py
+++ b/c:/Projects/securelocal-ai/ui/finding_details.py
@@ -1,220 +1,220 @@
from __future__ import annotations
import tkinter as tk
from tkinter import ttk
from scanner.models import Finding
class FindingDetails(ttk.Frame):
def __init__(self, master: tk.Misc) -> None:
super().__init__(master, padding=(10, 8), style="Panel.TFrame")
self.columnconfigure(0, weight=1)
self.rowconfigure(1, weight=1)
ttk.Label(self, text="Finding details", style="Section.TLabel").grid(row=0, column=0, sticky="w")
self.content = tk.Text(
self,
wrap="word",
state="disabled",
bg="#FFFFFF",
relief="flat",
borderwidth=0,
highlightthickness=0,
padx=8,
pady=8,
spacing1=4,
spacing2=2,
spacing3=2,
)
self.content.grid(row=1, column=0, sticky="nsew", pady=(8, 0))
scrollbar = ttk.Scrollbar(self, orient="vertical", command=self.content.yview)
scrollbar.grid(row=1, column=1, sticky="ns", pady=(8, 0))
self.content.configure(yscrollcommand=scrollbar.set)
self.content.tag_configure("title", font=("Segoe UI", 12, "bold"), foreground="#0F172A")
self.content.tag_configure("meta", font=("Segoe UI", 10, "bold"), foreground="#475467")
self.content.tag_configure("body", font=("Segoe UI", 10), foreground="#1E293B")
self.content.tag_configure("section", font=("Segoe UI", 10, "bold"), foreground="#1E293B")
self.content.tag_configure("muted", font=("Segoe UI", 9), foreground="#475467")
def show_finding(self, finding: Finding | None) -> None:
self.content.configure(state="normal")
self.content.delete("1.0", "end")
if finding is None:
self.content.insert("end", "Select a finding to view the explanation, evidence, limitations, and remediation.", "body")
self.content.configure(state="disabled")
return
self.content.insert("end", f"{finding.title}\n", "title")
self.content.insert(
"end",
f"Severity: {finding.severity.value} | Rule: {finding.rule_id} | Confidence: {finding.confidence}\n",
"meta",
)
self.content.insert(
"end",
f"Location: {finding.file_path}:{finding.line}:{finding.column}\n\n",
"body",
)
sections = [
("Why it matters", finding.description),
("Redacted evidence", finding.evidence),
("Limitations", finding.limitations),
("Remediation", finding.remediation),
]
for label, value in sections:
self.content.insert("end", f"{label}\n", "section")
self.content.insert("end", f"{value}\n\n", "body")
self.content.configure(state="disabled")
{"command":"@'\nfrom pathlib import Path\np = Path(r'c:\\Projects\\securelocal-ai\\ui\\main_window.py')\ntext = p.read_text(encoding='utf-8-sig')\nold = ''' self.summary_cards_frame.columnconfi...
pattern not found
<shellId: 3 completed with exit code 1>
{"path":"c:\\Projects\\securelocal-ai\\ui\\main_window.py","old_str":" self.summary_cards_frame.columnconfigure(0, weight=1)\n self.summary_cards_frame.columnconfigure(1, weight=1)\n ...
diff --git a/c:/Projects/securelocal-ai/ui/main_window.py b/c:/Projects/securelocal-ai/ui/main_window.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/main_window.py
+++ b/c:/Projects/securelocal-ai/ui/main_window.py
@@ -97,6 +97,7 @@
self.summary_cards_frame.columnconfigure(1, weight=1)
self.summary_cards_frame.columnconfigure(2, weight=1)
self.summary_cards_frame.columnconfigure(3, weight=1)
+ self.summary_cards_frame.columnconfigure(4, weight=1)
self._build_summary_cards()
toolbar = ttk.Frame(container, style="Toolbar.TFrame", padding=(12, 10))
{"path":"c:\\Projects\\securelocal-ai\\ui\\main_window.py","old_str":" def _build_summary_cards(self) -> None:\n self.summary_cards = {}\n labels = [\n (\"Total findings\",...
diff --git a/c:/Projects/securelocal-ai/ui/main_window.py b/c:/Projects/securelocal-ai/ui/main_window.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/main_window.py
+++ b/c:/Projects/securelocal-ai/ui/main_window.py
@@ -196,10 +196,13 @@
("High", "high"),
("Medium", "medium"),
("Low", "low"),
+ ("Info", "info"),
]
for index, (label_text, key) in enumerate(labels):
card = ttk.Frame(self.summary_cards_frame, padding=(12, 10), style="Card.TFrame")
card.grid(row=0, column=index, sticky="nsew", padx=(0 if index == 0 else 8, 0))
+ if key == "info":
+ card.grid_remove()
ttk.Label(card, text=label_text, style="SummaryLabel.TLabel").pack(anchor="w")
value = tk.StringVar(value="0")
ttk.Label(card, textvariable=value, style="SummaryValue.TLabel").pack(anchor="w", pady=(4, 0))
{"path":"c:\\Projects\\securelocal-ai\\ui\\main_window.py","old_str":" def _update_summary_cards(self, result: ScanResult | None) -> None:\n cards = {\n \"total\": \"0\",\n ...
diff --git a/c:/Projects/securelocal-ai/ui/main_window.py b/c:/Projects/securelocal-ai/ui/main_window.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/main_window.py
+++ b/c:/Projects/securelocal-ai/ui/main_window.py
@@ -215,6 +215,7 @@
"high": "0",
"medium": "0",
"low": "0",
+ "info": "0",
}
if result is not None:
counts = result.counts_by_severity
@@ -222,9 +223,17 @@
cards["high"] = str(counts["High"])
cards["medium"] = str(counts["Medium"])
cards["low"] = str(counts["Low"])
+ cards["info"] = str(counts["Info"])
for key, value in cards.items():
if key in self.summary_cards:
self.summary_cards[key][1].set(value)
+ info_card = self.summary_cards.get("info")
+ if info_card is not None:
+ card, _ = info_card
+ if cards["info"] == "0":
+ card.grid_remove()
+ else:
+ card.grid(row=0, column=4, sticky="nsew", padx=(8, 0))
def _choose_folder(self) -> None:
selected = filedialog.askdirectory(title="Choose a Python project folder")
{"path":"c:\\Projects\\securelocal-ai\\ui\\main_window.py","old_str":" self.summary.configure(\n text=(\n f\"{result.files_scanned} files scanned · \"\n ...
diff --git a/c:/Projects/securelocal-ai/ui/main_window.py b/c:/Projects/securelocal-ai/ui/main_window.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/main_window.py
+++ b/c:/Projects/securelocal-ai/ui/main_window.py
@@ -282,13 +282,16 @@
self._populate_findings()
self._update_summary_cards(result)
counts = result.counts_by_severity
- self.summary.configure(
- text=(
- f"{result.files_scanned} files scanned · "
- f"{counts['Critical']} critical · {counts['High']} high · "
- f"{counts['Medium']} medium · {counts['Low']} low"
- )
- )
+ summary_bits = [
+ f"{result.files_scanned} files scanned",
+ f"{counts['Critical']} critical",
+ f"{counts['High']} high",
+ f"{counts['Medium']} medium",
+ f"{counts['Low']} low",
+ ]
+ if counts["Info"]:
+ summary_bits.append(f"{counts['Info']} info")
+ self.summary.configure(text=" · ".join(summary_bits))
skipped_note = f" {len(result.skipped_files)} file(s) skipped." if result.skipped_files else ""
limit_note = " A safety limit was reached; this scan may be incomplete." if result.truncated else ""
self.status.set(
{
"path": "c:\\Projects\\securelocal-ai\\ui\\main_window.py",
"view_range": [
80,
220
]
}
diff --git a/c:/Projects/securelocal-ai/ui/main_window.py b/c:/Projects/securelocal-ai/ui/main_window.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/main_window.py
+++ b/c:/Projects/securelocal-ai/ui/main_window.py
@@ -80,141 +80,141 @@
ttk.Button(
folder_frame,
text="Choose folder…",
command=self._choose_folder,
style="Secondary.TButton",
).grid(row=0, column=1, padx=(0, 10))
self.scan_button = ttk.Button(
folder_frame,
text="Scan project",
command=self._start_scan,
style="Primary.TButton",
)
self.scan_button.grid(row=0, column=2)
self.summary_cards_frame = ttk.Frame(container, padding=(0, 0, 0, 8))
self.summary_cards_frame.grid(row=2, column=0, sticky="ew")
self.summary_cards_frame.columnconfigure(0, weight=1)
self.summary_cards_frame.columnconfigure(1, weight=1)
self.summary_cards_frame.columnconfigure(2, weight=1)
self.summary_cards_frame.columnconfigure(3, weight=1)
self.summary_cards_frame.columnconfigure(4, weight=1)
self._build_summary_cards()
toolbar = ttk.Frame(container, style="Toolbar.TFrame", padding=(12, 10))
toolbar.grid(row=3, column=0, sticky="ew", pady=(0, 10))
toolbar.columnconfigure(4, weight=1)
ttk.Label(toolbar, text="Severity", style="Toolbar.TLabel").grid(row=0, column=0, sticky="w")
self.severity_filter = tk.StringVar(value="All")
self.severity_combo = ttk.Combobox(
toolbar,
textvariable=self.severity_filter,
state="readonly",
values=("All", *(severity.value for severity in Severity)),
width=12,
)
self.severity_combo.grid(row=0, column=1, sticky="w", padx=(6, 18))
self.severity_combo.bind("<<ComboboxSelected>>", self._apply_filters)
ttk.Label(toolbar, text="Search", style="Toolbar.TLabel").grid(row=0, column=2, sticky="w")
self.search_text = tk.StringVar()
search = ttk.Entry(toolbar, textvariable=self.search_text, width=30)
search.grid(row=0, column=3, sticky="ew", padx=(6, 12))
self.search_text.trace_add("write", self._apply_filters)
self.summary = ttk.Label(toolbar, text="Choose a project folder to begin.", style="Toolbar.TLabel")
self.summary.grid(row=0, column=4, sticky="e")
actions = ttk.Frame(container, padding=(0, 0, 0, 8))
actions.grid(row=4, column=0, sticky="ew")
self.ai_model = tk.StringVar(value=DEFAULT_MODEL)
ttk.Label(actions, text="Local model", style="Section.TLabel").pack(side="left")
ttk.Entry(actions, textvariable=self.ai_model, width=22).pack(side="left", padx=(6, 8))
self.explain_button = ttk.Button(
actions,
text="Explain selected with local AI",
command=self._explain_selected,
state="disabled",
style="Primary.TButton",
)
self.explain_button.pack(side="left", padx=(0, 12))
self.export_json_button = ttk.Button(
actions,
text="Export JSON…",
command=self._export_json,
state="disabled",
style="Secondary.TButton",
)
self.export_json_button.pack(side="right", padx=(8, 0))
self.export_md_button = ttk.Button(
actions,
text="Export Markdown…",
command=self._export_markdown,
state="disabled",
style="Secondary.TButton",
)
self.export_md_button.pack(side="right")
content = ttk.Panedwindow(container, orient="vertical")
content.grid(row=5, column=0, sticky="nsew")
table_frame = ttk.Frame(content, style="Panel.TFrame")
table_frame.columnconfigure(0, weight=1)
table_frame.rowconfigure(0, weight=1)
columns = ("severity", "rule", "file", "line", "title")
self.table = ttk.Treeview(table_frame, columns=columns, show="headings", selectmode="browse")
self.table.tag_configure("oddrow", background="#F8FAFC")
self.table.tag_configure("Critical", foreground="#B42318")
self.table.tag_configure("High", foreground="#D97706")
self.table.tag_configure("Medium", foreground="#AC6B0C")
self.table.tag_configure("Low", foreground="#2F6FED")
self.table.tag_configure("Info", foreground="#475467")
for column, heading, width in (
("severity", "Severity", 100),
("rule", "Rule", 86),
("file", "File", 260),
("line", "Line", 62),
("title", "Finding", 360),
):
self.table.heading(column, text=heading)
self.table.column(column, width=width, anchor="w")
self.table.grid(row=0, column=0, sticky="nsew")
self.table.bind("<<TreeviewSelect>>", self._selection_changed)
vertical = ttk.Scrollbar(table_frame, orient="vertical", command=self.table.yview)
vertical.grid(row=0, column=1, sticky="ns")
self.table.configure(yscrollcommand=vertical.set)
horizontal = ttk.Scrollbar(table_frame, orient="horizontal", command=self.table.xview)
horizontal.grid(row=1, column=0, sticky="ew")
self.table.configure(xscrollcommand=horizontal.set)
self.details = FindingDetails(content)
content.add(table_frame, weight=3)
content.add(self.details, weight=2)
self.status = tk.StringVar(value="Scanning uses local deterministic rules. Source files are not changed or uploaded.")
ttk.Label(container, textvariable=self.status, style="Status.TLabel").grid(row=6, column=0, sticky="ew", pady=(8, 0))
def _build_summary_cards(self) -> None:
self.summary_cards = {}
labels = [
("Total findings", "total"),
("High", "high"),
("Medium", "medium"),
("Low", "low"),
("Info", "info"),
]
for index, (label_text, key) in enumerate(labels):
card = ttk.Frame(self.summary_cards_frame, padding=(12, 10), style="Card.TFrame")
card.grid(row=0, column=index, sticky="nsew", padx=(0 if index == 0 else 8, 0))
if key == "info":
card.grid_remove()
ttk.Label(card, text=label_text, style="SummaryLabel.TLabel").pack(anchor="w")
value = tk.StringVar(value="0")
ttk.Label(card, textvariable=value, style="SummaryValue.TLabel").pack(anchor="w", pady=(4, 0))
self.summary_cards[key] = (card, value)
self._update_summary_cards(None)
def _update_summary_cards(self, result: ScanResult | None) -> None:
cards = {
"total": "0",
"high": "0",
"medium": "0",
"low": "0",
"info": "0",
}
if result is not None:
{
"path": "c:\\Projects\\securelocal-ai\\ui\\main_window.py",
"view_range": [
220,
360
]
}
diff --git a/c:/Projects/securelocal-ai/ui/main_window.py b/c:/Projects/securelocal-ai/ui/main_window.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/main_window.py
+++ b/c:/Projects/securelocal-ai/ui/main_window.py
@@ -220,141 +220,141 @@
if result is not None:
counts = result.counts_by_severity
cards["total"] = str(len(result.findings))
cards["high"] = str(counts["High"])
cards["medium"] = str(counts["Medium"])
cards["low"] = str(counts["Low"])
cards["info"] = str(counts["Info"])
for key, value in cards.items():
if key in self.summary_cards:
self.summary_cards[key][1].set(value)
info_card = self.summary_cards.get("info")
if info_card is not None:
card, _ = info_card
if cards["info"] == "0":
card.grid_remove()
else:
card.grid(row=0, column=4, sticky="nsew", padx=(8, 0))
def _choose_folder(self) -> None:
selected = filedialog.askdirectory(title="Choose a Python project folder")
if selected:
self.project_path.set(selected)
def _start_scan(self) -> None:
selected = self.project_path.get().strip()
if not selected or not Path(selected).is_dir():
messagebox.showerror("Choose a project", "Select an existing project folder first.")
return
self.scan_button.configure(state="disabled")
self.status.set("Scanning Python files locally…")
self.summary.configure(text="Scan in progress")
self._clear_findings()
thread = threading.Thread(target=self._scan_worker, args=(selected,), daemon=True)
thread.start()
def _scan_worker(self, selected: str) -> None:
try:
self.events.put(("scan", scan_project(selected)))
except (OSError, ValueError, RecursionError) as error:
self.events.put(("error", str(error)))
def _process_events(self) -> None:
try:
while True:
event, payload = self.events.get_nowait()
if event == "scan":
if isinstance(payload, ScanResult):
self._show_result(payload)
elif event == "explanation":
if isinstance(payload, tuple) and len(payload) == 2:
self._show_explanation(payload)
elif event == "error":
self._handle_error(payload)
except queue.Empty:
pass
self.root.after(100, self._process_events)
def _show_result(self, result: ScanResult) -> None:
self.result = result
self.scan_button.configure(state="normal")
self.export_json_button.configure(state="normal")
self.export_md_button.configure(state="normal")
self._populate_findings()
self._update_summary_cards(result)
counts = result.counts_by_severity
summary_bits = [
f"{result.files_scanned} files scanned",
f"{counts['Critical']} critical",
f"{counts['High']} high",
f"{counts['Medium']} medium",
f"{counts['Low']} low",
]
if counts["Info"]:
summary_bits.append(f"{counts['Info']} info")
self.summary.configure(text=" · ".join(summary_bits))
skipped_note = f" {len(result.skipped_files)} file(s) skipped." if result.skipped_files else ""
limit_note = " A safety limit was reached; this scan may be incomplete." if result.truncated else ""
self.status.set(
f"Scan complete. Findings are deterministic; no files were changed or uploaded."
f"{skipped_note}{limit_note}"
)
def _populate_findings(self) -> None:
for item in self.table.get_children():
self.table.delete(item)
self.findings_by_id.clear()
if self.result is None:
return
for index, finding in enumerate(self.result.findings):
item_id = f"finding-{index}"
self.findings_by_id[item_id] = finding
tags = (finding.severity.value,)
if index % 2 == 1:
tags = tags + ("oddrow",)
self.table.insert(
"",
"end",
iid=item_id,
tags=tags,
values=(
finding.severity.value,
finding.rule_id,
finding.file_path,
finding.line,
finding.title,
),
)
self._apply_filters()
def _apply_filters(self, *_: object) -> None:
query = self.search_text.get().strip().casefold()
severity = self.severity_filter.get()
for item_id, finding in self.findings_by_id.items():
matches_severity = severity == "All" or finding.severity.value == severity
haystack = " ".join((finding.rule_id, finding.title, finding.file_path, finding.evidence)).casefold()
visible = matches_severity and query in haystack
if visible and not self.table.exists(item_id):
self.table.reattach(item_id, "", "end")
elif not visible and self.table.exists(item_id):
self.table.detach(item_id)
def _selection_changed(self, _event: tk.Event) -> None:
selected = self.table.selection()
finding = self.findings_by_id.get(selected[0]) if selected else None
self.details.show_finding(finding)
self.explain_button.configure(state="normal" if finding else "disabled")
def _clear_findings(self) -> None:
self.result = None
self.findings_by_id.clear()
for item in self.table.get_children():
self.table.delete(item)
self._update_summary_cards(None)
self.summary.configure(text="Choose a project folder to begin.")
self.details.show_finding(None)
self.explain_button.configure(state="disabled")
self.export_json_button.configure(state="disabled")
self.export_md_button.configure(state="disabled")
def _handle_error(self, payload: object) -> None:
self.scan_button.configure(state="normal")
The redesign is in place; I’m validating the existing suite to confirm the scanner, exporter, and Ollama UI behavior still hold after the visual refresh.
{"command":"cd c:\\Projects\\securelocal-ai; python -m unittest discover -s tests -v","description":"Run the project test suite to validate the UI redesign against existing scanner and report behavior...
Shell output was automatically compacted. Compacted output below. Original at C:\Users\Kushal\AppData\Local\Temp\original-output-1792144992147-61398d0ed7cb40f387ce627323e15288.txt; only use if exact omitted lines are needed.
[unittest progress: omitted 46 non-diagnostic line(s)]
[unittest progress: omitted 1 non-diagnostic line(s)]
Ran 46 tests in 0.312s
OK
<shellId: 4 completed with exit code 0>
Done — I redesigned the Tkinter UI without changing the scanner logic or security behavior.
Files updated:
- ui/main_window.py
- ui/finding_details.py
What changed:
- Modern light-themed security workbench with navy branding, cleaner spacing, and consistent button styling
- Organized project selection + scan controls into a dedicated section
- Added live summary cards for total/high/medium/low findings, with Info shown only when present
- Refreshed filtering toolbar and findings table with better readability, row styling, and severity color cues
- Reworked the detail panel into a structured, readable view for title, severity, rule, evidence, limitations, and remediation
- Preserved the existing scan, filter, export, and local Ollama request flows and error handling
Validation:
- Ran: python -m unittest discover -s tests -v
- Result: 46 tests passed, OK
There is a regression after the Tkinter UI redesign.
When I run python app.py and click Scan project on tests/fixtures/vulnerable, the scan results are not displayed. The terminal shows:
_tkinter.TclError: Item finding-6 already exists
The traceback points to:
-
ui/main_window.py, line 267:_process_events -
ui/main_window.py, line 282:_show_result -
ui/main_window.py, line 314:_populate_findings -
self.table.insert(...)Please investigate and fix the root cause.
Requirements:
- Inspect
_populate_findings(),_show_result(), and the Treeview initialization. - Determine why a Treeview item ID such as
finding-6is being inserted more than once. - Check whether the table is properly cleared before repopulating it after a scan, and whether finding IDs are unique.
- Fix the underlying issue while preserving stable selection behavior and any severity tags or row styling.
- Ensure repeated scans of the same folder and scans of different folders both work without duplicate-ID errors.
- Preserve the summary counts, severity filters, text search, finding details, exports, and local Ollama integration.
- Do not change scanner rules, finding generation, security controls, or report schemas to work around this UI bug.
- Add or update focused tests if the table-population logic can be tested safely.
- Run
python -m unittest discover -s tests -vandpython -m compileall -q ., and report the actual results. Make the smallest appropriate change. Do not rewrite the UI or modify unrelated files. Do not commit or push anything.
After fixing the issue, explain its root cause and list the files changed.
{
"skill": "python-fact-grounded-coding"
}
Skill "python-fact-grounded-coding" loaded successfully. Follow the instructions in the skill context.
I’ll trace the Treeview creation and repopulation path, then check whether item IDs collide across insertions or stale rows survive a scan. I’ll keep the fix within the UI and focused tests, then run both requested validation commands.
{
"path": "c:\\Projects\\securelocal-ai\\ui\\main_window.py",
"view_range": [
1,
380
]
}
diff --git a/c:/Projects/securelocal-ai/ui/main_window.py b/c:/Projects/securelocal-ai/ui/main_window.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/main_window.py
+++ b/c:/Projects/securelocal-ai/ui/main_window.py
@@ -1,380 +1,380 @@
from __future__ import annotations
import queue
import threading
import tkinter as tk
from pathlib import Path
from tkinter import filedialog, messagebox, ttk
from ai.ollama_client import DEFAULT_MODEL, OllamaClient, OllamaError
from reports.exporter import export_json, export_markdown
from scanner.engine import scan_project
from scanner.models import Finding, ScanResult, Severity
from ui.finding_details import FindingDetails
class SecureLocalApp:
def __init__(self, root: tk.Tk) -> None:
self.root = root
self.root.title("SecureLocal AI — Code Security Auditor")
self.root.geometry("1120x780")
self.root.minsize(860, 600)
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
self.root.configure(bg="#F3F6FB")
self.events: queue.Queue[tuple[str, object]] = queue.Queue()
self.result: ScanResult | None = None
self.findings_by_id: dict[str, Finding] = {}
self.summary_cards: dict[str, tuple[ttk.Label, tk.StringVar]] = {}
self._configure_style()
self._build_ui()
self.root.after(100, self._process_events)
def _configure_style(self) -> None:
style = ttk.Style(self.root)
style.theme_use("clam")
style.configure(".", font=("Segoe UI", 10), background="#F3F6FB", foreground="#0F172A")
style.configure("Header.TFrame", background="#112A46")
style.configure("Header.TLabel", font=("Segoe UI", 20, "bold"), background="#112A46", foreground="#FFFFFF")
style.configure("Subtitle.TLabel", font=("Segoe UI", 10), background="#112A46", foreground="#D7E2F3")
style.configure("Section.TLabel", font=("Segoe UI", 10, "bold"), background="#F3F6FB", foreground="#1E293B")
style.configure("Section.TLabelframe", background="#F3F6FB", bordercolor="#D4DDEB", lightcolor="#D4DDEB", darkcolor="#D4DDEB")
style.configure("Section.TLabelframe.Label", font=("Segoe UI", 10, "bold"), background="#F3F6FB", foreground="#1E293B")
style.configure("Card.TFrame", background="#FFFFFF", relief="solid", borderwidth=1)
style.configure("SummaryLabel.TLabel", font=("Segoe UI", 9, "bold"), background="#FFFFFF", foreground="#475467")
style.configure("SummaryValue.TLabel", font=("Segoe UI", 18, "bold"), background="#FFFFFF", foreground="#0F172A")
style.configure("Toolbar.TFrame", background="#EEF3FA")
style.configure("Toolbar.TLabel", background="#EEF3FA", foreground="#475467", font=("Segoe UI", 9, "bold"))
style.configure("Status.TLabel", background="#F3F6FB", foreground="#475467", wraplength=1100)
style.configure("Primary.TButton", font=("Segoe UI", 10, "bold"), background="#2F6FED", foreground="#FFFFFF")
style.map("Primary.TButton", background=[("active", "#255ad6"), ("pressed", "#1f4bb7")], foreground=[("disabled", "#E2E8F0")])
style.configure("Secondary.TButton", background="#FFFFFF", foreground="#0F172A")
style.map("Secondary.TButton", background=[("active", "#EEF3FA"), ("pressed", "#E2E8F0")])
style.configure("Treeview", rowheight=26, background="#FFFFFF", fieldbackground="#FFFFFF", borderwidth=0)
style.map("Treeview", background=[("selected", "#D9E7FF")], foreground=[("selected", "#0F172A")])
style.configure("Treeview.Heading", font=("Segoe UI", 10, "bold"), background="#E2E8F0", foreground="#0F172A")
style.map("Treeview.Heading", background=[("active", "#D9E7FF")])
style.configure("Panel.TFrame", background="#FFFFFF")
def _build_ui(self) -> None:
container = ttk.Frame(self.root, padding=16, style="Panel.TFrame")
container.grid(row=0, column=0, sticky="nsew")
container.columnconfigure(0, weight=1)
container.rowconfigure(5, weight=1)
header = ttk.Frame(container, style="Header.TFrame", padding=(18, 16))
header.grid(row=0, column=0, sticky="ew")
header.columnconfigure(0, weight=1)
ttk.Label(header, text="SecureLocal AI", style="Header.TLabel").grid(row=0, column=0, sticky="w")
ttk.Label(
header,
text="Offline-first Python code security checks for local review and remediation.",
style="Subtitle.TLabel",
).grid(row=1, column=0, sticky="w", pady=(4, 0))
folder_frame = ttk.LabelFrame(container, text="Project selection", padding=12)
folder_frame.grid(row=1, column=0, sticky="ew", pady=(16, 10))
folder_frame.columnconfigure(0, weight=1)
self.project_path = tk.StringVar()
ttk.Entry(folder_frame, textvariable=self.project_path, width=60).grid(row=0, column=0, sticky="ew", padx=(0, 10))
ttk.Button(
folder_frame,
text="Choose folder…",
command=self._choose_folder,
style="Secondary.TButton",
).grid(row=0, column=1, padx=(0, 10))
self.scan_button = ttk.Button(
folder_frame,
text="Scan project",
command=self._start_scan,
style="Primary.TButton",
)
self.scan_button.grid(row=0, column=2)
self.summary_cards_frame = ttk.Frame(container, padding=(0, 0, 0, 8))
self.summary_cards_frame.grid(row=2, column=0, sticky="ew")
self.summary_cards_frame.columnconfigure(0, weight=1)
self.summary_cards_frame.columnconfigure(1, weight=1)
self.summary_cards_frame.columnconfigure(2, weight=1)
self.summary_cards_frame.columnconfigure(3, weight=1)
self.summary_cards_frame.columnconfigure(4, weight=1)
self._build_summary_cards()
toolbar = ttk.Frame(container, style="Toolbar.TFrame", padding=(12, 10))
toolbar.grid(row=3, column=0, sticky="ew", pady=(0, 10))
toolbar.columnconfigure(4, weight=1)
ttk.Label(toolbar, text="Severity", style="Toolbar.TLabel").grid(row=0, column=0, sticky="w")
self.severity_filter = tk.StringVar(value="All")
self.severity_combo = ttk.Combobox(
toolbar,
textvariable=self.severity_filter,
state="readonly",
values=("All", *(severity.value for severity in Severity)),
width=12,
)
self.severity_combo.grid(row=0, column=1, sticky="w", padx=(6, 18))
self.severity_combo.bind("<<ComboboxSelected>>", self._apply_filters)
ttk.Label(toolbar, text="Search", style="Toolbar.TLabel").grid(row=0, column=2, sticky="w")
self.search_text = tk.StringVar()
search = ttk.Entry(toolbar, textvariable=self.search_text, width=30)
search.grid(row=0, column=3, sticky="ew", padx=(6, 12))
self.search_text.trace_add("write", self._apply_filters)
self.summary = ttk.Label(toolbar, text="Choose a project folder to begin.", style="Toolbar.TLabel")
self.summary.grid(row=0, column=4, sticky="e")
actions = ttk.Frame(container, padding=(0, 0, 0, 8))
actions.grid(row=4, column=0, sticky="ew")
self.ai_model = tk.StringVar(value=DEFAULT_MODEL)
ttk.Label(actions, text="Local model", style="Section.TLabel").pack(side="left")
ttk.Entry(actions, textvariable=self.ai_model, width=22).pack(side="left", padx=(6, 8))
self.explain_button = ttk.Button(
actions,
text="Explain selected with local AI",
command=self._explain_selected,
state="disabled",
style="Primary.TButton",
)
self.explain_button.pack(side="left", padx=(0, 12))
self.export_json_button = ttk.Button(
actions,
text="Export JSON…",
command=self._export_json,
state="disabled",
style="Secondary.TButton",
)
self.export_json_button.pack(side="right", padx=(8, 0))
self.export_md_button = ttk.Button(
actions,
text="Export Markdown…",
command=self._export_markdown,
state="disabled",
style="Secondary.TButton
{
"path": "c:\\Projects\\securelocal-ai\\tests\\test_ui_ollama.py",
"view_range": [
1,
240
]
}
diff --git a/c:/Projects/securelocal-ai/tests/test_ui_ollama.py b/c:/Projects/securelocal-ai/tests/test_ui_ollama.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/tests/test_ui_ollama.py
+++ b/c:/Projects/securelocal-ai/tests/test_ui_ollama.py
@@ -1,240 +1,240 @@
from __future__ import annotations
import queue
import unittest
from unittest.mock import patch
from ai.ollama_client import OllamaError
from scanner.models import Finding, Severity
from ui.main_window import SecureLocalApp
class OllamaUiTests(unittest.TestCase):
def test_explanation_error_is_queued_for_ui_without_escaping_worker(self):
app = SecureLocalApp.__new__(SecureLocalApp)
app.events = queue.Queue()
finding = Finding(
"SLA001", "Dynamic code execution", Severity.HIGH, "x.py", 1, 1,
"eval(value)", "description", "remediation", "High", "limitations"
)
with patch(
"ui.main_window.OllamaClient.explain_finding",
side_effect=OllamaError("Ollama is unavailable at the configured loopback address."),
):
app._explanation_worker(finding, "qwen2.5-coder:3b")
self.assertEqual(
app.events.get_nowait(),
(
"explanation",
(False, "Ollama is unavailable at the configured loopback address."),
),
)
if __name__ == "__main__":
unittest.main()
{
"pattern": "_populate_findings|finding-\\{|Treeview\\(|reattach|detach",
"paths": "c:\\Projects\\securelocal-ai",
"glob": "*.py",
"output_mode": "content",
"-n": true,
"head_limit": 100
}
c:\Projects\securelocal-ai\ui\main_window.py:161: self.table = ttk.Treeview(table_frame, columns=columns, show="headings", selectmode="browse")
c:\Projects\securelocal-ai\ui\main_window.py:282: self._populate_findings()
c:\Projects\securelocal-ai\ui\main_window.py:302: def _populate_findings(self) -> None:
c:\Projects\securelocal-ai\ui\main_window.py:309: item_id = f"finding-{index}"
c:\Projects\securelocal-ai\ui\main_window.py:337: self.table.reattach(item_id, "", "end")
c:\Projects\securelocal-ai\ui\main_window.py:339: self.table.detach(item_id)
{
"path": "c:\\Projects\\securelocal-ai\\ui\\main_window.py",
"view_range": [
150,
355
]
}
diff --git a/c:/Projects/securelocal-ai/ui/main_window.py b/c:/Projects/securelocal-ai/ui/main_window.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/main_window.py
+++ b/c:/Projects/securelocal-ai/ui/main_window.py
@@ -150,206 +150,206 @@
state="disabled",
style="Secondary.TButton",
)
self.export_md_button.pack(side="right")
content = ttk.Panedwindow(container, orient="vertical")
content.grid(row=5, column=0, sticky="nsew")
table_frame = ttk.Frame(content, style="Panel.TFrame")
table_frame.columnconfigure(0, weight=1)
table_frame.rowconfigure(0, weight=1)
columns = ("severity", "rule", "file", "line", "title")
self.table = ttk.Treeview(table_frame, columns=columns, show="headings", selectmode="browse")
self.table.tag_configure("oddrow", background="#F8FAFC")
self.table.tag_configure("Critical", foreground="#B42318")
self.table.tag_configure("High", foreground="#D97706")
self.table.tag_configure("Medium", foreground="#AC6B0C")
self.table.tag_configure("Low", foreground="#2F6FED")
self.table.tag_configure("Info", foreground="#475467")
for column, heading, width in (
("severity", "Severity", 100),
("rule", "Rule", 86),
("file", "File", 260),
("line", "Line", 62),
("title", "Finding", 360),
):
self.table.heading(column, text=heading)
self.table.column(column, width=width, anchor="w")
self.table.grid(row=0, column=0, sticky="nsew")
self.table.bind("<<TreeviewSelect>>", self._selection_changed)
vertical = ttk.Scrollbar(table_frame, orient="vertical", command=self.table.yview)
vertical.grid(row=0, column=1, sticky="ns")
self.table.configure(yscrollcommand=vertical.set)
horizontal = ttk.Scrollbar(table_frame, orient="horizontal", command=self.table.xview)
horizontal.grid(row=1, column=0, sticky="ew")
self.table.configure(xscrollcommand=horizontal.set)
self.details = FindingDetails(content)
content.add(table_frame, weight=3)
content.add(self.details, weight=2)
self.status = tk.StringVar(value="Scanning uses local deterministic rules. Source files are not changed or uploaded.")
ttk.Label(container, textvariable=self.status, style="Status.TLabel").grid(row=6, column=0, sticky="ew", pady=(8, 0))
def _build_summary_cards(self) -> None:
self.summary_cards = {}
labels = [
("Total findings", "total"),
("High", "high"),
("Medium", "medium"),
("Low", "low"),
("Info", "info"),
]
for index, (label_text, key) in enumerate(labels):
card = ttk.Frame(self.summary_cards_frame, padding=(12, 10), style="Card.TFrame")
card.grid(row=0, column=index, sticky="nsew", padx=(0 if index == 0 else 8, 0))
if key == "info":
card.grid_remove()
ttk.Label(card, text=label_text, style="SummaryLabel.TLabel").pack(anchor="w")
value = tk.StringVar(value="0")
ttk.Label(card, textvariable=value, style="SummaryValue.TLabel").pack(anchor="w", pady=(4, 0))
self.summary_cards[key] = (card, value)
self._update_summary_cards(None)
def _update_summary_cards(self, result: ScanResult | None) -> None:
cards = {
"total": "0",
"high": "0",
"medium": "0",
"low": "0",
"info": "0",
}
if result is not None:
counts = result.counts_by_severity
cards["total"] = str(len(result.findings))
cards["high"] = str(counts["High"])
cards["medium"] = str(counts["Medium"])
cards["low"] = str(counts["Low"])
cards["info"] = str(counts["Info"])
for key, value in cards.items():
if key in self.summary_cards:
self.summary_cards[key][1].set(value)
info_card = self.summary_cards.get("info")
if info_card is not None:
card, _ = info_card
if cards["info"] == "0":
card.grid_remove()
else:
card.grid(row=0, column=4, sticky="nsew", padx=(8, 0))
def _choose_folder(self) -> None:
selected = filedialog.askdirectory(title="Choose a Python project folder")
if selected:
self.project_path.set(selected)
def _start_scan(self) -> None:
selected = self.project_path.get().strip()
if not selected or not Path(selected).is_dir():
messagebox.showerror("Choose a project", "Select an existing project folder first.")
return
self.scan_button.configure(state="disabled")
self.status.set("Scanning Python files locally…")
self.summary.configure(text="Scan in progress")
self._clear_findings()
thread = threading.Thread(target=self._scan_worker, args=(selected,), daemon=True)
thread.start()
def _scan_worker(self, selected: str) -> None:
try:
self.events.put(("scan", scan_project(selected)))
except (OSError, ValueError, RecursionError) as error:
self.events.put(("error", str(error)))
def _process_events(self) -> None:
try:
while True:
event, payload = self.events.get_nowait()
if event == "scan":
if isinstance(payload, ScanResult):
self._show_result(payload)
elif event == "explanation":
if isinstance(payload, tuple) and len(payload) == 2:
self._show_explanation(payload)
elif event == "error":
self._handle_error(payload)
except queue.Empty:
pass
self.root.after(100, self._process_events)
def _show_result(self, result: ScanResult) -> None:
self.result = result
self.scan_button.configure(state="normal")
self.export_json_button.configure(state="normal")
self.export_md_button.configure(state="normal")
self._populate_findings()
self._update_summary_cards(result)
counts = result.counts_by_severity
summary_bits = [
f"{result.files_scanned} files scanned",
f"{counts['Critical']} critical",
f"{counts['High']} high",
f"{counts['Medium']} medium",
f"{counts['Low']} low",
]
if counts["Info"]:
summary_bits.append(f"{counts['Info']} info")
self.summary.configure(text=" · ".join(summary_bits))
skipped_note = f" {len(result.skipped_files)} file(s) skipped." if result.skipped_files else ""
limit_note = " A safety limit was reached; this scan may be incomplete." if result.truncated else ""
self.status.set(
f"Scan complete. Findings are deterministic; no files were changed or uploaded."
f"{skipped_note}{limit_note}"
)
def _populate_findings(self) -> None:
for item in self.table.get_children():
self.table.delete(item)
self.findings_by_id.clear()
if self.result is None:
return
for index, finding in enumerate(self.result.findings):
item_id = f"finding-{index}"
self.findings_by_id[item_id] = finding
tags = (finding.severity.value,)
if index % 2 == 1:
tags = tags + ("oddrow",)
self.table.insert(
"",
"end",
iid=item_id,
tags=tags,
values=(
finding.severity.value,
finding.rule_id,
finding.file_path,
finding.line,
finding.title,
),
)
self._apply_filters()
def _apply_filters(self, *_: object) -> None:
query = self.search_text.get().strip().casefold()
severity = self.severity_filter.get()
for item_id, finding in self.findings_by_id.items(
{
"path": "c:\\Projects\\securelocal-ai\\scanner\\models.py",
"view_range": [
1,
100
]
}
diff --git a/c:/Projects/securelocal-ai/scanner/models.py b/c:/Projects/securelocal-ai/scanner/models.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/scanner/models.py
+++ b/c:/Projects/securelocal-ai/scanner/models.py
@@ -1,100 +1,100 @@
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
class Severity(str, Enum):
CRITICAL = "Critical"
HIGH = "High"
MEDIUM = "Medium"
LOW = "Low"
INFO = "Info"
@dataclass(frozen=True)
class Finding:
rule_id: str
title: str
severity: Severity
file_path: str
line: int
column: int
evidence: str
description: str
remediation: str
confidence: str
limitations: str
@dataclass(frozen=True)
class SkippedFile:
file_path: str
reason: str
@dataclass(frozen=True)
class ScanResult:
project_name: str
scanned_at: str
files_scanned: int
skipped_files: tuple[SkippedFile, ...]
findings: tuple[Finding, ...]
truncated: bool
@property
def counts_by_severity(self) -> dict[str, int]:
counts = {severity.value: 0 for severity in Severity}
for finding in self.findings:
counts[finding.severity.value] += 1
return counts
{
"path": "c:\\Projects\\securelocal-ai\\tests\\test_scanner.py",
"view_range": [
1,
120
]
}
diff --git a/c:/Projects/securelocal-ai/tests/test_scanner.py b/c:/Projects/securelocal-ai/tests/test_scanner.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/tests/test_scanner.py
+++ b/c:/Projects/securelocal-ai/tests/test_scanner.py
@@ -1,120 +1,120 @@
from __future__ import annotations
import os
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from scanner.engine import scan_project
FIXTURES = Path(__file__).parent / "fixtures"
class ScannerTests(unittest.TestCase):
def test_vulnerable_fixture_reports_expected_rules_and_redacted_secret(self):
result = scan_project(FIXTURES / "vulnerable")
ids = {finding.rule_id for finding in result.findings}
self.assertEqual(
ids,
{"SLA001", "SLA002", "SLA003", "SLA004", "SLA005", "SLA006", "SLA007"},
)
secret = "ghp_abcdefghijklmnopqrstuvwxyz012345"
self.assertTrue(all(secret not in finding.evidence for finding in result.findings))
self.assertEqual(result.files_scanned, 1)
def test_clean_fixture_has_no_findings(self):
result = scan_project(FIXTURES / "clean")
self.assertEqual(result.findings, ())
self.assertEqual(result.files_scanned, 1)
def test_invalid_syntax_is_skipped_without_source_text_in_reason(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "broken.py"
path.write_text("this is not valid Python !!!\n", encoding="utf-8")
result = scan_project(directory)
self.assertEqual(result.findings, ())
self.assertEqual(result.files_scanned, 0)
self.assertIn("Invalid Python syntax", result.skipped_files[0].reason)
self.assertNotIn("this is not valid", result.skipped_files[0].reason)
def test_large_and_invalid_encoding_files_are_skipped(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "large.py").write_text("x" * 20, encoding="utf-8")
(root / "encoding.py").write_bytes(b"\xff\xfe\x00")
result = scan_project(root, max_file_bytes=10)
reasons = {item.file_path: item.reason for item in result.skipped_files}
self.assertEqual(reasons["large.py"], "File exceeds the per-file size limit.")
self.assertEqual(reasons["encoding.py"], "File is not valid UTF-8 text.")
def test_declared_python_encoding_is_respected(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "latin1.py"
path.write_bytes("# coding: latin-1\nlabel = 'caf\xe9'\neval(user_input)\n".encode("latin-1"))
result = scan_project(directory)
self.assertEqual(result.files_scanned, 1)
self.assertEqual([item.rule_id for item in result.findings], ["SLA001"])
def test_scan_results_are_deterministic(self):
first = scan_project(FIXTURES / "vulnerable")
second = scan_project(FIXTURES / "vulnerable")
self.assertEqual(first.findings, second.findings)
self.assertEqual(first.files_scanned, second.files_scanned)
self.assertEqual(first.skipped_files, second.skipped_files)
def test_scan_does_not_execute_or_modify_source_files(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
marker = root / "should-not-exist"
source = root / "payload.py"
original = f"from pathlib import Path\nPath({str(marker)!r}).write_text('executed')\neval('1 + 1')\n"
source.write_text(original, encoding="utf-8")
result = scan_project(root)
self.assertIn("SLA001", {finding.rule_id for finding in result.findings})
self.assertFalse(marker.exists())
self.assertEqual(source.read_text(encoding="utf-8"), original)
def test_file_identity_change_before_open_is_skipped(self):
with tempfile.TemporaryDirectory() as directory, tempfile.TemporaryDirectory() as outside:
root = Path(directory)
source = root / "sample.py"
external = Path(outside) / "outside.py"
source.write_text("eval(user_input)\n", encoding="utf-8")
external.write_text("eval(external_input)\n", encoding="utf-8")
real_open = os.open
def open_external(_path, flags):
return real_open(external, flags)
with patch("scanner.engine.os.open", side_effect=open_external):
result = scan_project(root)
self.assertEqual(result.files_scanned, 0)
self.assertEqual(result.findings, ())
self.assertEqual(
result.skipped_files[0].reason,
"File changed before it could be scanned safely.",
)
if __name__ == "__main__":
unittest.main()
The duplicate is caused by filtering: _apply_filters() detaches nonmatching rows, but Treeview’s get_children() omits detached rows. On the next scan, clearing only visible children leaves those old finding-N IDs registered, so a new row reuses an existing ID. I’m adding a regression test that exercises this lifecycle before making the targeted cleanup fix.
*** Begin Patch
*** Add File: c:\Projects\securelocal-ai\tests\test_ui_table.py
+from __future__ import annotations
+
+import unittest
+
+from scanner.models import Finding, ScanResult, Severity
+from...
diff --git a/c:/Projects/securelocal-ai/tests/test_ui_table.py b/c:/Projects/securelocal-ai/tests/test_ui_table.py
create file mode 100644
index 0000000..0000000
--- a/dev/null
+++ b/c:/Projects/securelocal-ai/tests/test_ui_table.py
@@ -1,0 +1,106 @@
+from __future__ import annotations
+
+import unittest
+
+from scanner.models import Finding, ScanResult, Severity
+from ui.main_window import SecureLocalApp
+
+
+class FakeTreeview:
+ def __init__(self) -> None:
+ self.items: dict[str, tuple[tuple[str, ...], bool]] = {}
+
+ def get_children(self):
+ return tuple(item_id for item_id, (_, attached) in self.items.items() if attached)
+
+ def exists(self, item_id: str) -> bool:
+ return item_id in self.items
+
+ def delete(self, item_id: str) -> None:
+ del self.items[item_id]
+
+ def insert(self, _parent, _index, *, iid, tags, values) -> None:
+ if iid in self.items:
+ raise RuntimeError(f"Item {iid} already exists")
+ self.items[iid] = (tuple(tags), True)
+
+ def detach(self, item_id: str) -> None:
+ tags, _ = self.items[item_id]
+ self.items[item_id] = (tags, False)
+
+ def reattach(self, item_id: str, _parent, _index) -> None:
+ tags, _ = self.items[item_id]
+ self.items[item_id] = (tags, True)
+
+
+class DummyWidget:
+ def configure(self, **_kwargs) -> None:
+ pass
+
+
+def make_result(*findings: Finding) -> ScanResult:
+ return ScanResult("project", "2026-01-01T00:00:00+00:00", 1, (), findings, False)
+
+
+def make_finding(title: str, severity: Severity) -> Finding:
+ return Finding(
+ "SLA001",
+ title,
+ severity,
+ "example.py",
+ 1,
+ 1,
+ "redacted evidence",
+ "description",
+ "remediation",
+ "High",
+ "limitations",
+ )
+
+
+class FindingsTableTests(unittest.TestCase):
+ def make_app(self) -> SecureLocalApp:
+ app = SecureLocalApp.__new__(SecureLocalApp)
+ app.table = FakeTreeview()
+ app.findings_by_id = {}
+ app.search_text = type("Search", (), {"get": lambda self: ""})()
+ app.severity_filter = type("Filter", (), {"get": lambda self: "All"})()
+ app.details = DummyWidget()
+ app.explain_button = DummyWidget()
+ app.export_json_button = DummyWidget()
+ app.export_md_button = DummyWidget()
+ app.summary = DummyWidget()
+ app.summary_cards = {}
+ return app
+
+ def test_repeated_scans_clear_filtered_and_detached_rows(self):
+ app = self.make_app()
+ app.result = make_result(
+ make_finding("High finding", Severity.HIGH),
+ make_finding("Low finding", Severity.LOW),
+ )
+ app._populate_findings()
+
+ app.severity_filter = type("Filter", (), {"get": lambda self: "High"})()
+ app._apply_filters()
+ self.assertFalse(app.table.items["finding-1"][1])
+
+ app._clear_findings()
+ app.result = make_result(
+ make_finding("High finding", Severity.HIGH),
+ make_finding("Low finding", Severity.LOW),
+ )
+ app._populate_findings()
+ self.assertEqual(set(app.table.items), {"finding-0", "finding-1"})
+ self.assertEqual(app.table.items["finding-1"][0], ("Low", "oddrow"))
+
+ app._clear_findings()
+ app.result = make_result(make_finding("Different project", Severity.MEDIUM))
+ app._populate_findings()
+ self.assertEqual(set(app.table.items), {"finding-0"})
+ self.assertEqual(app.findings_by_id["finding-0"].title, "Different project")
+
+
+if __name__ == "__main__":
+ unittest.main()
+
{"command":"cd c:\\Projects\\securelocal-ai; python -m unittest tests.test_ui_table -v","description":"Run the new filtered-row regression test to confirm the existing duplicate-ID failure","mode":"sy...
test_repeated_scans_clear_filtered_and_detached_rows (tests.test_ui_table.FindingsTableTests.test_repeated_scans_clear_filtered_and_detached_rows) ... ERROR
======================================================================
ERROR: test_repeated_scans_clear_filtered_and_detached_rows (tests.test_ui_table.FindingsTableTests.test_repeated_scans_clear_filtered_and_detached_rows)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Projects\securelocal-ai\tests\test_ui_table.py", line 88, in test_repeated_scans_clear_filtered_and_detached_rows
app._clear_findings()
File "C:\Projects\securelocal-ai\ui\main_window.py", line 354, in _clear_findings
self.details.show_finding(None)
^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'DummyWidget' object has no attribute 'show_finding'
----------------------------------------------------------------------
Ran 1 test in 0.002s
FAILED (errors=1)
<shellId: 5 completed with exit code 1>
*** Begin Patch
*** Update File: c:\Projects\securelocal-ai\tests\test_ui_table.py
@@
class DummyWidget:
def configure(self, **_kwargs) -> None:
pass
+ def show_finding(self, _find...
diff --git a/c:/Projects/securelocal-ai/tests/test_ui_table.py b/c:/Projects/securelocal-ai/tests/test_ui_table.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/tests/test_ui_table.py
+++ b/c:/Projects/securelocal-ai/tests/test_ui_table.py
@@ -37,6 +37,9 @@
def configure(self, **_kwargs) -> None:
pass
+ def show_finding(self, _finding) -> None:
+ pass
+
def make_result(*findings: Finding) -> ScanResult:
return ScanResult("project", "2026-01-01T00:00:00+00:00", 1, (), findings, False)
{"command":"cd c:\\Projects\\securelocal-ai; python -m unittest tests.test_ui_table -v","description":"Confirm the regression test now reaches the duplicate Treeview item ID","mode":"sync","initial_wa...
test_repeated_scans_clear_filtered_and_detached_rows (tests.test_ui_table.FindingsTableTests.test_repeated_scans_clear_filtered_and_detached_rows) ... ERROR
======================================================================
ERROR: test_repeated_scans_clear_filtered_and_detached_rows (tests.test_ui_table.FindingsTableTests.test_repeated_scans_clear_filtered_and_detached_rows)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Projects\securelocal-ai\tests\test_ui_table.py", line 96, in test_repeated_scans_clear_filtered_and_detached_rows
app._populate_findings()
File "C:\Projects\securelocal-ai\ui\main_window.py", line 314, in _populate_findings
self.table.insert(
File "C:\Projects\securelocal-ai\tests\test_ui_table.py", line 24, in insert
raise RuntimeError(f"Item {iid} already exists")
RuntimeError: Item finding-1 already exists
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (errors=1)
<shellId: 6 completed with exit code 1>
*** Begin Patch
*** Update File: c:\Projects\securelocal-ai\ui\main_window.py
@@
def _populate_findings(self) -> None:
- for item in self.table.get_children():
- self.table.dele...
diff --git a/c:/Projects/securelocal-ai/ui/main_window.py b/c:/Projects/securelocal-ai/ui/main_window.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/main_window.py
+++ b/c:/Projects/securelocal-ai/ui/main_window.py
@@ -300,9 +300,7 @@
)
def _populate_findings(self) -> None:
- for item in self.table.get_children():
- self.table.delete(item)
- self.findings_by_id.clear()
+ self._clear_table_rows()
if self.result is None:
return
for index, finding in enumerate(self.result.findings):
@@ -326,6 +324,13 @@
)
self._apply_filters()
+ def _clear_table_rows(self) -> None:
+ item_ids = tuple(dict.fromkeys((*self.findings_by_id, *self.table.get_children())))
+ for item_id in item_ids:
+ if self.table.exists(item_id):
+ self.table.delete(item_id)
+ self.findings_by_id.clear()
+
def _apply_filters(self, *_: object) -> None:
query = self.search_text.get().strip().casefold()
severity = self.severity_filter.get()
@@ -346,9 +351,7 @@
def _clear_findings(self) -> None:
self.result = None
- self.findings_by_id.clear()
- for item in self.table.get_children():
- self.table.delete(item)
+ self._clear_table_rows()
self._update_summary_cards(None)
self.summary.configure(text="Choose a project folder to begin.")
self.details.show_finding(None)
{"command":"cd c:\\Projects\\securelocal-ai; python -m unittest tests.test_ui_table -v","description":"Run the focused Treeview repeated-scan regression test after the cleanup fix","mode":"sync","init...
test_repeated_scans_clear_filtered_and_detached_rows (tests.test_ui_table.FindingsTableTests.test_repeated_scans_clear_filtered_and_detached_rows) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
<shellId: 7 completed with exit code 0>
The regression now reproduces before the fix and passes after it. The cleanup removes every item ID still tracked by the findings map—including detached rows—plus visible Treeview rows, before resetting the map. I’m running the full test suite and compilation check now.
{
"command": "cd c:\\Projects\\securelocal-ai; python -m unittest discover -s tests -v",
"description": "Run all unit tests including the new Treeview regression test",
"mode": "sync",
"initial_wait": 120
}
Shell output was automatically compacted. Compacted output below. Original at C:\Users\Kushal\AppData\Local\Temp\original-output-1792146514377-bbcc35b91f2b4b5eb7fa6a84c4685df9.txt; only use if exact omitted lines are needed.
[unittest progress: omitted 47 non-diagnostic line(s)]
[unittest progress: omitted 1 non-diagnostic line(s)]
Ran 47 tests in 0.328s
OK
<shellId: 9 completed with exit code 0>
{
"command": "cd c:\\Projects\\securelocal-ai; python -m compileall -q .",
"description": "Compile all Python source files",
"mode": "sync",
"initial_wait": 120
}
<shellId: 8 completed with exit code 0>
{"pattern":"def _populate_findings|def _clear_table_rows|def _clear_findings|def test_repeated_scans","paths":"c:\\Projects\\securelocal-ai","glob":"*.py","output_mode":"content","-n":true,"head_limit...
c:\Projects\securelocal-ai\ui\main_window.py:302: def _populate_findings(self) -> None:
c:\Projects\securelocal-ai\ui\main_window.py:327: def _clear_table_rows(self) -> None:
c:\Projects\securelocal-ai\ui\main_window.py:352: def _clear_findings(self) -> None:
c:\Projects\securelocal-ai\tests\test_ui_table.py:79: def test_repeated_scans_clear_filtered_and_detached_rows(self):
Fixed the duplicate Treeview ID regression.
finding-N IDs are unique within each scan because they’re assigned from the finding index. The problem was that filtering detaches rows rather than deleting them, and get_children() doesn’t return detached rows. Clearing the visible rows before a scan therefore left old IDs registered in the Treeview; when the next scan reused an ID, insertion failed.
Added shared table cleanup that deletes rows tracked in findings_by_id, including detached rows, as well as visible Treeview rows. This cleanup runs before both repopulation and scan clearing. Severity tags, row styling, and per-scan IDs remain unchanged.
Files changed
- ui/main_window.py
- tests/test_ui_table.py
Validation
- python -m unittest discover -s tests -v — 47 tests passed
- python -m compileall -q . — passed (no output)
Fix the vertical layout and scrolling behavior in the existing SecureLocal AI Tkinter application.
Problem
After the recent UI redesign, the findings Treeview is too short and shows only around three findings at a time, even when the main window is maximized. The finding-details panel occupies too much vertical space. Users must scroll the findings table to see the remaining rows, making the scanning workflow inconvenient.
Inspect the current layout in ui/main_window.py and any relevant UI files before editing.
Required changes
- Give the findings table more default vertical space.
- Configure a useful initial height that shows approximately 8–12 findings when the window is maximized, depending on available screen space.
- Ensure the table's vertical scrollbar remains usable when there are more findings than fit.
- Keep the existing columns, severity tags, row styling, selection, search, and severity filtering.
Make the details panel use space more efficiently.
Avoid allowing the details panel to consume most of the window when the findings table has many rows.
Keep finding details scrollable and readable.
If appropriate, use a vertically resizable
ttk.Panedwindowor another layout mechanism already compatible with the application.Set sensible initial pane proportions so the findings table receives adequate space on startup.
Avoid forcing the user to resize panes manually every time the application opens.
Make the layout respond correctly to resizing.
When the window is maximized, display as many findings as practical.
When the window is smaller, allow both the table and details panel to remain accessible with their own scrolling.
Prevent the details content from clipping important information.
Do not introduce nested scrollbars unnecessarily.
Preserve functionality.
Do not change scanner rules, finding generation, report schemas, evidence redaction, or Ollama behavior.
Preserve the duplicate-Treeview-ID fix.
Preserve the severity summary cards, project selection, scan button, filters, AI controls, and export buttons.
Do not remove finding details merely to make the table taller.
Test the change.
Inspect the actual geometry and layout manager used by the existing widgets before choosing a fix.
Make the smallest appropriate change.
Run
python -m unittest discover -s tests -v.Run
python -m compileall -q ..Report the files changed and actual test results.
Do not commit or push anything. Do not rewrite the application or modify unrelated functionality.
Acceptance criteria
After running python app.py on a maximized window with tests/fixtures/vulnerable selected, the findings table should show substantially more than three rows—ideally 8–12 where screen dimensions permit—without requiring the user to drag its scrollbar. The details panel must remain usable, and all findings must still be reachable by scrolling.
{
"skill": "devrelay-community-wisdom"
}
Skill "devrelay-community-wisdom" loaded successfully. Follow the instructions in the skill context.
{
"skill": "python-fact-grounded-coding"
}
Skill "python-fact-grounded-coding" loaded successfully. Follow the instructions in the skill context.
{
"query": "DevRelay search dev.to semantic article content comments and get active DEV challenges plus list user's MLH events"
}
[
"devrelay-gateway-get_challenges",
"devrelay-gateway-search_dev_to_semantic",
"devrelay-gateway-get_mlh_event",
"devrelay-gateway-get_followers",
"devrelay-gateway-get_events"
]
{
"query": "Tkinter ttk Panedwindow vertical layout Treeview height resize and panes scrollable detail panel",
"per_page": 10
}
{
"Ok": [
{
"id": 2338250,
"title": "Tkinter GUI guide",
"description": "1.Create the window from tkinter import * b = Tk() b.title(\"tkinter...",
"tags": [
"programming",
"beginners",
"python",
"discuss"
],
"path": "/hunter-intel/tkinter-gui-guide-366e",
"url": "https://dev.to/hunter-intel/tkinter-gui-guide-366e",
"score": 1.0
},
{
"id": 3966846,
"title": "Under the Hood: Drag, Touch, and CSS Cascade in a Real Diff UI",
"description": "TLDR: Three interconnected UI problems reveal how layout-aware drag detection, unified touch/mouse...",
"tags": [
"python",
"datascience",
"pdf",
"automation"
],
"path": "/bonzai2carn/under-the-hood-drag-touch-and-css-cascade-in-a-real-diff-ui-1b66",
"url": "https://dev.to/bonzai2carn/under-the-hood-drag-touch-and-css-cascade-in-a-real-diff-ui-1b66",
"score": 1.0
},
{
"id": 4778617,
"title": "What If a Flutter Split View Didn’t Need Material or Cupertino?",
"description": "I recently published agnostic_split_view, an open-source Flutter package built around a simple...",
"tags": [
"flutter",
"opensource",
"programming",
"dart"
],
"path": "/muhammad_abdal/what-if-a-flutter-split-view-didnt-need-material-or-cupertino-27o6",
"url": "https://dev.to/muhammad_abdal/what-if-a-flutter-split-view-didnt-need-material-or-cupertino-27o6",
"score": 1.0
},
{
"id": 4853269,
"title": "Building a reading ruler that floats over every Android app: touch pass-through, Android 12's opacity cap, and masks",
"description": "I make ReadFocus+, a small Android app that floats a translucent strip (a \"reading ruler\") over the...",
"tags": [
"showdev",
"android",
"java",
"a11y"
],
"path": "/codedac/building-a-reading-ruler-that-floats-over-every-android-app-touch-pass-through-android-12s-5eck",
"url": "https://dev.to/codedac/building-a-reading-ruler-that-floats-over-every-android-app-touch-pass-through-android-12s-5eck",
"score": 1.0
},
{
"id": 2384563,
"title": "✅ GUI Based Universal Unit Converter Using Python With Source Code",
"description": "import tkinter as tk from tkinter import ttk # Conversion functions for each...",
"tags": [
"programming",
"beginners",
"tutorial",
"python"
],
"path": "/srijan-xi/gui-based-universal-unit-converter-using-python-with-source-code-4fi8",
"url": "https://dev.to/srijan-xi/gui-based-universal-unit-converter-using-python-with-source-code-4fi8",
"score": 1.0
},
{
"id": 4734190,
"title": "I built DirectSurface, a Canvas UI framework for dockable browser workspaces",
"description": "DirectSurface 0.1.0 is an MIT-licensed TypeScript package for building browser workspaces with...",
"tags": [
"showdev",
"typescript",
"javascript",
"webdev"
],
"path": "/hailinbigbaby/i-built-directsurface-a-canvas-ui-framework-for-dockable-browser-workspaces-54lg",
"url": "https://dev.to/hailinbigbaby/i-built-directsurface-a-canvas-ui-framework-for-dockable-browser-workspaces-54lg",
"score": 1.0
},
{
"id": 4695808,
"title": "Logging Every tmux Pane",
"description": "The tmux-logging plugin is a manual toggle. I wanted every pane logged from the moment it exists, in...",
"tags": [
"tmux",
"macos",
"cli",
"productivity"
],
"path": "/bendevcode/logging-every-tmux-pane-5cpf",
"url": "https://dev.to/bendevcode/logging-every-tmux-pane-5cpf",
"score": 1.0
},
{
"id": 4774150,
"title": "How I Built an AI Studio That Turns Manhwa Chapters into Narrated Recap Videos",
"description": "If you've ever fallen down the rabbit hole of manhwa recap channels on YouTube — \"The Weakest Hunter...",
"tags": [
"ai",
"automation",
"machinelearning",
"opensource"
],
"path": "/aashish124/how-i-built-an-ai-studio-that-turns-manhwa-chapters-into-narrated-recap-videos-5enl",
"url": "https://dev.to/aashish124/how-i-built-an-ai-studio-that-turns-manhwa-chapters-into-narrated-recap-videos-5enl",
"score": 1.0
},
{
"id": 4795580,
"title": "GIT Tree VS Code Extension",
"description": "I got tired of switching between VS Code and a separate Git app, so I built one into the...",
"tags": [
"git",
"opensource",
"showdev",
"vscode"
],
"path": "/javiandev/git-tree-vs-code-extension-1fd8",
"url": "https://dev.to/javiandev/git-tree-vs-code-extension-1fd8",
"score": 1.0
},
{
"id": 4728981,
"title": "Stop Letting AI Code Your GUI Before It Draws It",
"description": "You want to make an interesting screen in your app. Diligently, you describe it to the AI. The AI...",
"tags": [
"ai",
"frontend",
"webdev",
"programming"
],
"path": "/james_koppel_3aa6e45753b4/plan-better-guis-1ec",
"url": "https://dev.to/james_koppel_3aa6e45753b4/plan-better-guis-1ec",
"score": 1.0
}
]
}
{
"query": "Tkinter GUI layout pitfalls vertical scrollbar Treeview fixed height Panedwindow proportion resize",
"per_page": 10
}
{
"Ok": [
{
"id": 3589392,
"title": "🐍 tkinter frame and label widget guide: common mistakes and how to fix them",
"description": "💥 The First Time I Tried Making a Login Form… Man. I remember my first Tkinter login...",
"tags": [
"python",
"tkinter",
"gui"
],
"path": "/ptp2308/tkinter-frame-and-label-widget-guide-common-mistakes-and-how-to-fix-them-45ec",
"url": "https://dev.to/ptp2308/tkinter-frame-and-label-widget-guide-common-mistakes-and-how-to-fix-them-45ec",
"score": 1.0
},
{
"id": 2338250,
"title": "Tkinter GUI guide",
"description": "1.Create the window from tkinter import * b = Tk() b.title(\"tkinter...",
"tags": [
"programming",
"beginners",
"python",
"discuss"
],
"path": "/hunter-intel/tkinter-gui-guide-366e",
"url": "https://dev.to/hunter-intel/tkinter-gui-guide-366e",
"score": 1.0
},
{
"id": 2383279,
"title": "Build a Data Analyzer App with Tkinter, Pandas & NumPy (Python GUI Project)",
"description": "Introduction Want to mix GUI and data power? Here's how to create a desktop app in Python...",
"tags": [
"python",
"tkinter",
"pandas",
"numpy"
],
"path": "/hunter-intel/build-a-data-analyzer-app-with-tkinter-pandas-numpy-python-gui-project-17mn",
"url": "https://dev.to/hunter-intel/build-a-data-analyzer-app-with-tkinter-pandas-numpy-python-gui-project-17mn",
"score": 1.0
},
{
"id": 4598014,
"title": "MILP-Based Reactive GUI Framework Solves Dynamic Widget Layout with Complex Constraints",
"description": "Introduction Reactive GUI frameworks have long promised dynamic, responsive layouts that...",
"tags": [
"gui",
"milp",
"optimization",
"constraints"
],
"path": "/serbyte/milp-based-reactive-gui-framework-solves-dynamic-widget-layout-with-complex-constraints-4l81",
"url": "https://dev.to/serbyte/milp-based-reactive-gui-framework-solves-dynamic-widget-layout-with-complex-constraints-4l81",
"score": 1.0
},
{
"id": 4728981,
"title": "Stop Letting AI Code Your GUI Before It Draws It",
"description": "You want to make an interesting screen in your app. Diligently, you describe it to the AI. The AI...",
"tags": [
"ai",
"frontend",
"webdev",
"programming"
],
"path": "/james_koppel_3aa6e45753b4/plan-better-guis-1ec",
"url": "https://dev.to/james_koppel_3aa6e45753b4/plan-better-guis-1ec",
"score": 1.0
},
{
"id": 3214902,
"title": "I Built a Native RTL Python GUI with Tkinter (and it actually looks good)",
"description": "Let's be honest: Python Tkinter has a bad reputation. Most people think of Windows 95-style gray...",
"tags": [
"python",
"opensource",
"gui",
"showdev"
],
"path": "/freerave/i-built-a-native-rtl-python-gui-with-tkinter-and-it-actually-looks-good-4og5",
"url": "https://dev.to/freerave/i-built-a-native-rtl-python-gui-with-tkinter-and-it-actually-looks-good-4og5",
"score": 1.0
},
{
"id": 2384563,
"title": "✅ GUI Based Universal Unit Converter Using Python With Source Code",
"description": "import tkinter as tk from tkinter import ttk # Conversion functions for each...",
"tags": [
"programming",
"beginners",
"tutorial",
"python"
],
"path": "/srijan-xi/gui-based-universal-unit-converter-using-python-with-source-code-4fi8",
"url": "https://dev.to/srijan-xi/gui-based-universal-unit-converter-using-python-with-source-code-4fi8",
"score": 1.0
},
{
"id": 4751339,
"title": "Built a PowerShell-Native GUI Framework — PoshUI",
"description": "Been working on this for a while and finally have a public release worth sharing: PoshUI, a GUI...",
"tags": [
"dotnet",
"opensource",
"programming",
"showdev"
],
"path": "/kenneth_anders_b405932385/built-a-powershell-native-gui-framework-poshui-2f0f",
"url": "https://dev.to/kenneth_anders_b405932385/built-a-powershell-native-gui-framework-poshui-2f0f",
"score": 1.0
},
{
"id": 4854295,
"title": "Why My Fonts Caused Layout Shift: Field Notes on next/font, size-adjust, and Arabic Fallback Metrics",
"description": "Headline: Font-driven layout shift is a metrics mismatch, not a download-speed problem — the...",
"tags": [
"nextjs",
"performance",
"webdev",
"css"
],
"path": "/ahmed_mahmoud360/why-my-fonts-caused-layout-shift-field-notes-on-nextfont-size-adjust-and-arabic-fallback-metrics-2288",
"url": "https://dev.to/ahmed_mahmoud360/why-my-fonts-caused-layout-shift-field-notes-on-nextfont-size-adjust-and-arabic-fallback-metrics-2288",
"score": 1.0
},
{
"id": 4625057,
"title": "Why a WPF ScrollViewer Does Not Scroll and How to Fix It",
"description": "Originally published at s-iguchi09.github.io. A Japanese version is also available. ...",
"tags": [
"wpf",
"dotnet",
"csharp"
],
"path": "/s-iguchi09/why-a-wpf-scrollviewer-does-not-scroll-and-how-to-fix-it-2p9g",
"url": "https://dev.to/s-iguchi09/why-a-wpf-scrollviewer-does-not-scroll-and-how-to-fix-it-2p9g",
"score": 1.0
}
]
}
{
"per_page": 10
}
{"Ok":[{"id":82,"title":"Hacktoberfest Open-Source AI Challenge: Week 4","slug":"hacktoberfest-week4-2026-10-26","description":"Register now to be notified as details drop.","details":null,"full_details":"","type_of":"challenge","starts_at":"2026-10-26T16:00:00.000Z","ends_at":"2026-10-31T06:59:00.000Z","published":true,"cover_image":"https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/events/cover_image/82/2dc6f675-b1e1-4f3f-b542-79b3677c2651.png","location_url":null},{"id":81,"title":"Hacktoberfest Open-Source AI Challenge: Week 3","slug":"hacktoberfest-week3-2026-10-19","description":"Register now to be notified as details drop.","details":null,"full_details":"","type_of":"challenge","starts_at":"2026-10-19T16:00:00.000Z","ends_at":"2026-10-25T06:59:00.000Z","published":true,"cover_image":"https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/events/cover_image/81/3599abf1-d96c-4e0c-bb18-89b345c3b5d0.png","location_url":null},{"id":80,"title":"Hacktoberfest Open-Source AI Challenge: Week 2","slug":"hacktoberfest-week2-2026-10-12","description":"The Hacktoberfest Open-Source AI Challenge: Week 2 runs October 12 to October 18. This week's theme is Side Hustle: build something with open-weight models or open-source AI that someone would actually pay for. $2,550 in prizes across 18 winners.\r\n\r\nThis year's Hacktoberfest is all about building brand-new projects with open-source AI and open-weight models. Every week brings a new theme, and every challenge is a fresh start, so you're competing on the same footing as everyone else.","details":null,"full_details":"The Hacktoberfest Open-Source AI Challenge: Week 2 runs October 12 to October 18. Build something with open-weight models or open-source AI that someone would actually pay for. $2,550 in prizes across 18 winners.\r\n\r\nThis year's Hacktoberfest is all about building brand-new projects with open-source AI and open-weight models. Week 2 is the second of five Hacktoberfest DEV Challenges. See all five challenges and every partner category on the HF26 DEV Challenge Hub: https://dev.to/challenges/hf26\r\n\r\nThis Week's Theme: Side Hustle\r\n\r\nBuild something with open-weight models or open-source AI that someone would actually pay for.\r\n\r\nThat can mean running an open-weight model, building on an open-source agent harness or framework, running inference locally, or all three. Whatever you pick, the open pieces should be what makes your project work.\r\n\r\nThink small, specific, and useful. The best builds here solve a real problem for a real person, whether that's a freelancer, a small business owner, a niche hobby community, or your friend who keeps complaining about the same tedious task. A few ideas to get you going:\r\n\r\n An invoice and receipt sorter for freelancers that runs locally, so client data never leaves their laptop\r\n A listing writer for Etsy or eBay sellers that turns a few product photos into a polished description\r\n A menu translator for a local restaurant that handles their specific dishes better than a generic tool\r\n\r\nIn your post, tell us why open innovation matters for what you built. Does running your own model keep costs low enough that the business actually works? Keep your customers' data private? Let you fine-tune for a niche that a general-purpose API handles poorly? Avoid getting locked into someone else's pricing?\r\n\r\nBonus points if you land your first user. Get it in front of someone, let them use it, and tell us how it went. Did they pay? Would they? What did they ask for next?\r\n\r\nPrize Categories\r\n\r\nAlongside the overall winner, there are 17 prize categories for projects that use a specific partner's technology. You don't need to use any of these to win the overall prize.\r\n\r\nFeatured categories: Best Use of Render, Best Use of TabPFN, Best Use of Tinker, Best Use of Arduino, Best Use of DigitalOcean, Best Use of Gemma\r\n\r\nPartner categories: Best Use of Backboard, Best Use of ElevenLabs, Best Use of Entire, Best Use of GitHub Copilot, Best Use of Mastra, Best Use of MongoDB Atlas, Best Use of Sentry Agent Tracing, Best Use of SerpApi, Best Use of Temporal, Best Use of Tiger Data, Best Use of a .tech Domain\r\n\r\nOne project can enter every category it genuinely uses, but each participant can only win once per challenge. Several partners are giving participants free credits and promo codes, including Tinker, Render, Backboard, and ElevenLabs. Claim yours at hacktoberfest.com/my.\r\n\r\nJudging Criteria\r\n\r\n Writing Quality (weighted most heavily): Is the post clear and engaging? Does it explain what you built, who it's for, and why open innovation matters?\r\n Relevance to the Prompt and Theme: Is open-source AI or open-weight models at the core of the project? Is it something someone would actually pay for?\r\n Creativity: Is it an original idea, or a fresh take on a familiar problem?\r\n Technical Execution: Does it work, and is it well built?\r\n
[Truncated — full output (10.1 KB) temporarily saved to C:\Users\Kushal\AppData\Local\Temp\1792147423880-copilot-tool-output-afdad573d1674d5991e4ac738336a9b0.txt]
{
"type_of": "challenge",
"per_page": 10
}
{"Ok":[{"id":82,"title":"Hacktoberfest Open-Source AI Challenge: Week 4","slug":"hacktoberfest-week4-2026-10-26","description":"Register now to be notified as details drop.","details":null,"full_details":"","type_of":"challenge","starts_at":"2026-10-26T16:00:00.000Z","ends_at":"2026-10-31T06:59:00.000Z","published":true,"cover_image":"https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/events/cover_image/82/2dc6f675-b1e1-4f3f-b542-79b3677c2651.png","location_url":null},{"id":81,"title":"Hacktoberfest Open-Source AI Challenge: Week 3","slug":"hacktoberfest-week3-2026-10-19","description":"Register now to be notified as details drop.","details":null,"full_details":"","type_of":"challenge","starts_at":"2026-10-19T16:00:00.000Z","ends_at":"2026-10-25T06:59:00.000Z","published":true,"cover_image":"https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/events/cover_image/81/3599abf1-d96c-4e0c-bb18-89b345c3b5d0.png","location_url":null},{"id":80,"title":"Hacktoberfest Open-Source AI Challenge: Week 2","slug":"hacktoberfest-week2-2026-10-12","description":"The Hacktoberfest Open-Source AI Challenge: Week 2 runs October 12 to October 18. This week's theme is Side Hustle: build something with open-weight models or open-source AI that someone would actually pay for. $2,550 in prizes across 18 winners.\r\n\r\nThis year's Hacktoberfest is all about building brand-new projects with open-source AI and open-weight models. Every week brings a new theme, and every challenge is a fresh start, so you're competing on the same footing as everyone else.","details":null,"full_details":"The Hacktoberfest Open-Source AI Challenge: Week 2 runs October 12 to October 18. Build something with open-weight models or open-source AI that someone would actually pay for. $2,550 in prizes across 18 winners.\r\n\r\nThis year's Hacktoberfest is all about building brand-new projects with open-source AI and open-weight models. Week 2 is the second of five Hacktoberfest DEV Challenges. See all five challenges and every partner category on the HF26 DEV Challenge Hub: https://dev.to/challenges/hf26\r\n\r\nThis Week's Theme: Side Hustle\r\n\r\nBuild something with open-weight models or open-source AI that someone would actually pay for.\r\n\r\nThat can mean running an open-weight model, building on an open-source agent harness or framework, running inference locally, or all three. Whatever you pick, the open pieces should be what makes your project work.\r\n\r\nThink small, specific, and useful. The best builds here solve a real problem for a real person, whether that's a freelancer, a small business owner, a niche hobby community, or your friend who keeps complaining about the same tedious task. A few ideas to get you going:\r\n\r\n An invoice and receipt sorter for freelancers that runs locally, so client data never leaves their laptop\r\n A listing writer for Etsy or eBay sellers that turns a few product photos into a polished description\r\n A menu translator for a local restaurant that handles their specific dishes better than a generic tool\r\n\r\nIn your post, tell us why open innovation matters for what you built. Does running your own model keep costs low enough that the business actually works? Keep your customers' data private? Let you fine-tune for a niche that a general-purpose API handles poorly? Avoid getting locked into someone else's pricing?\r\n\r\nBonus points if you land your first user. Get it in front of someone, let them use it, and tell us how it went. Did they pay? Would they? What did they ask for next?\r\n\r\nPrize Categories\r\n\r\nAlongside the overall winner, there are 17 prize categories for projects that use a specific partner's technology. You don't need to use any of these to win the overall prize.\r\n\r\nFeatured categories: Best Use of Render, Best Use of TabPFN, Best Use of Tinker, Best Use of Arduino, Best Use of DigitalOcean, Best Use of Gemma\r\n\r\nPartner categories: Best Use of Backboard, Best Use of ElevenLabs, Best Use of Entire, Best Use of GitHub Copilot, Best Use of Mastra, Best Use of MongoDB Atlas, Best Use of Sentry Agent Tracing, Best Use of SerpApi, Best Use of Temporal, Best Use of Tiger Data, Best Use of a .tech Domain\r\n\r\nOne project can enter every category it genuinely uses, but each participant can only win once per challenge. Several partners are giving participants free credits and promo codes, including Tinker, Render, Backboard, and ElevenLabs. Claim yours at hacktoberfest.com/my.\r\n\r\nJudging Criteria\r\n\r\n Writing Quality (weighted most heavily): Is the post clear and engaging? Does it explain what you built, who it's for, and why open innovation matters?\r\n Relevance to the Prompt and Theme: Is open-source AI or open-weight models at the core of the project? Is it something someone would actually pay for?\r\n Creativity: Is it an original idea, or a fresh take on a familiar problem?\r\n Technical Execution: Does it work, and is it well built?\r\n
[Truncated — full output (10.1 KB) temporarily saved to C:\Users\Kushal\AppData\Local\Temp\1792147423880-copilot-tool-output-869c812985734ca5b043d4830ea4afd6.txt]
{
"query": "DevRelay get_article_content, get_comments for article ids, list_my_mlh_events for user's registered MLH events"
}
[
"devrelay-gateway-get_my_articles",
"devrelay-gateway-get_article_content",
"devrelay-gateway-get_comments",
"devrelay-gateway-create_article",
"devrelay-gateway-get_mlh_event"
]
{
"query": "DevRelay list registered MLH events current user"
}
[
"devrelay-gateway-list_my_mlh_events",
"devrelay-gateway-register_for_mlh_event",
"devrelay-gateway-get_reading_list",
"devrelay-gateway-get_authenticated_user",
"devrelay-gateway-search_mlh_events"
]
{
"path": "c:\\Projects\\securelocal-ai\\ui\\main_window.py",
"view_range": [
1,
390
]
}
diff --git a/c:/Projects/securelocal-ai/ui/main_window.py b/c:/Projects/securelocal-ai/ui/main_window.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/main_window.py
+++ b/c:/Projects/securelocal-ai/ui/main_window.py
@@ -1,390 +1,390 @@
from __future__ import annotations
import queue
import threading
import tkinter as tk
from pathlib import Path
from tkinter import filedialog, messagebox, ttk
from ai.ollama_client import DEFAULT_MODEL, OllamaClient, OllamaError
from reports.exporter import export_json, export_markdown
from scanner.engine import scan_project
from scanner.models import Finding, ScanResult, Severity
from ui.finding_details import FindingDetails
class SecureLocalApp:
def __init__(self, root: tk.Tk) -> None:
self.root = root
self.root.title("SecureLocal AI — Code Security Auditor")
self.root.geometry("1120x780")
self.root.minsize(860, 600)
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
self.root.configure(bg="#F3F6FB")
self.events: queue.Queue[tuple[str, object]] = queue.Queue()
self.result: ScanResult | None = None
self.findings_by_id: dict[str, Finding] = {}
self.summary_cards: dict[str, tuple[ttk.Label, tk.StringVar]] = {}
self._configure_style()
self._build_ui()
self.root.after(100, self._process_events)
def _configure_style(self) -> None:
style = ttk.Style(self.root)
style.theme_use("clam")
style.configure(".", font=("Segoe UI", 10), background="#F3F6FB", foreground="#0F172A")
style.configure("Header.TFrame", background="#112A46")
style.configure("Header.TLabel", font=("Segoe UI", 20, "bold"), background="#112A46", foreground="#FFFFFF")
style.configure("Subtitle.TLabel", font=("Segoe UI", 10), background="#112A46", foreground="#D7E2F3")
style.configure("Section.TLabel", font=("Segoe UI", 10, "bold"), background="#F3F6FB", foreground="#1E293B")
style.configure("Section.TLabelframe", background="#F3F6FB", bordercolor="#D4DDEB", lightcolor="#D4DDEB", darkcolor="#D4DDEB")
style.configure("Section.TLabelframe.Label", font=("Segoe UI", 10, "bold"), background="#F3F6FB", foreground="#1E293B")
style.configure("Card.TFrame", background="#FFFFFF", relief="solid", borderwidth=1)
style.configure("SummaryLabel.TLabel", font=("Segoe UI", 9, "bold"), background="#FFFFFF", foreground="#475467")
style.configure("SummaryValue.TLabel", font=("Segoe UI", 18, "bold"), background="#FFFFFF", foreground="#0F172A")
style.configure("Toolbar.TFrame", background="#EEF3FA")
style.configure("Toolbar.TLabel", background="#EEF3FA", foreground="#475467", font=("Segoe UI", 9, "bold"))
style.configure("Status.TLabel", background="#F3F6FB", foreground="#475467", wraplength=1100)
style.configure("Primary.TButton", font=("Segoe UI", 10, "bold"), background="#2F6FED", foreground="#FFFFFF")
style.map("Primary.TButton", background=[("active", "#255ad6"), ("pressed", "#1f4bb7")], foreground=[("disabled", "#E2E8F0")])
style.configure("Secondary.TButton", background="#FFFFFF", foreground="#0F172A")
style.map("Secondary.TButton", background=[("active", "#EEF3FA"), ("pressed", "#E2E8F0")])
style.configure("Treeview", rowheight=26, background="#FFFFFF", fieldbackground="#FFFFFF", borderwidth=0)
style.map("Treeview", background=[("selected", "#D9E7FF")], foreground=[("selected", "#0F172A")])
style.configure("Treeview.Heading", font=("Segoe UI", 10, "bold"), background="#E2E8F0", foreground="#0F172A")
style.map("Treeview.Heading", background=[("active", "#D9E7FF")])
style.configure("Panel.TFrame", background="#FFFFFF")
def _build_ui(self) -> None:
container = ttk.Frame(self.root, padding=16, style="Panel.TFrame")
container.grid(row=0, column=0, sticky="nsew")
container.columnconfigure(0, weight=1)
container.rowconfigure(5, weight=1)
header = ttk.Frame(container, style="Header.TFrame", padding=(18, 16))
header.grid(row=0, column=0, sticky="ew")
header.columnconfigure(0, weight=1)
ttk.Label(header, text="SecureLocal AI", style="Header.TLabel").grid(row=0, column=0, sticky="w")
ttk.Label(
header,
text="Offline-first Python code security checks for local review and remediation.",
style="Subtitle.TLabel",
).grid(row=1, column=0, sticky="w", pady=(4, 0))
folder_frame = ttk.LabelFrame(container, text="Project selection", padding=12)
folder_frame.grid(row=1, column=0, sticky="ew", pady=(16, 10))
folder_frame.columnconfigure(0, weight=1)
self.project_path = tk.StringVar()
ttk.Entry(folder_frame, textvariable=self.project_path, width=60).grid(row=0, column=0, sticky="ew", padx=(0, 10))
ttk.Button(
folder_frame,
text="Choose folder…",
command=self._choose_folder,
style="Secondary.TButton",
).grid(row=0, column=1, padx=(0, 10))
self.scan_button = ttk.Button(
folder_frame,
text="Scan project",
command=self._start_scan,
style="Primary.TButton",
)
self.scan_button.grid(row=0, column=2)
self.summary_cards_frame = ttk.Frame(container, padding=(0, 0, 0, 8))
self.summary_cards_frame.grid(row=2, column=0, sticky="ew")
self.summary_cards_frame.columnconfigure(0, weight=1)
self.summary_cards_frame.columnconfigure(1, weight=1)
self.summary_cards_frame.columnconfigure(2, weight=1)
self.summary_cards_frame.columnconfigure(3, weight=1)
self.summary_cards_frame.columnconfigure(4, weight=1)
self._build_summary_cards()
toolbar = ttk.Frame(container, style="Toolbar.TFrame", padding=(12, 10))
toolbar.grid(row=3, column=0, sticky="ew", pady=(0, 10))
toolbar.columnconfigure(4, weight=1)
ttk.Label(toolbar, text="Severity", style="Toolbar.TLabel").grid(row=0, column=0, sticky="w")
self.severity_filter = tk.StringVar(value="All")
self.severity_combo = ttk.Combobox(
toolbar,
textvariable=self.severity_filter,
state="readonly",
values=("All", *(severity.value for severity in Severity)),
width=12,
)
self.severity_combo.grid(row=0, column=1, sticky="w", padx=(6, 18))
self.severity_combo.bind("<<ComboboxSelected>>", self._apply_filters)
ttk.Label(toolbar, text="Search", style="Toolbar.TLabel").grid(row=0, column=2, sticky="w")
self.search_text = tk.StringVar()
search = ttk.Entry(toolbar, textvariable=self.search_text, width=30)
search.grid(row=0, column=3, sticky="ew", padx=(6, 12))
self.search_text.trace_add("write", self._apply_filters)
self.summary = ttk.Label(toolbar, text="Choose a project folder to begin.", style="Toolbar.TLabel")
self.summary.grid(row=0, column=4, sticky="e")
actions = ttk.Frame(container, padding=(0, 0, 0, 8))
actions.grid(row=4, column=0, sticky="ew")
self.ai_model = tk.StringVar(value=DEFAULT_MODEL)
ttk.Label(actions, text="Local model", style="Section.TLabel").pack(side="left")
ttk.Entry(actions, textvariable=self.ai_model, width=22).pack(side="left", padx=(6, 8))
self.explain_button = ttk.Button(
actions,
text="Explain selected with local AI",
command=self._explain_selected,
state="disabled",
style="Primary.TButton",
)
self.explain_button.pack(side="left", padx=(0, 12))
self.export_json_button = ttk.Button(
actions,
text="Export JSON…",
command=self._export_json,
state="disabled",
style="Secondary.TButton",
)
self.export_json_button.pack(side="right", padx=(8, 0))
self.export_md_button = ttk.Button(
actions,
text="Export Markdown…",
command=self._export_markdown,
state="disabled",
style="Secondary.TButton
{
"path": "c:\\Projects\\securelocal-ai\\ui\\finding_details.py",
"view_range": [
1,
180
]
}
diff --git a/c:/Projects/securelocal-ai/ui/finding_details.py b/c:/Projects/securelocal-ai/ui/finding_details.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/finding_details.py
+++ b/c:/Projects/securelocal-ai/ui/finding_details.py
@@ -1,180 +1,180 @@
from __future__ import annotations
import tkinter as tk
from tkinter import ttk
from scanner.models import Finding
class FindingDetails(ttk.Frame):
def __init__(self, master: tk.Misc) -> None:
super().__init__(master, padding=(10, 8), style="Panel.TFrame")
self.columnconfigure(0, weight=1)
self.rowconfigure(1, weight=1)
ttk.Label(self, text="Finding details", style="Section.TLabel").grid(row=0, column=0, sticky="w")
self.content = tk.Text(
self,
wrap="word",
state="disabled",
bg="#FFFFFF",
relief="flat",
borderwidth=0,
highlightthickness=0,
padx=8,
pady=8,
spacing1=4,
spacing2=2,
spacing3=2,
)
self.content.grid(row=1, column=0, sticky="nsew", pady=(8, 0))
scrollbar = ttk.Scrollbar(self, orient="vertical", command=self.content.yview)
scrollbar.grid(row=1, column=1, sticky="ns", pady=(8, 0))
self.content.configure(yscrollcommand=scrollbar.set)
self.content.tag_configure("title", font=("Segoe UI", 12, "bold"), foreground="#0F172A")
self.content.tag_configure("meta", font=("Segoe UI", 10, "bold"), foreground="#475467")
self.content.tag_configure("body", font=("Segoe UI", 10), foreground="#1E293B")
self.content.tag_configure("section", font=("Segoe UI", 10, "bold"), foreground="#1E293B")
self.content.tag_configure("muted", font=("Segoe UI", 9), foreground="#475467")
def show_finding(self, finding: Finding | None) -> None:
self.content.configure(state="normal")
self.content.delete("1.0", "end")
if finding is None:
self.content.insert("end", "Select a finding to view the explanation, evidence, limitations, and remediation.", "body")
self.content.configure(state="disabled")
return
self.content.insert("end", f"{finding.title}\n", "title")
self.content.insert(
"end",
f"Severity: {finding.severity.value} | Rule: {finding.rule_id} | Confidence: {finding.confidence}\n",
"meta",
)
self.content.insert(
"end",
f"Location: {finding.file_path}:{finding.line}:{finding.column}\n\n",
"body",
)
sections = [
("Why it matters", finding.description),
("Redacted evidence", finding.evidence),
("Limitations", finding.limitations),
("Remediation", finding.remediation),
]
for label, value in sections:
self.content.insert("end", f"{label}\n", "section")
self.content.insert("end", f"{value}\n\n", "body")
self.content.configure(state="disabled")
{
"id": 3589392
}
{"Ok":"## 💥 The First Time I Tried Making a Login Form…\n\n\n\nMan. I remember my first Tkinter login form. \nSo proud of myself. Two entry boxes. A button. Done. How hard could it be?\n\n**📑 Table of Contents**\n\n - 💥 The First Time I Tried Making a Login Form…\n - 💻 Layout First, Design Later — Why *Structure* Matters\n - 📦 Frame as a Design Strategy, Not Just a Widget\n - 🏷️ Label: More Than Just Text\n - ⚙️ Building the Responsive Skeleton\n - 🎨 Styling with Purpose — Labels That Guide, Not Just Decorate\n - 🧠 Common Pitfalls (And How I Learned Them the Hard Way)\n - 🟩 Final Thoughts\n - ❓ Frequently Asked Questions\n - Can I use place() instead of grid() for responsiveness?\n - How do I make the password field hide input?\n - Why use Frame when I can just add widgets to the root window?\n\nRan the script. \nLabels missing. Inputs squished. Button? Vanished into the void.\n\nI spent two hours staring at a blank window wondering why the *frame* wasn’t showing anything. \nTurns out—no `grid()` or `pack()`, nothing renders. *At all.*\n\nYeah, I learned this the hard way. \nAnd honestly, it wasn’t about the code. It was about structure. About understanding how **tkinter frame and label widget guide** principles actually hold a UI together.\n\nYou’ve been there. \nThat moment when your GUI just… refuses to behave. \nLike it’s personally offended by your logic.\n\nHere's the thing: \nTkinter isn’t magic. \nIt’s plumbing. \nAnd if your pipes are leaky, the whole house floods.\n\n* * *\n\n## 💻 Layout First, Design Later — Why *Structure* Matters\n\n### 📦 Frame as a Design Strategy, Not Just a Widget\n\nA junior I was mentoring once spent an entire morning trying to center a label.\n\n### 🏷️ Label: More Than Just Text\n\nLabels do *so* much more than display static text.\n\n## ⚙️ Building the Responsive Skeleton\n\n“Responsive” in Tkinter doesn’t mean media queries. \nIt means: when I drag the window wider, nothing breaks. *(More on[PythonTPoint tutorials](<https://pythontpoint.in>))*\n\nAnd that’s where `grid()` shines — but only if you help it.\n\nYou need:\n\n - `grid()` for placement\n - `columnconfigure()` and `rowconfigure()` to make things stretch\n - `sticky=\"ew\"` to make widgets expand East-West\n\n> \"In GUI design, the container is as important as the content.\"\n\nHere’s a minimal working structure:\n \n \n import tkinter as tk\n \n root = tk.Tk()\n root.title(\"Login Form\")\n root.geometry(\"400x300\")\n root.columnconfigure(0, weight=1) # Make column 0 expandable\n \n # Title Frame\n title_frame = tk.Frame(root, pady=20)\n title_frame.grid(row=0, column=0, sticky=\"ew\")\n title_frame.columnconfigure(0, weight=1)\n \n title_label = tk.Label(title_frame, text=\"Welcome Back\", font=(\"Helvetica\", 16))\n title_label.grid(row=0, column=0)\n \n # Input Frame\n input_frame = tk.Frame(root, pady=10)\n input_frame.grid(row=1, column=0, sticky=\"ew\", padx=20)\n input_frame.columnconfigure(1, weight=1)\n \n tk.Label(input_frame, text=\"Username:\").grid(row=0, column=0, sticky=\"w\", pady=5)\n username_entry = tk.Entry(input_frame)\n username_entry.grid(row=0, column=1, sticky=\"ew\", pady=5)\n \n tk.Label(input_frame, text=\"Password:\").grid(row=1, column=0, sticky=\"w\", pady=5)\n password_entry = tk.Entry(input_frame, show=\"*\")\n password_entry.grid(row=1, column=1, sticky=\"ew\", pady=5)\n \n # Button Frame\n button_frame = tk.Frame(root, pady=20)\n button_frame.grid(row=2, column=0, sticky=\"ew\")\n button_frame.columnconfigure(0, weight=1)\n \n login_btn = tk.Button(button_frame, text=\"Login\", bg=\"blue\", fg=\"white\")\n login_btn.grid(row=0, column=0, padx=10)\n \n root.mainloop()\n \n\nNotice how each frame calls `columnconfigure()`? \nThat’s not decoration.\n\nIt’s telling Tkinter: “When the window grows, stretch the input box — not just the padding.”\n\nThis is the core of any decent **tkinter frame and label widget guide** : \nUse frames to group. \nLabels to guide. \nGrid weights to breathe.\n\n* * *\n\n## 🎨 Styling with Purpose — Labels That Guide, Not Just Decorate\n\nYou can have perfect layout — but if your labels are gray, tiny, and unreadable? Game over.\n\nStyling isn’t optional. \nIt’s part of the UX.\n\nUse labels to:\n\n - Set visual hierarchy (bigger font for title)\n - Provide feedback (“Login failed” in red)\n - Improve accessibility (high contrast, readable fonts)\n\nHere’s how to style a label properly:\n \n \n # Styled label\n title_label = tk.Label(\n title_frame,\n text=\"Secure Login\",\n font=(\"Arial\", 18, \"bold\"),\n fg=\"#2c3e50\",\n bg=\"#ecf0f1\",\n pady=10\n )\n title_label.grid(row=0, column=0,
[Truncated — full output (8.3 KB) temporarily saved to C:\Users\Kushal\AppData\Local\Temp\1792147625368-copilot-tool-output-d35e98427ba647edb528a201914bfa7c.txt]
{
"id": 2338250
}
{
"Ok": "## 1.Create the window\n\n```\nfrom tkinter import *\n\nb = Tk()\nb.title(\"tkinter project\")\nb.geometry(\"400x300\")\nb.minsize(400,400)\n\nb.mainloop()\n```\n\n## 2.Label(Display text)\n\n```\nfrom tkinter import *\n\nb = Tk()\n\nl1 = Label(b, text=\"Blackmare01wolf\", font=(\"arial\",14), fg=\"black\", bg=\"blue\")\nl1.place(x=0, y=0)\n\nb.mainloop()\n```\n\n## 3.Button(Clickable Button)\n\n```\nfrom tkinter import *\n\nb = Tk()\n\nb1 = Button(b, text=\"Blackmare01wolf\", font=(\"arial\",14), fg=\"black\", bg=\"blue\", bd=5, relief=RIDGE)\nb1.place(x=0, y=0)\n\nb.mainloop()\n```\n\n## 4.Entry(Text input box)\n\n```\nfrom tkinter import *\n\nb = Tk()\n\ne1 = Entry(b, font=(\"arial\",14), fg=\"black\", bd=5, relief=RIDGE)\ne1.place(x=0, y=0)\n\nb.mainloop()\n```\n\n## 5.Text box(Multi-line input box)\n\n```\nfrom tkinter import *\n\nb = Tk()\n\nt1 = Text(b, font=(\"arial\",14), height=30, width=30)\nt1.place(x=0, y=0)\n\nb.mainloop()\n```\n\nThis guide covered the basics of tkinter GUI development, including creating window and adding more widget and customize their styles. If you have any questions about GUI development, leave a comment below.\n\nFew codes are in github. [Github](github.com/blackmare01)"
}
{
"article_id": 3589392
}
{
"Ok": [
]
}
{
"article_id": 2338250
}
{
"Ok": [
]
}
{
}
{"Ok":[{"id":"01a10b71-4f15-8e11-31da-74970abc6c0d","status":"registered","event":{"id":"019f70e5-2d9b-76f8-9a2c-fc9661592609","name":"Global Hack Week: Hacktoberfest","slug":"global-hack-week-open-source-90","status":"ended","event_format":"hackweek","starts_at":1791554400,"ends_at":1792170000,"starts_at_utc":"2026-10-09T14:00:00Z","ends_at_utc":"2026-10-16T17:00:00Z","time_zone":"America/New_York","private":false,"website_url":"https://events.mlh.io/events/14553-global-hack-week-hacktoberfest","submission_url":null,"self_check_in_mode":"disabled","check_in":{"state":"event_over","summary":"This event is over, so check-in is closed."}}},{"id":"01a120e0-da00-37e2-339d-a2e42598e19c","status":"registered","event":{"id":"01a0ced1-50e8-1335-a5c2-33b29d7d155a","name":"Hacktoberfest 2026","slug":"hacktoberfest-2026","status":"in_progress","event_format":"evergreen","starts_at":1790769600,"ends_at":1793530799,"starts_at_utc":"2026-09-30T12:00:00Z","ends_at_utc":"2026-11-01T10:59:59Z","time_zone":null,"private":false,"website_url":"https://hacktoberfest.com","submission_url":null,"self_check_in_mode":"disabled","check_in":{"state":"organizers_check_in","summary":"There is no self check-in; the organizers check attendees in at the event."}}},{"id":"01a120e8-58fe-9fa6-d187-20e9c7bb88e6","status":"registered","event":{"id":"01a0f81e-a834-54e5-deb0-ad01ba34dfdd","name":"Hacktoberfest Open-Source AI Challenge: Week 1","slug":"hacktoberfest-open-source-ai-challenge-week-1","status":"ended","event_format":"evergreen","starts_at":1791216000,"ends_at":1791788340,"starts_at_utc":"2026-10-05T16:00:00Z","ends_at_utc":"2026-10-12T06:59:00Z","time_zone":null,"private":false,"website_url":"https://dev.to/events/challenges/hacktoberfest-week1-2026-10-05","submission_url":null,"self_check_in_mode":"disabled","check_in":{"state":"event_over","summary":"This event is over, so check-in is closed."}}},{"id":"01a12a6f-61a6-cf32-b0b0-8dbed17aa91d","status":"checked_in","event":{"id":"01a0ecff-14dd-b5c8-8771-f88fdb0e6ddf","name":"[GHW Hacktoberfest] Building a Puzzle Web App with AI : Part 2","slug":"ghw-hacktoberfest-opening-ceremony-copy-04","status":"ended","event_format":"hackday","starts_at":1791712800,"ends_at":1791727200,"starts_at_utc":"2026-10-11T10:00:00Z","ends_at_utc":"2026-10-11T14:00:00Z","time_zone":"America/New_York","private":false,"website_url":"https://events.mlh.io/events/15325-ghw-hacktoberfest-building-a-puzzle-web-app-with-ai-part-2","submission_url":null,"self_check_in_mode":"code_required","check_in":{"state":"event_over","summary":"This event is over, so check-in is closed."}}},{"id":"01a1306b-40cf-5203-14c2-d576984513fd","status":"checked_in","event":{"id":"01a0ece8-0088-4b6d-21a3-f7bfcf228314","name":"[GHW Hacktoberfest] TigerData Workshop","slug":"ghw-hacktoberfest-sponsor-stream-copy-38","status":"ended","event_format":"hackday","starts_at":1791813600,"ends_at":1791817200,"starts_at_utc":"2026-10-12T14:00:00Z","ends_at_utc":"2026-10-12T15:00:00Z","time_zone":"America/New_York","private":false,"website_url":"https://events.mlh.io/events/15318-ghw-hacktoberfest-tigerdata-workshop","submission_url":null,"self_check_in_mode":"code_required","check_in":{"state":"event_over","summary":"This event is over, so check-in is closed."}}},{"id":"01a130a4-8cdf-49c4-ab19-16396c7c55cd","status":"checked_in","event":{"id":"01a0ece8-030d-6d25-0c23-10d17db2a7b3","name":"[GHW Hacktoberfest] Cursor and Grok Bot 101 & Demo","slug":"ghw-hacktoberfest-sponsor-stream-copy-5e","status":"ended","event_format":"hackday","starts_at":1791817200,"ends_at":1791820800,"starts_at_utc":"2026-10-12T15:00:00Z","ends_at_utc":"2026-10-12T16:00:00Z","time_zone":"America/New_York","private":false,"website_url":"https://events.mlh.io/events/15319-ghw-hacktoberfest-cursor-and-grok-bot-101-demo","submission_url":null,"self_check_in_mode":"code_required","check_in":{"state":"event_over","summary":"This event is over, so check-in is closed."}}},{"id":"01a130dc-d43e-1a3b-945c-df017ac5df3e","status":"checked_in","event":{"id":"01a0d881-b562-1334-d72f-86c9367f3700","name":"[GHW Hacktoberfest] Today in Global Hack Week Day 4","slug":"ghw-hacktoberfest-mini-event-typeracer-copy-2a","status":"ended","event_format":"hackday","starts_at":1791820800,"ends_at":1791824400,"starts_at_utc":"2026-10-12T16:00:00Z","ends_at_utc":"2026-10-12T17:00:00Z","time_zone":"America/New_York","private":false,"website_url":"https://events.mlh.io/events/15248-ghw-hacktoberfest-today-in-global-hack-week-day-4","submission_url":null,"self_check_in_mode":"code_required","check_in":{"state":"event_over","summary":"This event is over, so check-in is closed."}}},{"id":"01a13143-864e-8b44-713d-bf39c7e6e021","status":"registered","event":{"id":"01a0f6fe-8d1b-d2b1-cf63-35c13d304089","name":"Hacktoberfest Meetup Noida x Hackloop","slug":"hacktoberfest-meetup-noida-x-hackloop","status":"canceled","event_format":"hackday","starts_at":1792072800,"ends_at":1792272600,"starts_at_utc":"2026-10-15T1
[Truncated — full output (8.7 KB) temporarily saved to C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt]
{
"path": "c:\\Projects\\securelocal-ai\\ui\\main_window.py",
"view_range": [
150,
210
]
}
diff --git a/c:/Projects/securelocal-ai/ui/main_window.py b/c:/Projects/securelocal-ai/ui/main_window.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/main_window.py
+++ b/c:/Projects/securelocal-ai/ui/main_window.py
@@ -150,61 +150,61 @@
state="disabled",
style="Secondary.TButton",
)
self.export_md_button.pack(side="right")
content = ttk.Panedwindow(container, orient="vertical")
content.grid(row=5, column=0, sticky="nsew")
table_frame = ttk.Frame(content, style="Panel.TFrame")
table_frame.columnconfigure(0, weight=1)
table_frame.rowconfigure(0, weight=1)
columns = ("severity", "rule", "file", "line", "title")
self.table = ttk.Treeview(table_frame, columns=columns, show="headings", selectmode="browse")
self.table.tag_configure("oddrow", background="#F8FAFC")
self.table.tag_configure("Critical", foreground="#B42318")
self.table.tag_configure("High", foreground="#D97706")
self.table.tag_configure("Medium", foreground="#AC6B0C")
self.table.tag_configure("Low", foreground="#2F6FED")
self.table.tag_configure("Info", foreground="#475467")
for column, heading, width in (
("severity", "Severity", 100),
("rule", "Rule", 86),
("file", "File", 260),
("line", "Line", 62),
("title", "Finding", 360),
):
self.table.heading(column, text=heading)
self.table.column(column, width=width, anchor="w")
self.table.grid(row=0, column=0, sticky="nsew")
self.table.bind("<<TreeviewSelect>>", self._selection_changed)
vertical = ttk.Scrollbar(table_frame, orient="vertical", command=self.table.yview)
vertical.grid(row=0, column=1, sticky="ns")
self.table.configure(yscrollcommand=vertical.set)
horizontal = ttk.Scrollbar(table_frame, orient="horizontal", command=self.table.xview)
horizontal.grid(row=1, column=0, sticky="ew")
self.table.configure(xscrollcommand=horizontal.set)
self.details = FindingDetails(content)
content.add(table_frame, weight=3)
content.add(self.details, weight=2)
self.status = tk.StringVar(value="Scanning uses local deterministic rules. Source files are not changed or uploaded.")
ttk.Label(container, textvariable=self.status, style="Status.TLabel").grid(row=6, column=0, sticky="ew", pady=(8, 0))
def _build_summary_cards(self) -> None:
self.summary_cards = {}
labels = [
("Total findings", "total"),
("High", "high"),
("Medium", "medium"),
("Low", "low"),
("Info", "info"),
]
for index, (label_text, key) in enumerate(labels):
card = ttk.Frame(self.summary_cards_frame, padding=(12, 10), style="Card.TFrame")
card.grid(row=0, column=index, sticky="nsew", padx=(0 if index == 0 else 8, 0))
if key == "info":
card.grid_remove()
ttk.Label(card, text=label_text, style="SummaryLabel.TLabel").pack(anchor="w")
value = tk.StringVar(value="0")
ttk.Label(card, textvariable=value, style="SummaryValue.TLabel").pack(anchor="w", pady=(4, 0))
self.summary_cards[key] = (card, value)
self._update_summary_cards(None)
{
"path": "c:\\Projects\\securelocal-ai\\ui\\main_window.py",
"view_range": [
270,
390
]
}
diff --git a/c:/Projects/securelocal-ai/ui/main_window.py b/c:/Projects/securelocal-ai/ui/main_window.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/main_window.py
+++ b/c:/Projects/securelocal-ai/ui/main_window.py
@@ -270,121 +270,121 @@
self._show_explanation(payload)
elif event == "error":
self._handle_error(payload)
except queue.Empty:
pass
self.root.after(100, self._process_events)
def _show_result(self, result: ScanResult) -> None:
self.result = result
self.scan_button.configure(state="normal")
self.export_json_button.configure(state="normal")
self.export_md_button.configure(state="normal")
self._populate_findings()
self._update_summary_cards(result)
counts = result.counts_by_severity
summary_bits = [
f"{result.files_scanned} files scanned",
f"{counts['Critical']} critical",
f"{counts['High']} high",
f"{counts['Medium']} medium",
f"{counts['Low']} low",
]
if counts["Info"]:
summary_bits.append(f"{counts['Info']} info")
self.summary.configure(text=" · ".join(summary_bits))
skipped_note = f" {len(result.skipped_files)} file(s) skipped." if result.skipped_files else ""
limit_note = " A safety limit was reached; this scan may be incomplete." if result.truncated else ""
self.status.set(
f"Scan complete. Findings are deterministic; no files were changed or uploaded."
f"{skipped_note}{limit_note}"
)
def _populate_findings(self) -> None:
self._clear_table_rows()
if self.result is None:
return
for index, finding in enumerate(self.result.findings):
item_id = f"finding-{index}"
self.findings_by_id[item_id] = finding
tags = (finding.severity.value,)
if index % 2 == 1:
tags = tags + ("oddrow",)
self.table.insert(
"",
"end",
iid=item_id,
tags=tags,
values=(
finding.severity.value,
finding.rule_id,
finding.file_path,
finding.line,
finding.title,
),
)
self._apply_filters()
def _clear_table_rows(self) -> None:
item_ids = tuple(dict.fromkeys((*self.findings_by_id, *self.table.get_children())))
for item_id in item_ids:
if self.table.exists(item_id):
self.table.delete(item_id)
self.findings_by_id.clear()
def _apply_filters(self, *_: object) -> None:
query = self.search_text.get().strip().casefold()
severity = self.severity_filter.get()
for item_id, finding in self.findings_by_id.items():
matches_severity = severity == "All" or finding.severity.value == severity
haystack = " ".join((finding.rule_id, finding.title, finding.file_path, finding.evidence)).casefold()
visible = matches_severity and query in haystack
if visible and not self.table.exists(item_id):
self.table.reattach(item_id, "", "end")
elif not visible and self.table.exists(item_id):
self.table.detach(item_id)
def _selection_changed(self, _event: tk.Event) -> None:
selected = self.table.selection()
finding = self.findings_by_id.get(selected[0]) if selected else None
self.details.show_finding(finding)
self.explain_button.configure(state="normal" if finding else "disabled")
def _clear_findings(self) -> None:
self.result = None
self._clear_table_rows()
self._update_summary_cards(None)
self.summary.configure(text="Choose a project folder to begin.")
self.details.show_finding(None)
self.explain_button.configure(state="disabled")
self.export_json_button.configure(state="disabled")
self.export_md_button.configure(state="disabled")
def _handle_error(self, payload: object) -> None:
self.scan_button.configure(state="normal")
self._update_summary_cards(None)
self.summary.configure(text="Scan could not be completed.")
self.status.set("The operation could not be completed.")
messagebox.showerror("SecureLocal AI", str(payload))
def _explain_selected(self) -> None:
selected = self.table.selection()
if not selected:
return
finding = self.findings_by_id[selected[0]]
model = self.ai_model.get().strip()
self.explain_button.configure(state="disabled")
self.status.set("Checking the local Ollama service and installed model…")
thread = threading.Thread(target=self._explanation_worker, args=(finding, model), daemon=True)
thread.start()
def _explanation_worker(self, finding: Finding, model: str) -> None:
try:
explanation = OllamaClient().explain_finding(finding, model)
self.events.put(("explanation", (True, explanation)))
except OllamaError as error:
self.events.put(("explanation", (False, str(error))))
def _show_explanation(self, payload: tuple[bool, str]) -> None:
success, text = payload
self.explain_button.configure(state="normal" if self.table.selection() else "disabled")
if success:
{"pattern":"\"name\"|\"status\"|\"starts_at_utc\"|\"ends_at_utc\"","paths":"C:\\Users\\Kushal\\AppData\\Local\\Temp\\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt","output_mod...
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "status": "registered",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "name": "Global Hack Week: Hacktoberfest",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "status": "ended",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "starts_at_utc": "2026-10-09T14:00:00Z",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "ends_at_utc": "2026-10-16T17:00:00Z",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "status": "registered",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "name": "Hacktoberfest 2026",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "status": "in_progress",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "starts_at_utc": "2026-09-30T12:00:00Z",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "ends_at_utc": "2026-11-01T10:59:59Z",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "status": "registered",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "name": "Hacktoberfest Open-Source AI Challenge: Week 1",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "status": "ended",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "starts_at_utc": "2026-10-05T16:00:00Z",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "ends_at_utc": "2026-10-12T06:59:00Z",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "status": "checked_in",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "name": "[GHW Hacktoberfest] Building a Puzzle Web App with AI : Part 2",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "status": "ended",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "starts_at_utc": "2026-10-11T10:00:00Z",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "ends_at_utc": "2026-10-11T14:00:00Z",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "status": "checked_in",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "name": "[GHW Hacktoberfest] TigerData Workshop",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "status": "ended",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "starts_at_utc": "2026-10-12T14:00:00Z",
C:\Users\Kushal\AppData\Local\Temp\1792147625391-copilot-tool-output-794e7729dc934e9d979341e5fdeb4d46.txt: "ends_at_utc": "2026-10-12T15:00:00Z",
{"pattern":"\"title\"|\"starts_at\"|\"ends_at\"|\"type_of\"","paths":"C:\\Users\\Kushal\\AppData\\Local\\Temp\\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt","output_mode":"co...
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "title": "Hacktoberfest Open-Source AI Challenge: Week 4",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "type_of": "challenge",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "starts_at": "2026-10-26T16:00:00.000Z",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "ends_at": "2026-10-31T06:59:00.000Z",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "title": "Hacktoberfest Open-Source AI Challenge: Week 3",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "type_of": "challenge",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "starts_at": "2026-10-19T16:00:00.000Z",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "ends_at": "2026-10-25T06:59:00.000Z",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "title": "Hacktoberfest Open-Source AI Challenge: Week 2",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "type_of": "challenge",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "starts_at": "2026-10-12T19:00:00.000Z",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "ends_at": "2026-10-19T06:59:00.000Z",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "title": "Hacktoberfest Open-Source AI Challenge: Week 1",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "type_of": "challenge",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "starts_at": "2026-10-05T18:00:00.000Z",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "ends_at": "2026-10-12T06:59:00.000Z",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "title": "Hacktoberfest Weekend Challenge: Build for a Friend",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "type_of": "challenge",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "starts_at": "2026-10-02T02:00:00.000Z",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "ends_at": "2026-10-05T06:59:00.000Z",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "title": "Kaggle Benchmark Writing Challenge ",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "type_of": "challenge",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "starts_at": "2026-09-23T21:00:00.000Z",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "ends_at": "2026-10-12T06:59:00.000Z",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "title": "Sanity Challenge ",
{"pattern":"Layout First|grid|pack|geometry|resize|columnconfigure|rowconfigure","paths":"C:\\Users\\Kushal\\AppData\\Local\\Temp\\1792147625368-copilot-tool-output-d35e98427ba647edb528a201914bfa7c.tx...
Output too large to read at once (8.4 KB). Saved to: C:\Users\Kushal\AppData\Local\Temp\copilot-tool-output-1792147704897-1.txt
Consider using tools like rg (for searching), head/tail (for viewing start/end), view with view_range (for specific sections), or jq (for JSON) to examine portions of the output.
Preview (first 500 chars):
C:\Users\Kushal\AppData\Local\Temp\1792147625368-copilot-tool-output-d35e98427ba647edb528a201914bfa7c.txt: "Ok": "## 💥 The First Time I Tried Making a Login Form…\n\n\n\nMan. I remember my first Tkinter login form. \nSo proud of myself. Two entry boxes. A button. Done. How hard could it be?\n\n**📑 Table of Contents**\n\n - 💥 The First Time I Tried Making
The geometry confirms the source of the cramped table: the main content is correctly given the expanding grid row, but its vertical ttk.Panedwindow currently allocates resize growth at a 3:2 table/details weight. The details Text also uses Tk’s default height (24 lines), inflating its requested pane size. I’ll keep the existing split layout and scrollbars, give details a smaller scrollable starting height, and shift pane growth toward the table.
*** Begin Patch
*** Update File: c:\Projects\securelocal-ai\ui\main_window.py
@@
- self.table = ttk.Treeview(table_frame, columns=columns, show="headings", selectmode="browse")
+ self.ta...
diff --git a/c:/Projects/securelocal-ai/ui/main_window.py b/c:/Projects/securelocal-ai/ui/main_window.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/main_window.py
+++ b/c:/Projects/securelocal-ai/ui/main_window.py
@@ -158,7 +158,13 @@
table_frame.columnconfigure(0, weight=1)
table_frame.rowconfigure(0, weight=1)
columns = ("severity", "rule", "file", "line", "title")
- self.table = ttk.Treeview(table_frame, columns=columns, show="headings", selectmode="browse")
+ self.table = ttk.Treeview(
+ table_frame,
+ columns=columns,
+ show="headings",
+ selectmode="browse",
+ height=10,
+ )
self.table.tag_configure("oddrow", background="#F8FAFC")
self.table.tag_configure("Critical", foreground="#B42318")
self.table.tag_configure("High", foreground="#D97706")
@@ -183,8 +189,8 @@
horizontal.grid(row=1, column=0, sticky="ew")
self.table.configure(xscrollcommand=horizontal.set)
self.details = FindingDetails(content)
- content.add(table_frame, weight=3)
- content.add(self.details, weight=2)
+ content.add(table_frame, weight=4)
+ content.add(self.details, weight=1)
self.status = tk.StringVar(value="Scanning uses local deterministic rules. Source files are not changed or uploaded.")
ttk.Label(container, textvariable=self.status, style="Status.TLabel").grid(row=6, column=0, sticky="ew", pady=(8, 0))
diff --git a/c:/Projects/securelocal-ai/ui/finding_details.py b/c:/Projects/securelocal-ai/ui/finding_details.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/finding_details.py
+++ b/c:/Projects/securelocal-ai/ui/finding_details.py
@@ -14,6 +14,7 @@
ttk.Label(self, text="Finding details", style="Section.TLabel").grid(row=0, column=0, sticky="w")
self.content = tk.Text(
self,
+ height=6,
wrap="word",
state="disabled",
bg="#FFFFFF",
{
"command": "cd c:\\Projects\\securelocal-ai; python -m unittest discover -s tests -v",
"description": "Run the complete test suite after the layout adjustment",
"mode": "sync",
"initial_wait": 120
}
Shell output was automatically compacted. Compacted output below. Original at C:\Users\Kushal\AppData\Local\Temp\original-output-1792147857607-1f0edfc3af104332a5a68f9b5e58e8ac.txt; only use if exact omitted lines are needed.
[unittest progress: omitted 47 non-diagnostic line(s)]
[unittest progress: omitted 1 non-diagnostic line(s)]
Ran 47 tests in 0.357s
OK
<shellId: 11 completed with exit code 0>
{
"command": "cd c:\\Projects\\securelocal-ai; python -m compileall -q .",
"description": "Compile Python files after the layout adjustment",
"mode": "sync",
"initial_wait": 120
}
<shellId: 10 completed with exit code 0>
{
"query": "DevRelay get_challenge_details by challenge id inspect sponsor challenge descriptions"
}
[
"devrelay-gateway-get_challenge_details",
"devrelay-gateway-get_challenges",
"devrelay-gateway-get_concept_details",
"devrelay-gateway-update_challenge_entry",
"devrelay-gateway-get_mlh_event"
]
{"pattern":"Hacktoberfest Open-Source AI Challenge: Week 2","paths":"C:\\Users\\Kushal\\AppData\\Local\\Temp\\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt","output_mode":"con...
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt- },
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt- {
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt- "id": 80,
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "title": "Hacktoberfest Open-Source AI Challenge: Week 2",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt- "slug": "hacktoberfest-week2-2026-10-12",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "description": "The Hacktoberfest Open-Source AI Challenge: Week 2 runs October 12 to October 18. This week's theme is Side Hustle: build something with open-weight models or open-source AI that someone would actually pay for. $2,550 in prizes across 18 winners.\r\n\r\nThis year's Hacktoberfest is all about building brand-new projects with open-source AI and open-weight models. Every week brings a new theme, and every challenge is a fresh start, so you're competing on the same footing as everyone else.",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt- "details": null,
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt: "full_details": "The Hacktoberfest Open-Source AI Challenge: Week 2 runs October 12 to October 18. Build something with open-weight models or open-source AI that someone would actually pay for. $2,550 in prizes across 18 winners.\r\n\r\nThis year's Hacktoberfest is all about building brand-new projects with open-source AI and open-weight models. Week 2 is the second of five Hacktoberfest DEV Challenges. See all five challenges and every partner category on the HF26 DEV Challenge Hub: https://dev.to/challenges/hf26\r\n\r\nThis Week's Theme: Side Hustle\r\n\r\nBuild something with open-weight models or open-source AI that someone would actually pay for.\r\n\r\nThat can mean running an open-weight model, building on an open-source agent harness or framework, running inference locally, or all three. Whatever you pick, the open pieces should be what makes your project work.\r\n\r\nThink small, specific, and useful. The best builds here solve a real problem for a real person, whether that's a freelancer, a small business owner, a niche hobby community, or your friend who keeps complaining about the same tedious task. A few ideas to get you going:\r\n\r\n An invoice and receipt sorter for freelancers that runs locally, so client data never leaves their laptop\r\n A listing writer for Etsy or eBay sellers that turns a few product photos into a polished description\r\n A menu translator for a local restaurant that handles their specific dishes better than a generic tool\r\n\r\nIn your post, tell us why open innovation matters for what you built. Does running your own model keep costs low enough that the business actually works? Keep your customers' data private? Let you fine-tune for a niche that a general-purpose API handles poorly? Avoid getting locked into someone else's pricing?\r\n\r\nBonus points if you land your first user. Get it in front of someone, let them use it, and tell us how it went. Did they pay? Would they? What did they ask for next?\r\n\r\nPrize Categories\r\n\r\nAlongside the overall winner, there are 17 prize categories for projects that use a specific partner's technology. You don't need to use any of these to win the overall prize.\r\n\r\nFeatured categories: Best Use of Render, Best Use of TabPFN, Best Use of Tinker, Best Use of Arduino, Best Use of DigitalOcean, Best Use of Gemma\r\n\r\nPartner categories: Best Use of Backboard, Best Use of ElevenLabs, Best Use of Entire, Best Use of GitHub Copilot, Best Use of Mastra, Best Use of MongoDB Atlas, Best Use of Sentry Agent Tracing, Best Use of SerpApi, Best Use of Temporal, Best Use of Tiger Data, Best Use of a .tech Domain\r\n\r\nOne project can enter every category it genuinely uses, but each participant can only win once per challenge. Several partners are giving participants free credits and promo codes, including Tinker, Render, Backboard, and ElevenLabs. Claim yours at hacktoberfest.com/my.\r\n\r\nJudging Criteria\r\n\r\n Writing Quality (weighted most heavily): Is the post clear and engaging? Does it explain what you built, who it's for, and why open innovation matters?\r\n Relevance to the Prompt and Theme: Is open-source AI or open-weight models at the core of the project? Is it something someone would actually pay for?\r\n Creativity: Is it an original idea, or a fresh take on a familiar problem?\r\n Technical Execution: Does it work, and is it well built?\r\n Use of Partner Technology (optional): If entering a partner category, does the project use that technology in a meaningful way?\r\n\r\nSaving your agent session with DevRelay and embedding it in your post is optional, but it helps the judges understand your process.\r\n\r\nPrizes 🏆\r\n\r\n Overall winner (1): $250 USD, a DEV++ membership, and an exclusive DEV badge\r\n Featured category winners (6): $200 USD and an exclusive DEV badge each\r\n Partner category winners (11): $100 USD and an exclusive DEV badge each\r\n\r\nAll participants with a valid submission will receive a completion badge on their DEV profile.\r\n\r\nHow To Participate\r\n\r\nPublish a post on DEV using the Side Hustle submission template and be sure to include the required challenge tag: #hf26challenge. Tell us what you built and who would pay for it, show us a demo, and explain why open innovation matters for what you built.\r\n\r\nImportant Dates\r\n\r\n October 12: Hacktoberfest Open-Source AI Challenge: Week 2 begins!\r\n October 18: Submissions due at 11:59 PM PDT\r\n Week of October 19: Winners announced",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt- "type_of": "challenge",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt- "starts_at": "2026-10-12T19:00:00.000Z",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt- "ends_at": "2026-10-19T06:59:00.000Z",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt- "published": true,
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt- "cover_image": "https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/events/cover_image/80/16b4432f-e597-4e36-912d-ec622b7b90df.png",
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt- "location_url": null
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt- },
C:\Users\Kushal\AppData\Local\Temp\1792147423868-copilot-tool-output-2ea31a63b5134dfb8b4a4016e1a1fd01.txt- {
{"message":"The layout fix and requested validation are complete. This project may fit the active Hacktoberfest Open-Source AI Challenge: Week 2, which is about a useful project built around open-sour...
User responded:
draft_dev_post: false
Implemented the vertical layout adjustment.
The main content already expands with the window, but the vertical ttk.Panedwindow gave resize space a 3:2 table-to-details weight. The details text also requested Tk’s default 24-line height, reducing the table’s initial space. The table now explicitly requests 10 rows, the details text requests 6 lines and remains scrollable, and pane resize space is weighted 4:1 toward the table. Both panes retain their own scrollbars; the existing columns, severity styling, and duplicate-ID fix are unchanged.
Files changed
- ui/main_window.py
- ui/finding_details.py
Validation
- python -m unittest discover -s tests -v — 47 tests passed
- python -m compileall -q . — passed (no output)
I verified the layout configuration in code, but did not manually measure the maximized window’s rendered row count.
The active Hacktoberfest Open-Source AI Challenge: Week 2 focuses on useful projects built with open-source AI or open-weight models. SecureLocal AI’s local Ollama integration may be relevant, subject to the challenge’s submission rules.
🌐 Community Wisdom: 🐍 tkinter frame and label widget guide: common mistakes and how to fix them
Source: ptp2308
Tags:python,tkinter,guiThe article emphasizes getting layout structure right before refining appearance. Its advice is general; it does not specifically cover Panedwindow sizing. The article had no comments, so there was no discussion feedback to weigh.
🌐 Community Wisdom: Tkinter GUI guide
Source: hunter-intel
Tags:programming,beginners,python,discussThis is a basic widget guide rather than a layout case study; it illustrates that text-widget dimensions can be specified explicitly, but offers little evidence about pane proportions. It had no comments. Overall, the available community evidence is limited and indirect.
Inspect the JSON and Markdown report export handlers in SecureLocal AI.
Problem: When I scan tests/fixtures/vulnerable and export a Markdown report, the report is being saved inside the vulnerable fixture folder.
Please determine why the export dialog defaults to that directory.
Requirements:
- Identify how the export dialog's
initialdirand default filename are determined. - Change the default export directory to a dedicated
reports/folder at the project root, rather than the currently selected scan folder. - Create the
reports/directory if needed, using safe, predictable path handling. - Apply the same default location to both Markdown and JSON exports.
- Preserve the existing save dialog so I can choose another location.
- Do not automatically overwrite existing reports. Preserve the current overwrite confirmation behavior.
- Do not change report content, metadata, redaction, or export formats.
- Ensure exported reports are not accidentally scanned as project source files during normal test runs. Do not weaken scanner behavior to achieve this.
- Add focused tests if the export-path logic can be tested safely.
- Run
python -m unittest discover -s tests -vandpython -m compileall -q ., and report the actual results. Inspect the current implementation first and make the smallest appropriate change. Do not commit or push anything.
{
"skill": "devrelay-community-wisdom"
}
Skill "devrelay-community-wisdom" loaded successfully. Follow the instructions in the skill context.
{
"skill": "python-fact-grounded-coding"
}
Skill "python-fact-grounded-coding" loaded successfully. Follow the instructions in the skill context.
{
"query": "DevRelay search dev.to semantic articles Tkinter filedialog initialdir default filename exporting files relative paths"
}
[
"devrelay-gateway-search_dev_to_semantic",
"devrelay-gateway-search_concepts",
"devrelay-gateway-get_articles",
"devrelay-gateway-get_article_by_path",
"devrelay-gateway-get_comments"
]
{
"query": "Python Tkinter filedialog.asksaveasfilename initialdir default filename current working directory",
"per_page": 10
}
{
"Ok": [
{
"id": 4273802,
"title": "Python pathlib: Modern File Path Handling Guide",
"description": "Python pathlib: Modern File Path Handling Guide tags: python, tutorial, programming,...",
"tags": [
"python",
"tutorial",
"programming",
"beginners"
],
"path": "/qingluan/python-pathlib-modern-file-path-handling-guide-5076",
"url": "https://dev.to/qingluan/python-pathlib-modern-file-path-handling-guide-5076",
"score": 1.0
},
{
"id": 3846818,
"title": "File Handling in Python: Working With Real Data",
"description": "So far, everything we’ve done exists only inside the program. But what if your program could remember...",
"tags": [
"beginners",
"codenewbie",
"python",
"tutorial"
],
"path": "/stillpurrfect/file-handling-in-python-working-with-real-data-5g43",
"url": "https://dev.to/stillpurrfect/file-handling-in-python-working-with-real-data-5g43",
"score": 1.0
},
{
"id": 1124750,
"title": "How to get an absolute path in Python",
"description": "Here are the steps on how to get the absolute path in Python including an explanation of what an absolute path is.",
"tags": [
"python",
"path",
"absolute",
"os"
],
"path": "/dannysteenman/how-to-get-an-absolute-path-in-python-1fgj",
"url": "https://dev.to/dannysteenman/how-to-get-an-absolute-path-in-python-1fgj",
"score": 1.0
},
{
"id": 3824777,
"title": "File I/O in Python",
"description": "File I/O in Python: Read and Write Data Like a Script Working with files is a key skill...",
"tags": [
"automation",
"beginners",
"python",
"tutorial"
],
"path": "/nicholus_mush_af352791aab/file-io-in-python-4b8",
"url": "https://dev.to/nicholus_mush_af352791aab/file-io-in-python-4b8",
"score": 1.0
},
{
"id": 4812949,
"title": "I Built a Simple File Backup System With Python for My VPS",
"description": "Hello, I’m Arthur. One thing I learned after working with servers is that having a backup is not...",
"tags": [
"webdev",
"python",
"devops",
"linux"
],
"path": "/arthur_luca/i-built-a-simple-file-backup-system-with-python-for-my-vps-ldi",
"url": "https://dev.to/arthur_luca/i-built-a-simple-file-backup-system-with-python-for-my-vps-ldi",
"score": 1.0
},
{
"id": 4856495,
"title": "Portable PL Test Equipment: Keeping Sample IDs Attached to the Data",
"description": "A folder with three files called reading_01.txt, reading_02.txt and reading_03.txt looks tidy...",
"tags": [
"python",
"iot",
"dataengineering",
"tutorial"
],
"path": "/chesky0418/portable-pl-test-equipment-keeping-sample-ids-attached-to-the-data-2bmb",
"url": "https://dev.to/chesky0418/portable-pl-test-equipment-keeping-sample-ids-attached-to-the-data-2bmb",
"score": 1.0
},
{
"id": 4853497,
"title": "I Turned Three Beginner Python Scripts Into a Windows Desktop App",
"description": "I Turned Three Beginner Python Scripts Into a Windows Desktop App In May 2026 I was...",
"tags": [
"python",
"cybersecurity",
"beginners",
"ollama"
],
"path": "/quantumcatpy/i-turned-three-beginner-python-scripts-into-a-windows-desktop-app-d8o",
"url": "https://dev.to/quantumcatpy/i-turned-three-beginner-python-scripts-into-a-windows-desktop-app-d8o",
"score": 1.0
},
{
"id": 4840949,
"title": "Linux `cd`: Navigate Directories Without Losing Your Place",
"description": "When a command reads the wrong file or a relative path fails, the problem may be your current...",
"tags": [
"webdev",
"productivity",
"beginners",
"devops"
],
"path": "/__3381495fd2b/linux-cd-navigate-directories-without-losing-your-place-33og",
"url": "https://dev.to/__3381495fd2b/linux-cd-navigate-directories-without-losing-your-place-33og",
"score": 1.0
},
{
"id": 4847096,
"title": "pwd in Linux: Know Which Directory Your Shell Is In",
"description": "A command can behave differently depending on the directory your shell is in. Before you edit a file,...",
"tags": [
"productivity",
"javascript",
"beginners",
"devops"
],
"path": "/__3381495fd2b/pwd-in-linux-know-which-directory-your-shell-is-in-5c6m",
"url": "https://dev.to/__3381495fd2b/pwd-in-linux-know-which-directory-your-shell-is-in-5c6m",
"score": 1.0
},
{
"id": 4840675,
"title": "Refuse the Spike If the Assistant Nods First",
"description": "A backend developer opened a blank scratch directory after a standup left one cache expiry rule...",
"tags": [
"testing",
"python",
"ai",
"devops"
],
"path": "/applab_743/refuse-the-spike-if-the-assistant-nods-first-3ej1",
"url": "https://dev.to/applab_743/refuse-the-spike-if-the-assistant-nods-first-3ej1",
"score": 1.0
}
]
}
{
"query": "Tkinter file dialog default save directory handling export reports in Python application",
"per_page": 10
}
{
"Ok": [
{
"id": 3846818,
"title": "File Handling in Python: Working With Real Data",
"description": "So far, everything we’ve done exists only inside the program. But what if your program could remember...",
"tags": [
"beginners",
"codenewbie",
"python",
"tutorial"
],
"path": "/stillpurrfect/file-handling-in-python-working-with-real-data-5g43",
"url": "https://dev.to/stillpurrfect/file-handling-in-python-working-with-real-data-5g43",
"score": 1.0
},
{
"id": 4168847,
"title": "#05 – Python File Handling & Exceptions",
"description": "Welcome to Day 5! Today we shift from volatile, temporary in-memory variables to persistent storage...",
"tags": [
"ai",
"programming",
"productivity",
"python"
],
"path": "/thirusakthi-dev/05-file-handling-exceptions-3opk",
"url": "https://dev.to/thirusakthi-dev/05-file-handling-exceptions-3opk",
"score": 1.0
},
{
"id": 4805274,
"title": "Building a To-Do List App in Python (With File Saving)",
"description": "📌 Quick Info Topic: A beginner project that saves data to a file Target Audience:...",
"tags": [
"tutorial",
"programming",
"python",
"beginners"
],
"path": "/sameerqaiser17/building-a-to-do-list-app-in-python-with-file-saving-2o23",
"url": "https://dev.to/sameerqaiser17/building-a-to-do-list-app-in-python-with-file-saving-2o23",
"score": 1.0
},
{
"id": 3818248,
"title": "File Handling – Day 12",
"description": "File Handling In the previous lesson, we learned about Exception Handling and how Python manages...",
"tags": [
"beginners",
"coding",
"python",
"tutorial"
],
"path": "/augustineowino357design/python-programming-for-beginners-day-12-4boi",
"url": "https://dev.to/augustineowino357design/python-programming-for-beginners-day-12-4boi",
"score": 1.0
},
{
"id": 4856495,
"title": "Portable PL Test Equipment: Keeping Sample IDs Attached to the Data",
"description": "A folder with three files called reading_01.txt, reading_02.txt and reading_03.txt looks tidy...",
"tags": [
"python",
"iot",
"dataengineering",
"tutorial"
],
"path": "/chesky0418/portable-pl-test-equipment-keeping-sample-ids-attached-to-the-data-2bmb",
"url": "https://dev.to/chesky0418/portable-pl-test-equipment-keeping-sample-ids-attached-to-the-data-2bmb",
"score": 1.0
},
{
"id": 3819871,
"title": "File Handling in Python: Reading, Writing, and Managing Data",
"description": "Writing Files Using open() with \"w\" mode, you can create or overwrite files. The with statement...",
"tags": [
"beginners",
"coding",
"python",
"tutorial"
],
"path": "/nyandiatech/file-handling-in-python-reading-writing-and-managing-data-2h5j",
"url": "https://dev.to/nyandiatech/file-handling-in-python-reading-writing-and-managing-data-2h5j",
"score": 1.0
},
{
"id": 4853497,
"title": "I Turned Three Beginner Python Scripts Into a Windows Desktop App",
"description": "I Turned Three Beginner Python Scripts Into a Windows Desktop App In May 2026 I was...",
"tags": [
"python",
"cybersecurity",
"beginners",
"ollama"
],
"path": "/quantumcatpy/i-turned-three-beginner-python-scripts-into-a-windows-desktop-app-d8o",
"url": "https://dev.to/quantumcatpy/i-turned-three-beginner-python-scripts-into-a-windows-desktop-app-d8o",
"score": 1.0
},
{
"id": 4737909,
"title": "I got tired of cleaning messy CSVs by hand, so I wrote 5 tiny Python tools",
"description": "Every data job I've ever touched starts the same way: someone hands me an export that's almost...",
"tags": [
"beginners",
"python",
"productivity"
],
"path": "/weilidev2026/i-got-tired-of-cleaning-messy-csvs-by-hand-so-i-wrote-5-tiny-python-tools-13p3",
"url": "https://dev.to/weilidev2026/i-got-tired-of-cleaning-messy-csvs-by-hand-so-i-wrote-5-tiny-python-tools-13p3",
"score": 1.0
},
{
"id": 3179741,
"title": "🏠 Building an AI House Price Predictor with Python (Step‑by‑Step)",
"description": "In this beginner‑friendly tutorial, we’ll build HousePrice Sentinel, a desktop application that...",
"tags": [
"tutorial",
"tkinter",
"ttkbootstrap",
"python"
],
"path": "/matetechnologie/building-an-ai-house-price-predictor-with-python-step-by-step-n45",
"url": "https://dev.to/matetechnologie/building-an-ai-house-price-predictor-with-python-step-by-step-n45",
"score": 1.0
},
{
"id": 4827470,
"title": "Checking a File for a UTF-8 Signature in Python",
"description": "A CSV import can show an unexpected character before the first header name even when the visible text...",
"tags": [
"coding",
"python",
"tutorial"
],
"path": "/baptistsec/checking-a-file-for-a-utf-8-signature-in-python-3adn",
"url": "https://dev.to/baptistsec/checking-a-file-for-a-utf-8-signature-in-python-3adn",
"score": 1.0
}
]
}
{
"per_page": 10
}
{
}
I’ll inspect the export handler, report exporter, and tests first to confirm how the dialog picks its directory and filename, and whether existing report outputs could enter test scans.
{
"path": "c:\\Projects\\securelocal-ai\\ui\\main_window.py",
"view_range": [
380,
460
]
}
diff --git a/c:/Projects/securelocal-ai/ui/main_window.py b/c:/Projects/securelocal-ai/ui/main_window.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/main_window.py
+++ b/c:/Projects/securelocal-ai/ui/main_window.py
@@ -380,81 +380,81 @@
model = self.ai_model.get().strip()
self.explain_button.configure(state="disabled")
self.status.set("Checking the local Ollama service and installed model…")
thread = threading.Thread(target=self._explanation_worker, args=(finding, model), daemon=True)
thread.start()
def _explanation_worker(self, finding: Finding, model: str) -> None:
try:
explanation = OllamaClient().explain_finding(finding, model)
self.events.put(("explanation", (True, explanation)))
except OllamaError as error:
self.events.put(("explanation", (False, str(error))))
def _show_explanation(self, payload: tuple[bool, str]) -> None:
success, text = payload
self.explain_button.configure(state="normal" if self.table.selection() else "disabled")
if success:
dialog = tk.Toplevel(self.root)
dialog.title("Local AI explanation — unverified advice")
dialog.geometry("620x460")
dialog.transient(self.root)
frame = ttk.Frame(dialog, padding=12)
frame.pack(fill="both", expand=True)
ttk.Label(
frame,
text="AI-generated local explanation. It may be incorrect; confirm findings independently.",
wraplength=560,
).pack(anchor="w", pady=(0, 8))
content = tk.Text(frame, wrap="word")
content.pack(fill="both", expand=True)
content.insert("1.0", text)
content.configure(state="disabled")
scrollbar = ttk.Scrollbar(frame, orient="vertical", command=content.yview)
scrollbar.pack(side="right", fill="y")
content.configure(yscrollcommand=scrollbar.set)
ttk.Button(dialog, text="Close", command=dialog.destroy).pack(pady=(8, 0))
self.status.set("Local AI explanation received. Deterministic findings were not changed.")
else:
self.status.set("Local AI explanation unavailable; built-in remediation remains available.")
messagebox.showinfo("Local AI unavailable", str(text))
def _export_json(self) -> None:
self._export("json")
def _export_markdown(self) -> None:
self._export("markdown")
def _export(self, format_name: str) -> None:
if self.result is None:
return
extension = ".json" if format_name == "json" else ".md"
destination = filedialog.asksaveasfilename(
title=f"Export {format_name.upper()} report",
defaultextension=extension,
filetypes=[(f"{format_name.upper()} report", f"*{extension}")],
)
if not destination:
return
try:
exporter = export_json if format_name == "json" else export_markdown
path = exporter(self.result, destination)
except OSError as error:
messagebox.showerror("Report export failed", f"The report could not be saved: {error}")
return
self.status.set(f"Report exported to {path}. The report contains findings, not source files.")
def run_app() -> None:
root = tk.Tk()
SecureLocalApp(root)
root.mainloop()
{
"path": "c:\\Projects\\securelocal-ai\\reports\\exporter.py",
"view_range": [
1,
260
]
}
diff --git a/c:/Projects/securelocal-ai/reports/exporter.py b/c:/Projects/securelocal-ai/reports/exporter.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/reports/exporter.py
+++ b/c:/Projects/securelocal-ai/reports/exporter.py
@@ -1,260 +1,260 @@
from __future__ import annotations
import json
import re
from dataclasses import asdict
from pathlib import Path
from scanner.engine import SCANNER_VERSION
from scanner.models import Finding, ScanResult, Severity
from scanner.rules import RULES, redact_line
DISCLAIMER = (
"This tool reports heuristic patterns in Python source. Findings may be false positives or "
"miss vulnerabilities. Review the source and consult a qualified security professional for "
"a security assessment. It does not replace a professional audit."
)
SEVERITY_ORDER = {
Severity.CRITICAL: 0,
Severity.HIGH: 1,
Severity.MEDIUM: 2,
Severity.LOW: 3,
Severity.INFO: 4,
}
def _markdown_code(value: str) -> str:
longest_run = max(
(len(match.group(0)) for match in re.finditer(r"`+", value)),
default=0,
)
delimiter = "`" * (longest_run + 1)
return f"{delimiter}{value}{delimiter}"
def _finding_dict(finding: Finding) -> dict:
item = asdict(finding)
item["severity"] = finding.severity.value
item["evidence"] = redact_line(finding.evidence)[:240]
return item
def report_data(result: ScanResult) -> dict:
return {
"scanner_version": SCANNER_VERSION,
"scanned_at": result.scanned_at,
"project_name": result.project_name,
"files_scanned": result.files_scanned,
"files_skipped": [
{"file_path": item.file_path, "reason": item.reason}
for item in result.skipped_files
],
"counts_by_severity": result.counts_by_severity,
"truncated": result.truncated,
"findings": [_finding_dict(finding) for finding in result.findings],
"rule_limitations": [
{"rule_id": rule.rule_id, "title": rule.title, "limitations": rule.limitations}
for rule in RULES.values()
],
"disclaimer": DISCLAIMER,
}
def export_json(result: ScanResult, destination: str | Path) -> Path:
path = Path(destination)
content = json.dumps(report_data(result), indent=2, ensure_ascii=True) + "\n"
path.write_text(content, encoding="utf-8", newline="\n")
return path
def export_markdown(result: ScanResult, destination: str | Path) -> Path:
path = Path(destination)
lines = [
"# SecureLocal AI scan report",
"",
f"- **Scanner version:** {SCANNER_VERSION}",
f"- **Scanned at (UTC):** {result.scanned_at}",
f"- **Project:** {_markdown_code(result.project_name)}",
f"- **Python files scanned:** {result.files_scanned}",
f"- **Files skipped:** {len(result.skipped_files)}",
f"- **Scan truncated by a safety limit:** {'Yes' if result.truncated else 'No'}",
"",
"## Severity summary",
"",
]
for severity in Severity:
lines.append(f"- **{severity.value}:** {result.counts_by_severity[severity.value]}")
lines.extend(["", "## Findings", ""])
for finding in sorted(
result.findings,
key=lambda item: (
SEVERITY_ORDER[item.severity],
item.file_path,
item.line,
item.rule_id,
),
):
evidence = redact_line(finding.evidence)
lines.extend(
[
f"### {finding.severity.value} — {finding.title} (`{finding.rule_id}`)",
"",
f"- **Location:** {_markdown_code(f'{finding.file_path}:{finding.line}:{finding.column}')}",
f"- **Confidence:** {finding.confidence}",
f"- **Evidence:** {_markdown_code(evidence)}",
f"- **Why it matters:** {finding.description}",
f"- **Remediation:** {finding.remediation}",
f"- **Limitations:** {finding.limitations}",
"",
]
)
if not result.findings:
lines.append("No findings were reported by the enabled rules.")
lines.append("")
if result.skipped_files:
lines.extend(["## Skipped files", ""])
lines.extend(
f"- {_markdown_code(item.file_path)} — {item.reason}" for item in result.skipped_files
)
lines.append("")
lines.extend(["## Rule limitations", ""])
for rule in RULES.values():
lines.append(f"- **{rule.rule_id} — {rule.title}:** {rule.limitations}")
lines.append("")
lines.extend(["## Disclaimer", "", DISCLAIMER, ""])
path.write_text("\n".join(lines), encoding="utf-8", newline="\n")
return path
{
"path": "c:\\Projects\\securelocal-ai\\tests\\test_reports.py",
"view_range": [
1,
220
]
}
diff --git a/c:/Projects/securelocal-ai/tests/test_reports.py b/c:/Projects/securelocal-ai/tests/test_reports.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/tests/test_reports.py
+++ b/c:/Projects/securelocal-ai/tests/test_reports.py
@@ -1,220 +1,220 @@
from __future__ import annotations
import json
import tempfile
import unittest
from pathlib import Path
from reports.exporter import export_json, export_markdown
from scanner.engine import scan_project
from scanner.models import ScanResult
FIXTURES = Path(__file__).parent / "fixtures"
SECRET = "ghp_abcdefghijklmnopqrstuvwxyz012345"
class ReportTests(unittest.TestCase):
def test_json_and_markdown_reports_redact_credentials(self):
result = scan_project(FIXTURES / "vulnerable")
with tempfile.TemporaryDirectory() as directory:
json_path = export_json(result, Path(directory) / "report.json")
markdown_path = export_markdown(result, Path(directory) / "report.md")
json_text = json_path.read_text(encoding="utf-8")
markdown_text = markdown_path.read_text(encoding="utf-8")
self.assertNotIn(SECRET, json_text)
self.assertNotIn(SECRET, markdown_text)
self.assertIn("[REDACTED]", json_text)
self.assertIn("SLA007", json_text)
self.assertIn("professional audit", markdown_text)
def test_zero_findings_report_contains_summary_and_disclaimer(self):
result = scan_project(FIXTURES / "clean")
with tempfile.TemporaryDirectory() as directory:
path = export_markdown(result, Path(directory) / "clean.md")
content = path.read_text(encoding="utf-8")
self.assertIn("No findings were reported", content)
self.assertIn("Severity summary", content)
self.assertIn("Disclaimer", content)
def test_reports_redact_service_credentials_from_finding_evidence(self):
database_password = "db-password-value"
with tempfile.TemporaryDirectory() as directory:
root = Path(directory) / "project"
root.mkdir()
source = root / "query.py"
source.write_text(
"cursor.execute(f'SELECT * FROM users WHERE id = {user_id}') "
f"# [REDACTED]",
encoding="utf-8",
)
result = scan_project(root)
json_path = export_json(result, Path(directory) / "report.json")
markdown_path = export_markdown(result, Path(directory) / "report.md")
json_text = json_path.read_text(encoding="utf-8")
markdown_text = markdown_path.read_text(encoding="utf-8")
self.assertNotIn(database_password, json_text)
self.assertNotIn(database_password, markdown_text)
self.assertIn("[REDACTED]", json_text)
def test_reports_redact_bracketed_credentials_and_escape_markdown_evidence(self):
secret = "bracket-secret-value"
with tempfile.TemporaryDirectory() as directory:
root = Path(directory) / "project"
root.mkdir()
(root / "query.py").write_text(
f'eval(user_input); config["api_key"] = "{secret}" # ` <img src=x>\\n',
encoding="utf-8",
)
result = scan_project(root)
json_path = export_json(result, Path(directory) / "report.json")
markdown_path = export_markdown(result, Path(directory) / "report.md")
json_text = json_path.read_text(encoding="utf-8")
markdown_text = markdown_path.read_text(encoding="utf-8")
self.assertNotIn(secret, json_text)
self.assertNotIn(secret, markdown_text)
data = json.loads(json_text)
self.assertIn(
'config["api_key"] = "[REDACTED]"',
data["findings"][0]["evidence"],
)
self.assertIn("``", markdown_text)
self.assertNotIn("\\` <img src=x>", markdown_text)
def test_json_has_scanner_metadata_counts_and_multiple_findings(self):
result = scan_project(FIXTURES / "vulnerable")
with tempfile.TemporaryDirectory() as directory:
path = export_json(result, Path(directory) / "report.json")
data = json.loads(path.read_text(encoding="utf-8"))
self.assertEqual(data["scanner_version"], "0.1.0")
self.assertEqual(data["files_scanned"], 1)
self.assertGreater(len(data["findings"]), 1)
self.assertEqual(data["counts_by_severity"]["High"], 8)
def test_report_with_no_scanned_files_is_supported(self):
result = ScanResult("empty", "2026-01-01T00:00:00+00:00", 0, (), (), False)
with tempfile.TemporaryDirectory() as directory:
path = export_json(result, Path(directory) / "empty.json")
data = json.loads(path.read_text(encoding="utf-8"))
self.assertEqual(data["findings"], [])
self.assertEqual(data["counts_by_severity"]["Critical"], 0)
if __name__ == "__main__":
unittest.main()
{
"path": "c:\\Projects\\securelocal-ai\\scanner\\file_discovery.py",
"view_range": [
1,
220
]
}
diff --git a/c:/Projects/securelocal-ai/scanner/file_discovery.py b/c:/Projects/securelocal-ai/scanner/file_discovery.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/scanner/file_discovery.py
+++ b/c:/Projects/securelocal-ai/scanner/file_discovery.py
@@ -1,220 +1,220 @@
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
from scanner.models import SkippedFile
IGNORED_DIRECTORIES = frozenset(
{
".git",
".hg",
".svn",
".venv",
"venv",
"env",
"node_modules",
"__pycache__",
".tox",
".mypy_cache",
".pytest_cache",
".ruff_cache",
".idea",
".vscode",
"build",
"dist",
"coverage",
"htmlcov",
"site-packages",
}
)
DEFAULT_MAX_FILES = 5000
MAX_SKIPPED_FILES = 1000
@dataclass(frozen=True)
class DiscoveryResult:
root: Path
files: tuple[Path, ...]
skipped_files: tuple[SkippedFile, ...]
truncated: bool
def discover_python_files(
project_root: str | os.PathLike[str], max_files: int = DEFAULT_MAX_FILES
) -> DiscoveryResult:
root = Path(project_root).resolve(strict=True)
if not root.is_dir():
raise ValueError("The selected project path is not a directory.")
if max_files < 1:
raise ValueError("max_files must be greater than zero.")
files: list[Path] = []
skipped: list[SkippedFile] = []
truncated = False
stop_discovery = False
def add_skipped(item: SkippedFile) -> None:
if len(skipped) < MAX_SKIPPED_FILES:
skipped.append(item)
def on_walk_error(error: OSError) -> None:
path = Path(error.filename) if error.filename else root
try:
relative = path.relative_to(root).as_posix()
except ValueError:
relative = "."
add_skipped(SkippedFile(relative, "Directory could not be read."))
for current, directory_names, file_names in os.walk(
root, topdown=True, followlinks=False, onerror=on_walk_error
):
current_path = Path(current)
kept_directories: list[str] = []
for name in sorted(directory_names):
candidate = current_path / name
if name in IGNORED_DIRECTORIES or candidate.is_symlink():
continue
try:
candidate.resolve(strict=True).relative_to(root)
except (OSError, ValueError):
continue
kept_directories.append(name)
directory_names[:] = kept_directories
for name in sorted(file_names):
if not name.endswith(".py"):
continue
candidate = current_path / name
try:
relative = candidate.relative_to(root).as_posix()
except ValueError:
add_skipped(SkippedFile(name, "Path is outside the selected project."))
continue
if candidate.is_symlink():
add_skipped(SkippedFile(relative, "Symbolic links are not scanned."))
continue
try:
candidate.resolve(strict=True).relative_to(root)
except (OSError, ValueError):
add_skipped(SkippedFile(relative, "File is outside the selected project."))
continue
if len(files) >= max_files:
add_skipped(SkippedFile(relative, "Project file-count limit reached."))
truncated = True
stop_discovery = True
break
files.append(candidate)
if stop_discovery:
break
files.sort(key=lambda path: path.relative_to(root).as_posix())
skipped.sort(key=lambda item: (item.file_path, item.reason))
return DiscoveryResult(root, tuple(files), tuple(skipped), truncated)
{"pattern":"asksaveasfilename|initialdir|export_json|export_markdown|reports","paths":["c:\\Projects\\securelocal-ai\\ui","c:\\Projects\\securelocal-ai\\tests","c:\\Projects\\securelocal-ai\\scanner",...
[grep content: 31 matches across 7 file(s) under c:/Projects/securelocal-ai]
scanner/__main__.py:9: from reports.exporter import report_data
ui/main_window.py (11 match(es)):
10: from reports.exporter import export_json, export_markdown
138: self.export_json_button = ttk.Button(
141: command=self._export_json,
145: self.export_json_button.pack(side="right", padx=(8, 0))
149: command=self._export_markdown,
286: self.export_json_button.configure(state="normal")
365: self.export_json_button.configure(state="disabled")
421: def _export_json(self) -> None:
424: def _export_markdown(self) -> None:
431: destination = filedialog.asksaveasfilename(
439: exporter = export_json if format_name == "json" else export_markdown
scanner/rules.py:76: "This rule reports shell=True but cannot determine whether command arguments are attacker-controlled.",
reports/exporter.py (3 match(es)):
13: "This tool reports heuristic patterns in Python source. Findings may be false positives or "
63: def export_json(result: ScanResult, destination: str | Path) -> Path:
70: def export_markdown(result: ScanResult, destination: str | Path) -> Path:
tests/test_reports.py (13 match(es)):
8: from reports.exporter import export_json, export_markdown
17: def test_json_and_markdown_reports_redact_credentials(self):
20: json_path = export_json(result, Path(directory) / "report.json")
21: markdown_path = export_markdown(result, Path(directory) / "report.md")
33: path = export_markdown(result, Path(directory) / "clean.md")
39: def test_reports_redact_service_credentials_from_finding_evidence(self):
51: json_path = export_json(result, Path(directory) / "report.json")
52: markdown_path = export_markdown(result, Path(directory) / "report.md")
59: def test_reports_redact_bracketed_credentials_and_escape_markdown_evidence(self):
69: json_path = export_json(result, Path(directory) / "report.json")
70: markdown_path = export_markdown(result, Path(directory) / "report.md")
86: path = export_json(result, Path(directory) / "report.json")
96: path = export_json(result, Path(directory) / "empty.json")
tests/test_scanner.py:15: def test_vulnerable_fixture_reports_expected_rules_and_redacted_secret(self):
tests/test_ui_table.py:73: app.export_json_button = DummyWidget()
{
"query": "get full article content and comments for dev.to article ID"
}
[
"devrelay-gateway-get_comments",
"devrelay-gateway-get_article_content",
"devrelay-gateway-get_article_by_path",
"devrelay-gateway-get_page_by_id",
"devrelay-gateway-get_organization_articles"
]
{
"id": 4273802
}
{
"article_id": 4273802
}
The handler currently omits both initialdir and initialfile, so the native save dialog falls back to its current/last-used directory; selecting a fixture folder can therefore carry over to export. I’ll add one shared dialog-default helper anchored to the application’s project root, create reports/ if absent, and choose a free default filename while keeping overwrite confirmation enabled.
*** Begin Patch
*** Update File: c:\Projects\securelocal-ai\ui\main_window.py
@@
import queue
+import re
import threading
import tkinter as tk
@@
from ui.finding_details import FindingDetails
...
diff --git a/c:/Projects/securelocal-ai/ui/main_window.py b/c:/Projects/securelocal-ai/ui/main_window.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/main_window.py
+++ b/c:/Projects/securelocal-ai/ui/main_window.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import queue
+import re
import threading
import tkinter as tk
from pathlib import Path
@@ -13,6 +14,34 @@
from ui.finding_details import FindingDetails
+def _report_dialog_options(
+ result: ScanResult,
+ format_name: str,
+ project_root: Path | None = None,
+) -> dict[str, object]:
+ extension = ".json" if format_name == "json" else ".md"
+ reports_dir = (project_root or Path(__file__).resolve().parents[1]) / "reports"
+ reports_dir.mkdir(parents=True, exist_ok=True)
+
+ project_name = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", result.project_name).strip(" .")
+ if not project_name:
+ project_name = "project"
+ filename = f"securelocal-{project_name}-report"
+ candidate = reports_dir / f"{filename}{extension}"
+ suffix = 2
+ while candidate.exists():
+ candidate = reports_dir / f"{filename}-{suffix}{extension}"
+ suffix += 1
+
+ return {
+ "initialdir": str(reports_dir),
+ "initialfile": candidate.name,
+ "defaultextension": extension,
+ "filetypes": [(f"{format_name.upper()} report", f"*{extension}")],
+ "confirmoverwrite": True,
+ }
+
+
class SecureLocalApp:
def __init__(self, root: tk.Tk) -> None:
self.root = root
@@ -427,11 +456,9 @@
def _export(self, format_name: str) -> None:
if self.result is None:
return
- extension = ".json" if format_name == "json" else ".md"
destination = filedialog.asksaveasfilename(
title=f"Export {format_name.upper()} report",
- defaultextension=extension,
- filetypes=[(f"{format_name.upper()} report", f"*{extension}")],
+ **_report_dialog_options(self.result, format_name),
)
if not destination:
return
*** Begin Patch
*** Update File: c:\Projects\securelocal-ai\ui\main_window.py
@@
- project_name = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", result.project_name).strip(" .")
+ project_name = re.sub(r...
diff --git a/c:/Projects/securelocal-ai/ui/main_window.py b/c:/Projects/securelocal-ai/ui/main_window.py
index 0000000..0000000 100644
--- a/c:/Projects/securelocal-ai/ui/main_window.py
+++ b/c:/Projects/securelocal-ai/ui/main_window.py
@@ -23,7 +23,8 @@
reports_dir = (project_root or Path(__file__).resolve().parents[1]) / "reports"
reports_dir.mkdir(parents=True, exist_ok=True)
- project_name = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", result.project_name).strip(" .")
+ project_name = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "_", result.project_name)
+ project_name = project_name.strip(" .")[:100]
if not project_name:
project_name = "project"
filename = f"securelocal-{project_name}-report"
*** Begin Patch
*** Add File: c:\Projects\securelocal-ai\tests\test_ui_export.py
+from __future__ import annotations
+
+import tempfile
+import unittest
+from pathlib import Path
+
+from scanner.model...
diff --git a/c:/Projects/securelocal-ai/tests/test_ui_export.py b/c:/Projects/securelocal-ai/tests/test_ui_export.py
create file mode 100644
index 0000000..0000000
--- a/dev/null
+++ b/c:/Projects/securelocal-ai/tests/test_ui_export.py
@@ -1,0 +1,54 @@
+from __future__ import annotations
+
+import tempfile
+import unittest
+from pathlib import Path
+
+from scanner.models import ScanResult
+from ui.main_window import _report_dialog_options
+
+
+class ReportDialogOptionsTests(unittest.TestCase):
+ def test_json_and_markdown_defaults_use_project_reports_directory(self):
+ with tempfile.TemporaryDirectory() as directory:
+ project_root = Path(directory)
+ result = ScanResult("vulnerable", "2026-01-01T00:00:00+00:00", 0, (), (), False)
+
+ markdown = _report_dialog_options(result, "markdown", project_root)
+ json = _report_dialog_options(result, "json", project_root)
+
+ reports_dir = project_root / "reports"
+ self.assertTrue(reports_dir.is_dir())
+ self.assertEqual(markdown["initialdir"], str(reports_dir))
+ self.assertEqual(json["initialdir"], str(reports_dir))
+ self.assertEqual(markdown["initialfile"], "securelocal-vulnerable-report.md")
+ self.assertEqual(json["initialfile"], "securelocal-vulnerable-report.json")
+ self.assertTrue(markdown["confirmoverwrite"])
+ self.assertTrue(json["confirmoverwrite"])
+
+ def test_default_filename_avoids_existing_report(self):
+ with tempfile.TemporaryDirectory() as directory:
+ project_root = Path(directory)
+ reports_dir = project_root / "reports"
+ reports_dir.mkdir()
+ existing = reports_dir / "securelocal-vulnerable-report.md"
+ existing.write_text("existing report", encoding="utf-8")
+ result = ScanResult("vulnerable", "2026-01-01T00:00:00+00:00", 0, (), (), False)
+
+ options = _report_dialog_options(result, "markdown", project_root)
+
+ self.assertEqual(options["initialfile"], "securelocal-vulnerable-report-2.md")
+ self.assertEqual(existing.read_text(encoding="utf-8"), "existing report")
+
+ def test_project_name_is_sanitized_for_default_filename(self):
+ with tempfile.TemporaryDirectory() as directory:
+ result = ScanResult("bad/name:project", "2026-01-01T00:00:00+00:00", 0, (), (), False)
+
+ options = _report_dialog_options(result, "json", Path(directory))
+
+ self.assertEqual(options["initialfile"], "securelocal-bad_name_project-report.json")
+
+
+if __name__ == "__main__":
+ unittest.main()
+
{"command":"cd c:\\Projects\\securelocal-ai; python -m unittest tests.test_ui_export -v","description":"Run focused tests for report dialog default paths and collision handling","mode":"sync","initial...
test_default_filename_avoids_existing_report (tests.test_ui_export.ReportDialogOptionsTests.test_default_filename_avoids_existing_report) ... ok
test_json_and_markdown_defaults_use_project_reports_directory (tests.test_ui_export.ReportDialogOptionsTests.test_json_and_markdown_defaults_use_project_reports_directory) ... ok
test_project_name_is_sanitized_for_default_filename (tests.test_ui_export.ReportDialogOptionsTests.test_project_name_is_sanitized_for_default_filename) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.027s
OK
<shellId: 13 completed with exit code 0>
{"command":"cd c:\\Projects\\securelocal-ai; python -c \"from pathlib import Path; p=Path('reports'); print(p.resolve()); print('exists:', p.is_dir())\"","description":"Confirm the application-root re...
C:\Projects\securelocal-ai\reports
exists: True
<shellId: 12 completed with exit code 0>
The shared path helper now anchors both export formats to the application-root reports/ directory, sanitizes the scan’s project name for a default filename, and picks the next available default suffix when that filename already exists. The save dialog remains interactive and has overwrite confirmation enabled. Focused path tests pass; I’m running the full requested checks now.
{
"command": "cd c:\\Projects\\securelocal-ai; python -m unittest discover -s tests -v",
"description": "Run the complete test suite, including report export path tests",
"mode": "sync",
"initial_wait": 120
}
Shell output was automatically compacted. Compacted output below. Original at C:\Users\Kushal\AppData\Local\Temp\original-output-1792149695803-06bf82d2cb4a47adb0cce6359227c18d.txt; only use if exact omitted lines are needed.
[unittest progress: omitted 50 non-diagnostic line(s)]
[unittest progress: omitted 1 non-diagnostic line(s)]
Ran 50 tests in 0.347s
OK
<shellId: 14 completed with exit code 0>
{
"command": "cd c:\\Projects\\securelocal-ai; python -m compileall -q .",
"description": "Compile all Python modules in the project",
"mode": "sync",
"initial_wait": 120
}
<shellId: 15 completed with exit code 0>
Root cause: The export handler omitted both initialdir and initialfile, so the native save dialog chose its current or previously used directory. After browsing to the vulnerable fixture, that directory could become the default for the next export.
Both export formats now use the same helper to open in the project-root reports/ directory, creating it if needed. The helper builds a sanitized default filename and chooses an unused suffix if that filename already exists. The save dialog remains available for choosing another location, with overwrite confirmation enabled. The scanner and report contents were not changed; discovery continues to include only .py files.
Files changed
- ui/main_window.py
- tests/test_ui_export.py
Validation
- Focused export-path tests: 3 passed
- python -m unittest discover -s tests -v: 50 passed
- python -m compileall -q .: passed (no output)
🌐 Community Wisdom: Python pathlib: Modern File Path Handling Guide
Source: qingluan
Tags:python,tutorial,programming,beginnersThis search result is a general reference for Python path handling, relevant to anchoring the export directory to the application location rather than relying on the current working directory. I couldn’t retrieve the article body or comments, so this is limited evidence rather than a verified discussion of Tkinter save dialogs.
🌐 Community Wisdom: File Handling in Python: Working With Real Data
Source: stillpurrfect
Tags:beginners,codenewbie,python,tutorialSearch surfaced this as general Python file-handling guidance; it offers little specific evidence about Tkinter dialog defaults or overwrite prompts. The article body and comments weren’t available to inspect.
This post is an entry for the Side Hustle prompt: a small, specific tool built around open-weight AI, with its prospective users and the privacy/cost trade-offs stated plainly. No partner category is claimed.




Top comments (0)