27 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 classification, token estimation, and aggressive history truncationaggregate.py- reads config, collects files/screenshots/discussion, buildsfile_itemswithmtimefor cache optimization, writes numbered.mdfiles tooutput_dirusingbuild_markdown_from_itemsto avoid double I/O;run()returns(markdown_str, path, file_items)tuple;summary_only=Falseby default (full file contents sent, not heuristic summaries)shell_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 tools (read_file, list_directory, search_files, get_file_summary, web_search, fetch_url); allowlist enforced against project file_items + base_dirs for file tools; web tools are unrestricted; 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 = 10) 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 the last tool call in each round, project files from
file_itemsare checked via_reread_file_items(). It usesmtimeto only re-read modified files, returning only thechangedfiles to build a minimal[FILES UPDATED]block. - 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: refreshed file contents are appended to the last function response's
outputstring as a[SYSTEM: FILES UPDATED]block. On the next tool round, stale[FILES UPDATED]blocks are stripped from history and old tool outputs are truncated to_history_trunc_limitcharacters to control token growth. _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 & history management:
- System prompt + context are combined into one string, chunked into <=120k char blocks, and sent as the
system=parameter array. Only the LAST chunk getscache_control: ephemeral, so the entire system prefix is cached as one unit. - Last tool in
_ANTHROPIC_TOOLS(run_powershell) hascache_control: ephemeral; this means the tools prefix is cached together with the system prefix after the first request. - The user message is sent as a plain
[{"type": "text", "text": user_message}]block with NO cache_control. The context lives insystem=, not in the first user message. _add_history_cache_breakpointplacescache_control:ephemeralon the last content block of the second-to-last user message, using the 4th cache breakpoint to cache the conversation history prefix._trim_anthropic_historyuses token estimation (_CHARS_PER_TOKEN = 3.5) to keep the prompt under_ANTHROPIC_MAX_PROMPT_TOKENS = 180_000. It strips stale file refreshes from old turns, and drops oldest turn pairs if still over budget.- The tools list is built once per session via
_get_anthropic_tools()and reused across all API calls within the tool loop, avoiding redundant Python-side reconstruction. _strip_cache_controls()removes stalecache_controlmarkers from all history entries before each API call, ensuring only the stable system/tools prefix consumes cache breakpoint slots.- 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;TOOL_NAMESset now includes all six tool names- 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 resultsweb_search(query)queries DuckDuckGo HTML endpoint and returns the top 5 results (title, URL, snippet) as a formatted string; uses a custom_DDGParser(HTMLParser subclass)fetch_url(url)fetches a URL, strips HTML tags/scripts via_TextExtractor(HTMLParser subclass), collapses whitespace, and truncates to 40k chars to prevent context blowup; handles DuckDuckGo redirect links automaticallysummarize.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 - Discussion history excerpts could be individually toggleable for inclusion in the generated md
MAX_TOOL_ROUNDSinai_client.pycaps agentic loops at 10 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
- Gemini uses explicit caching via
client.caches.create()to store thesystem_instruction+ tools as an immutable cached prefix with a 1-hour TTL. The cache is created once per chat session. - Proactively rebuilds cache at 90% of
_GEMINI_CACHE_TTL = 3600to avoid stale-reference errors. - When context changes (detected via
md_contenthash), the old cache is deleted, a new cache is created, and chat history is migrated to a fresh chat session pointing at the new cache. - Trims history by dropping oldest pairs if input tokens exceed
_GEMINI_MAX_INPUT_TOKENS = 900_000. - If cache creation fails (e.g., content is under the minimum token threshold — 1024 for Flash, 4096 for Pro), the system falls back to inline
system_instructionin the chat config. Implicit caching may still provide cost savings in this case. - The
<context>block lives insidesystem_instruction, NOT in user messages, preventing history bloat across turns. - On cleanup/exit, active caches are deleted via
ai_client.cleanup()to prevent orphaned billing.
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.
Recent Changes (Text Viewer Maximization)
- Global Text Viewer (gui.py): Added a dedicated, large popup window (win_text_viewer) to allow reading and scrolling through large, dense text blocks without feeling cramped.
- Comms History: Every multi-line text field in the comms log now has a [+] button next to its label that opens the text in the Global Text Viewer.
- Tool Log History: Added [+ Script] and [+ Output] buttons next to each logged tool call to easily maximize and read the full executed scripts and raw tool outputs.
- Last Script Output Popup: Expanded the default size of the popup (now 800x600) and gave the input script panel more vertical space to prevent it from feeling 'scrunched'. Added [+ Maximize] buttons for both the script and the output sections to inspect them in full detail.
- Confirm Dialog: The script confirmation modal now has a [+ Maximize] button so you can read large generated scripts in full-screen before approving them.
UI Enhancements (2026-02-21)
Global Word-Wrap
A new Word-Wrap checkbox has been added to the Projects panel. This setting is saved per-project in its .toml file.
- When enabled (default), long text in read-only panels (like the main Response window, Tool Call outputs, and Comms History) will wrap to fit the panel width.
- When disabled, text will not wrap, and a horizontal scrollbar will appear for oversized content.
This allows you to choose the best viewing mode for either prose or wide code blocks.
Maximizable Discussion Entries
Each entry in the Discussion History now features a [+ Max] button. Clicking this button opens the full text of that entry in the large Text Viewer popup, making it easy to read or copy large blocks of text from the conversation history without being constrained by the small input box. \n\n## Multi-Viewport & Docking\nThe application now supports Dear PyGui Viewport Docking. Windows can be dragged outside the main application area or docked together. A global 'Windows' menu in the viewport menu bar allows you to reopen any closed panels.
Extensive Documentation (2026-02-22)
Documentation has been completely rewritten matching the strict, structural format of VEFontCache-Odin.
docs/guide_architecture.md: Details the Python implementation algorithms, queue management for UI rendering, the specific AST heuristics used for context aggregation, and the distinct algorithms for trimming Anthropic history vs Gemini state caching.docs/Readme.md: The core interface manual.docs/guide_tools.md: Security architecture for_is_allowedpaths and definitions of the read-only vs destructive tool pipeline.
Updates (2026-02-22 — ai_client.py & aggregate.py)
mcp_client.py — Web Tools Added
web_search(query)andfetch_url(url)added as two new MCP tools alongside the existing four file tools.TOOL_NAMESset updated to include all six tool names for dispatch routing.MCP_TOOL_SPECSlist extended with full JSON schema definitions for both web tools.- Both tools are declared in
_build_anthropic_tools()and_gemini_tool_declaration()so they are available to both providers. - Web tools bypass the
_is_allowedpath check (no filesystem access); file tools retain the allowlist enforcement.
aggregate.py — run() double-I/O elimination
run()now callsbuild_file_items()once, then passes the result tobuild_markdown_from_items()instead of callingbuild_files_section()separately. This avoids reading every file twice per send.build_markdown_from_items()accepts asummary_onlyflag (defaultFalse); whenFalseit inlines full file content; whenTrueit delegates tosummarize.build_summary_markdown()for compact structural summaries.run()returns a 3-tuple(markdown_str, output_path, file_items)— thefile_itemslist is passed through togui.pyasself.last_file_itemsfor dynamic context refresh after tool calls.