21 KiB
Manual Slop
Summary
Is a local GUI tool for manually curating and sending context to AI APIs. It aggregates files, screenshots, and discussion history into a structured markdown file and sends it to a chosen AI provider with a user-written message. The AI can also execute PowerShell scripts within the project directory, with user confirmation required before each execution.
Stack:
dearpygui- GUI with docking/floating/resizable panelsgoogle-genai- Gemini APIanthropic- Anthropic APItomli-w- TOML writinguv- package/env management
Files:
gui.py- main GUI,Appclass, all panels, all callbacks, confirmation dialog, layout persistence, rich comms renderingai_client.py- unified provider wrapper, model listing, session management, send, tool/function-call loop, comms log, provider error classificationaggregate.py- reads config, collects files/screenshots/discussion, writes numbered.mdfiles tooutput_dirshell_runner.py- subprocess wrapper that runs PowerShell scripts sandboxed tobase_dir, returns stdout/stderr/exit code as a stringsession_logger.py- opens timestamped log files at session start; writes comms entries as JSON-L and tool calls as markdown; saves each AI-generated script as a.ps1fileproject_manager.py- per-project .toml load/save, entry serialisation (entry_to_str/str_to_entry with @timestamp support), default_project/default_discussion factories, migrate_from_legacy_config, flat_config for aggregate.run(), git helpers (get_git_commit, get_git_log)theme.py- palette definitions, font loading, scale, load_from_config/save_to_configgemini.py- legacy standalone Gemini wrapper (not used by the main GUI; superseded byai_client.py)file_cache.py- stub; Anthropic Files API path removed; kept so stale imports don't breakmcp_client.py- MCP-style read-only file tools (read_file, list_directory, search_files, get_file_summary); allowlist enforced against project file_items + base_dirs; dispatched by ai_client tool-use loop for both Anthropic and Geminisummarize.py- local heuristic summariser (no AI); .py via AST, .toml via regex, .md headings, generic preview; used by mcp_client.get_file_summary and aggregate.build_summary_sectionconfig.toml- global-only settings: [ai] provider+model+system_prompt, [theme] palette+font+scale, [projects] paths array + active pathmanual_slop.toml- per-project file: [project] name+git_dir+system_prompt+main_context, [output] namespace+output_dir, [files] base_dir+paths, [screenshots] base_dir+paths, [discussion] roles+active+[discussion.discussions.] git_commit+last_updated+historycredentials.toml- gemini api_key, anthropic api_keydpg_layout.ini- Dear PyGui window layout file (auto-saved on exit, auto-loaded on startup); gitignore this per-user
GUI Panels:
- Projects - active project name display (green), git directory input + Browse button, scrollable list of loaded project paths (click name to switch, x to remove), Add Project / New Project / Save All buttons
- Config - namespace, output dir, save (these are project-level fields from the active .toml)
- Files - base_dir, scrollable path list with remove, add file(s), add wildcard
- Screenshots - base_dir, scrollable path list with remove, add screenshot(s)
- Discussion History - discussion selector (collapsible header): listbox of named discussions, git commit + last_updated display, Update Commit button, Create/Rename/Delete buttons with name input; structured entry editor: each entry has collapse toggle (-/+), role combo, timestamp display, multiline content field; per-entry Ins/Del buttons when collapsed; global toolbar: + Entry, -All, +All, Clear All, Save; collapsible Roles sub-section; -> History buttons on Message and Response panels append current message/response as new entry with timestamp
- Provider - provider combo (gemini/anthropic), model listbox populated from API, fetch models button
- Message - multiline input, Gen+Send button, MD Only button, Reset session button, -> History button
- Response - readonly multiline displaying last AI response, -> History button
- Tool Calls - scrollable log of every PowerShell tool call the AI made; Clear button
- System Prompts - global (all projects) and project-specific multiline text areas for injecting custom system instructions. Combined with the built-in tool prompt.
- Comms History - rich structured live log of every API interaction; status line at top; colour legend; Clear button
Layout persistence:
dpg.configure_app(..., init_file="dpg_layout.ini")loads the ini at startup if it exists; DPG silently ignores a missing filedpg.save_init_file("dpg_layout.ini")is called immediately beforedpg.destroy_context()on clean exit- The ini records window positions, sizes, and dock node assignments in DPG's native format
- First run (no ini) uses the hardcoded
pos=defaults in_build_ui(); after that the ini takes over - Delete
dpg_layout.inito reset to defaults
Project management:
config.tomlis global-only:[ai],[theme],[projects](paths list + active path). No project data lives here.- Each project has its own
.tomlfile (e.g.manual_slop.toml). Multiple project tomls can be registered by path. App.__init__loads global config, then loads the active project.tomlviaproject_manager.load_project(). Falls back tomigrate_from_legacy_config()if no valid project file exists, creating a new.tomlautomatically._flush_to_project()pulls widget values intoself.project(the per-project dict) and serialises disc_entries into the active discussion's history list_flush_to_config()writes global settings ([ai], [theme], [projects]) intoself.config_save_active_project()writesself.projectto the active.tomlpath viaproject_manager.save_project()_do_generate()calls both flush methods, saves both files, then usesproject_manager.flat_config()to produce the dict thataggregate.run()expects — soaggregate.pyneeds zero changes- Switching projects: saves current project, loads new one, refreshes all GUI state, resets AI session
- New project: file dialog for save path, creates default project structure, saves it, switches to it
Discussion management (per-project):
- Each project
.tomlstores one or more named discussions under[discussion.discussions.<name>] - Each discussion has:
git_commit(str),last_updated(ISO timestamp),history(list of serialised entry strings) activekey in[discussion]tracks which discussion is currently selected- Creating a discussion: adds a new empty discussion dict via
default_discussion(), switches to it - Renaming: moves the dict to a new key, updates
activeif it was the current one - Deleting: removes the dict; cannot delete the last discussion; switches to first remaining if active was deleted
- Switching: flushes current entries to project, loads new discussion's history, rebuilds disc list
- Update Commit button: runs
git rev-parse HEADin the project'sgit_dirand stores result + timestamp in the active discussion - Timestamps: each disc entry carries a
tsfield (ISO datetime); shown next to the role combo; new entries from-> Historyor+ Entrygetnow_ts()
Entry serialisation (project_manager):
entry_to_str(entry)→"@<ts>\n<role>:\n<content>"(or"<role>:\n<content>"if no ts)str_to_entry(raw, roles)→ parses optional@<ts>prefix, then role line, then content; returns{role, content, collapsed, ts}- Round-trips correctly through TOML string arrays; handles legacy entries without timestamps
AI Tool Use (PowerShell):
- Both Gemini and Anthropic are configured with a
run_powershelltool/function declaration - When the AI wants to edit or create files it emits a tool call with a
scriptstring ai_clientruns a loop (maxMAX_TOOL_ROUNDS = 5) feeding tool results back until the AI stops calling tools- Before any script runs,
gui.pyshows a modalConfirmDialogon the main thread; the background send thread blocks on athreading.Eventuntil the user clicks Approve or Reject - The dialog displays
base_dir, shows the script in an editable text box (allowing last-second tweaks), and has Approve & Run / Reject buttons - On approval the (possibly edited) script is passed to
shell_runner.run_powershell()which prependsSet-Location -LiteralPath '<base_dir>'and runs it viapowershell -NoProfile -NonInteractive -Command - stdout, stderr, and exit code are returned to the AI as the tool result
- Rejections return
"USER REJECTED: command was not executed"to the AI - All tool calls (script + result/rejection) are appended to
_tool_logand displayed in the Tool Calls panel
Dynamic file context refresh (ai_client.py):
- After every tool call round, all project files from
file_itemsare re-read from disk via_reread_file_items() - For Anthropic: the refreshed file contents are injected as a
textblock appended to thetool_resultsuser message, prefixed with[FILES UPDATED]and an instruction not to re-read them - For Gemini: files are re-read (updating the
file_itemslist in place) but cannot be injected into tool results due to Gemini's structured function response format _build_file_context_text(file_items)formats the refreshed files as markdown code blocks (same format as the original context)- The
tool_result_sendcomms log entry filters out the injected text block (only logs actualtool_resultentries) to keep the comms panel clean file_itemsflows fromaggregate.build_file_items()→gui.pyself.last_file_items→ai_client.send(file_items=...)→_send_anthropic(file_items=...)/_send_gemini(file_items=...)- System prompt updated to tell the AI: "the user's context files are automatically refreshed after every tool call, so you do NOT need to re-read files that are already provided in the block"
Anthropic bug fixes applied (session history):
- Bug 1: SDK ContentBlock objects now converted to plain dicts via
_content_block_to_dict()before storing in_anthropic_history; prevents re-serialisation failures on subsequent tool-use rounds - Bug 2:
_repair_anthropic_historysimplified to dict-only path since history always contains dicts - Bug 3: Gemini part.function_call access now guarded with
hasattrcheck - Bug 4: Anthropic
b.type == "tool_use"changed togetattr(b, "type", None) == "tool_use"for safe access during response processing
Comms Log (ai_client.py):
_comms_log: list[dict]accumulates every API interaction during a session_append_comms(direction, kind, payload)called at each boundary: OUT/request before sending, IN/response after each model reply, OUT/tool_call before executing, IN/tool_result after executing, OUT/tool_result_send when returning results to the model- Entry fields:
ts(HH:MM:SS),direction(OUT/IN),kind,provider,model,payload(dict) - Anthropic responses also include
usage(input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens) andstop_reasonin payload get_comms_log()returns a snapshot;clear_comms_log()empties itcomms_log_callback(injected by gui.py) is called from the background thread with each new entry; gui queues entries in_pending_comms(lock-protected) and flushes them to the DPG panel each render frameCOMMS_CLAMP_CHARS = 300in gui.py governs the display cutoff for heavy text fields
Comms History panel — rich structured rendering (gui.py):
Rather than showing raw JSON, each comms entry is rendered using a kind-specific renderer function. Unknown kinds fall back to a generic key/value layout.
Colour maps:
- Direction: OUT = blue-ish
(100,200,255), IN = green-ish(140,255,160) - Kind: request=gold, response=light-green, tool_call=orange, tool_result=light-blue, tool_result_send=lavender
- Labels: grey
(180,180,180); values: near-white(220,220,220); dict keys/indices:(140,200,255); numbers/token counts:(180,255,180); sub-headers:(220,200,120)
Helper functions:
_add_text_field(parent, label, value)— labelled text; strings longer thanCOMMS_CLAMP_CHARSrender as an 80px readonly scrollableinput_text; shorter strings render asadd_text_add_kv_row(parent, key, val)— single horizontal key: value row_render_usage(parent, usage)— renders Anthropic token usage dict in a fixed display order (input → cache_read → cache_creation → output)_render_tool_calls_list(parent, tool_calls)— iterates tool call list, showing name, id, and all args via_add_text_field
Kind-specific renderers (in _KIND_RENDERERS dict, dispatched by _render_comms_entry):
_render_payload_request— showsmessagefield via_add_text_field_render_payload_response— shows round, stop_reason (orange), text, tool_calls list, usage block_render_payload_tool_call— shows name, optional id, script via_add_text_field_render_payload_tool_result— shows name, optional id, output via_add_text_field_render_payload_tool_result_send— iterates results list, shows tool_use_id and content per result_render_payload_generic— fallback for unknown kinds; renders all keys, using_add_text_fieldfor keys in_HEAVY_KEYS,_add_kv_rowfor others; dicts/lists are JSON-serialised
Entry layout: index + timestamp + direction + kind + provider/model header row, then payload rendered by the appropriate function, then a separator line.
Session Logger (session_logger.py):
open_session()called once at GUI startup; createslogs/andscripts/generated/directories; openslogs/comms_<ts>.logandlogs/toolcalls_<ts>.log(line-buffered)log_comms(entry)appends each comms entry as a JSON-L line to the comms log; called fromApp._on_comms_entry(background thread); thread-safe via GIL + line bufferinglog_tool_call(script, result, script_path)writes the script toscripts/generated/<ts>_<seq:04d>.ps1and appends a markdown record to the toolcalls log without the script body (just the file path + result); uses athreading.Lockfor the sequence counterclose_session()flushes and closes both file handles; called just beforedpg.destroy_context()
Anthropic prompt caching:
- System prompt sent as an array with
cache_control: ephemeralon the text block - Last tool in
_ANTHROPIC_TOOLShascache_control: ephemeral; system + tools prefix is cached together after the first request - First user message content[0] is the
<context>block withcache_control: ephemeral; content[1] is the user question without cache control - Cache stats (creation tokens, read tokens) are surfaced in the comms log usage dict and displayed in the Comms History panel
Data flow:
- GUI edits are held in
Appstate (self.files,self.screenshots,self.disc_entries,self.project) and dpg widget values _flush_to_project()pulls all widget values intoself.projectdict (per-project data)_flush_to_config()pulls global settings intoself.configdict_do_generate()calls both flush methods, saves both files, callsproject_manager.flat_config(self.project, disc_name)to produce a dict foraggregate.run(), which writes the md and returns(markdown_str, path, file_items)cb_generate_send()calls_do_generate()then threads a call toai_client.send(md, message, base_dir)ai_client.send()prepends the md as a<context>block to the user message and sends via the active provider chat session- If the AI responds with tool calls, the loop handles them (with GUI confirmation) before returning the final text response
- Sessions are stateful within a run (chat history maintained),
Resetclears them, the tool log, and the comms log
Config persistence:
config.toml— global only:[ai]provider+model,[theme]palette+font+scale,[projects]paths array + active path<project>.toml— per-project: output, files, screenshots, discussion (roles, active discussion name, all named discussions with their history+metadata)- On every send and save, both files are written
- On clean exit,
run()calls_flush_to_project(),_save_active_project(),_flush_to_config(),save_config()before destroying context
Threading model:
- DPG render loop runs on the main thread
- AI sends and model fetches run on daemon background threads
_pending_dialog(guarded by athreading.Lock) is set by the background thread and consumed by the render loop each frame, callingdialog.show()on the main threaddialog.wait()blocks the background thread on athreading.Eventuntil the user acts_pending_comms(guarded by a separatethreading.Lock) is populated by_on_comms_entry(background thread) and drained by_flush_pending_comms()each render frame (main thread)
Provider error handling:
ProviderError(kind, provider, original)wraps upstream API exceptions with a classifiedkind: quota, rate_limit, auth, balance, network, unknown_classify_anthropic_errorand_classify_gemini_errorinspect exception types and status codes/message bodies to assign the kindui_message()returns a human-readable label for display in the Response panel
MCP file tools (mcp_client.py + ai_client.py):
- Four read-only tools exposed to the AI as native function/tool declarations:
read_file,list_directory,search_files,get_file_summary - Access control:
mcp_client.configure(file_items, extra_base_dirs)is called before each send; builds an allowlist of resolved absolute paths from the project'sfile_itemsplus thebase_dir; any path that is not explicitly in the list or not under one of the allowed directories returnsACCESS DENIED mcp_client.dispatch(tool_name, tool_input)is the single dispatch entry point used by both Anthropic and Gemini tool-use loops- Anthropic: MCP tools appear before
run_powershellin the tools list (nocache_controlon them; onlyrun_powershellcarriescache_control: ephemeral) - Gemini: MCP tools are included in the
FunctionDeclarationlist alongsiderun_powershell get_file_summaryusessummarize.summarise_file()— same heuristic used for the initial<context>block, so the AI gets the same compact structural view it already knowslist_directorysorts dirs before files; shows name, type, and sizesearch_filesusesPath.glob()with the caller-supplied pattern (supports**/*.pystyle)read_filereturns raw UTF-8 text; errors (not found, access denied, decode error) are returned as error strings rather than exceptions, so the AI sees them as tool resultssummarize.pyheuristics:.py→ AST imports + ALL_CAPS constants + classes+methods + top-level functions;.toml→ table headers + top-level keys;.md→ h1–h3 headings with indentation; all others → line count + first 8 lines preview- Comms log: MCP tool calls log
OUT/tool_callwith{"name": ..., "args": {...}}andIN/tool_resultwith{"name": ..., "output": ...}; rendered in the Comms History panel via_render_payload_tool_call(shows each arg key/value) and_render_payload_tool_result(shows output)
Known extension points:
- Add more providers by adding a section to
credentials.toml, a_list_*and_send_*function inai_client.py, and the provider name to thePROVIDERSlist ingui.py - System prompt support could be added as a field in the project
.tomland passed inai_client.send() - Discussion history excerpts could be individually toggleable for inclusion in the generated md
MAX_TOOL_ROUNDSinai_client.pycaps agentic loops at 5 rounds; adjustableCOMMS_CLAMP_CHARSingui.pycontrols the character threshold for clamping heavy payload fields in the Comms History panel- Additional project metadata (description, tags, created date) could be added to
[project]in the per-project toml
Gemini Context Management
- Investigating ways to prevent context duplication in _gemini_chat history, as currently {md_content} is prepended to the user message on every single request, causing history bloat.
- Discussing explicit Gemini Context Caching API (client.caches.create()) to store read-only file context and avoid re-reading files across sessions.
Latest Changes
- Removed
Configpanel from the GUI to streamline per-project configuration. output_dirwas moved into the Projects panel.auto_add_historywas moved to the Discussion History panel.namespaceis no longer a configurable field;aggregate.pyautomatically uses the active project'snameproperty.
UI / Visual Updates
- The success blink notification on the response text box is now dimmer and more transparent to be less visually jarring.
- Added a new floating Last Script Output popup window. This window automatically displays and blinks blue whenever the AI executes a PowerShell tool, showing both the executed script and its result in real-time.