This commit is contained in:
2026-02-21 18:36:37 -05:00
parent 383a3f3971
commit 7427b7a9d7
5 changed files with 154 additions and 37 deletions

View File

@@ -24,11 +24,11 @@
- **Config** - namespace, output dir, save
- **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** - multiline text box, `---` as separator between excerpts, save splits on `---` back into toml array
- **Discussion History** - structured block editor; each entry has a role combo (User/AI/Vendor API/System) and a multiline content field; buttons: Insert Before, Remove per entry; global buttons: + Entry, Clear All, Save; `-> History` buttons on Message and Response panels append the current message/response as a new entry
- **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
- **Response** - readonly multiline displaying last AI response
- **Tool Calls** - scrollable log of every PowerShell tool call the AI made, showing script and result; Clear button
- **Tool Calls** - scrollable log of every PowerShell tool call the AI made; shows first line of script + result (script body omitted from display, full script saved to `.ps1` file via session_logger); Clear button
- **Comms History** - rich structured live log of every API interaction; status line at top; colour legend; Clear button; each entry rendered with kind-specific layout rather than raw JSON
**Layout persistence:**
@@ -88,7 +88,7 @@ Status line and colour legend live at the top of the Comms History window (above
**Session Logger (session_logger.py):**
- `open_session()` called once at GUI startup; creates `logs/` and `scripts/generated/` directories; opens `logs/comms_<ts>.log` and `logs/toolcalls_<ts>.log` (line-buffered)
- `log_comms(entry)` appends each comms entry as a JSON-L line to the comms log; called from `App._on_comms_entry` (background thread); thread-safe via GIL + line buffering
- `log_tool_call(script, result, script_path)` appends a markdown-formatted tool-call record to the toolcalls log and writes the script to `scripts/generated/<ts>_<seq:04d>.ps1`; uses a `threading.Lock` for the sequence counter
- `log_tool_call(script, result, script_path)` writes the script to `scripts/generated/<ts>_<seq:04d>.ps1` and appends a markdown record to the toolcalls log **without** the script body (just the file path + result), keeping the log readable; uses a `threading.Lock` for the sequence counter
- `close_session()` flushes and closes both file handles; called just before `dpg.destroy_context()`
- `_on_tool_log` in `App` is wired to `ai_client.tool_log_callback` and calls `session_logger.log_tool_call`

File diff suppressed because one or more lines are too long

View File

@@ -286,7 +286,7 @@ Collapsed=0
[Window][###104]
Pos=1525,1246
Size=2315,607
Size=2315,621
Collapsed=0
DockId=0x00000026,0
@@ -309,8 +309,8 @@ Collapsed=0
DockId=0x00000012,1
[Window][###97]
Pos=1525,1855
Size=2315,282
Pos=1525,1869
Size=2315,268
Collapsed=0
DockId=0x00000020,0
@@ -369,6 +369,31 @@ Pos=1578,868
Size=700,440
Collapsed=0
[Window][###717]
Pos=1578,868
Size=700,440
Collapsed=0
[Window][###849]
Pos=1578,868
Size=700,440
Collapsed=0
[Window][###987]
Pos=1578,868
Size=700,440
Collapsed=0
[Window][###1131]
Pos=1578,868
Size=700,440
Collapsed=0
[Window][###1281]
Pos=1578,868
Size=700,440
Collapsed=0
[Docking][Data]
DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,0 Size=3840,2137 Split=X Selected=0x40484D8F
DockNode ID=0x00000003 Parent=0x7C6B3D9B SizeRef=599,1161 Split=Y Selected=0xEE087978
@@ -395,10 +420,10 @@ DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,0 Size=3840,
DockNode ID=0x00000018 Parent=0x00000013 SizeRef=1309,1749 Split=Y Selected=0x88A8C2FF
DockNode ID=0x00000019 Parent=0x00000018 SizeRef=2440,1412 Split=X Selected=0x88A8C2FF
DockNode ID=0x00000023 Parent=0x00000019 SizeRef=1160,737 Split=Y Selected=0x4F935A1E
DockNode ID=0x0000001F Parent=0x00000023 SizeRef=2315,1853 Split=Y Selected=0x4F935A1E
DockNode ID=0x0000001F Parent=0x00000023 SizeRef=2315,1867 Split=Y Selected=0x4F935A1E
DockNode ID=0x00000025 Parent=0x0000001F SizeRef=2315,1244 CentralNode=1 Selected=0x4F935A1E
DockNode ID=0x00000026 Parent=0x0000001F SizeRef=2315,607 Selected=0x7D28643F
DockNode ID=0x00000020 Parent=0x00000023 SizeRef=2315,282 Selected=0x4C2F06CB
DockNode ID=0x00000026 Parent=0x0000001F SizeRef=2315,621 Selected=0x7D28643F
DockNode ID=0x00000020 Parent=0x00000023 SizeRef=2315,268 Selected=0x4C2F06CB
DockNode ID=0x00000024 Parent=0x00000019 SizeRef=1153,737 Selected=0x88A8C2FF
DockNode ID=0x0000001A Parent=0x00000018 SizeRef=2440,723 Selected=0x3A881EEF
DockNode ID=0x00000014 Parent=0x00000010 SizeRef=1967,445 Selected=0xC36FF36B

143
gui.py
View File

@@ -426,15 +426,8 @@ class App:
dpg.delete_item("tool_log_scroll", children_only=True)
for i, (script, result) in enumerate(self._tool_log, 1):
with dpg.group(parent="tool_log_scroll"):
dpg.add_text(f"Call #{i}", color=(140, 200, 255))
dpg.add_input_text(
default_value=script,
multiline=True,
readonly=True,
width=-1,
height=72,
)
dpg.add_text("Result:", color=(180, 255, 180))
first_line = script.strip().splitlines()[0][:80] if script.strip() else "(empty)"
dpg.add_text(f"Call #{i}: {first_line}", color=(140, 200, 255))
dpg.add_input_text(
default_value=result,
multiline=True,
@@ -455,8 +448,12 @@ class App:
self.config["screenshots"]["base_dir"] = dpg.get_value("shots_base_dir")
self.config["files"]["paths"] = self.files
self.config["screenshots"]["paths"] = self.screenshots
raw = dpg.get_value("discussion_box")
self.history = [s.strip() for s in raw.split("---") if s.strip()]
# Pull latest content edits from disc widgets
for i, entry in enumerate(self.disc_entries):
tag = f"disc_content_{i}"
if dpg.does_item_exist(tag):
entry["content"] = dpg.get_value(tag)
self.history = self._disc_serialize()
self.config["discussion"] = {"history": self.history}
self.config["ai"] = {
"provider": self.current_provider,
@@ -672,6 +669,7 @@ class App:
self.available_models = []
self._rebuild_models_list()
self._fetch_models(self.current_provider)
self._rebuild_disc_list()
def cb_model_changed(self, sender, app_data):
if app_data:
@@ -697,6 +695,103 @@ class App:
# ---------------------------------------------------------------- build ui
# ------------------------------------------------------------ disc history
def _disc_serialize(self) -> list[str]:
"""Flatten disc_entries back to a single TOML history string per logical block."""
lines = []
for e in self.disc_entries:
lines.append(f"{e['role']}:\n{e['content']}")
return lines
def _rebuild_disc_list(self):
if not dpg.does_item_exist("disc_scroll"):
return
dpg.delete_item("disc_scroll", children_only=True)
for i, entry in enumerate(self.disc_entries):
with dpg.group(parent="disc_scroll"):
with dpg.group(horizontal=True):
dpg.add_combo(
tag=f"disc_role_{i}",
items=DISC_ROLES,
default_value=entry["role"],
width=120,
callback=self._make_disc_role_cb(i),
)
dpg.add_button(
label="Insert Before",
callback=self._make_disc_insert_cb(i),
)
dpg.add_button(
label="Remove",
callback=self._make_disc_remove_cb(i),
)
dpg.add_input_text(
tag=f"disc_content_{i}",
default_value=entry["content"],
multiline=True,
width=-1,
height=100,
callback=self._make_disc_content_cb(i),
on_enter=False,
)
dpg.add_separator()
def _make_disc_role_cb(self, idx: int):
def cb(sender, app_data):
if idx < len(self.disc_entries):
self.disc_entries[idx]["role"] = app_data
return cb
def _make_disc_content_cb(self, idx: int):
def cb(sender, app_data):
if idx < len(self.disc_entries):
self.disc_entries[idx]["content"] = app_data
return cb
def _make_disc_insert_cb(self, idx: int):
def cb():
self.disc_entries.insert(idx, {"role": "User", "content": ""})
self._rebuild_disc_list()
return cb
def _make_disc_remove_cb(self, idx: int):
def cb():
if idx < len(self.disc_entries):
self.disc_entries.pop(idx)
self._rebuild_disc_list()
return cb
def cb_disc_append_entry(self):
self.disc_entries.append({"role": "User", "content": ""})
self._rebuild_disc_list()
def cb_disc_clear(self):
self.disc_entries.clear()
self._rebuild_disc_list()
def cb_disc_save(self):
# Pull any in-progress edits from widgets into disc_entries
for i, entry in enumerate(self.disc_entries):
tag = f"disc_content_{i}"
if dpg.does_item_exist(tag):
entry["content"] = dpg.get_value(tag)
self._flush_to_config()
save_config(self.config)
self._update_status("discussion saved")
def cb_append_message_to_history(self):
msg = dpg.get_value("ai_input")
if msg:
self.disc_entries.append({"role": "User", "content": msg})
self._rebuild_disc_list()
def cb_append_response_to_history(self):
resp = self.ai_response
if resp:
self.disc_entries.append({"role": "AI", "content": resp})
self._rebuild_disc_list()
def _build_ui(self):
with dpg.window(
@@ -781,22 +876,17 @@ class App:
label="Discussion History",
tag="win_discussion",
pos=(824, 8),
width=400,
height=500,
width=420,
height=600,
no_close=True,
):
dpg.add_input_text(
tag="discussion_box",
default_value="\n---\n".join(self.history),
multiline=True,
width=-1,
height=-64,
)
dpg.add_separator()
with dpg.group(horizontal=True):
dpg.add_button(label="Add Separator", callback=self.cb_add_excerpt)
dpg.add_button(label="Clear", callback=self.cb_clear_discussion)
dpg.add_button(label="Save", callback=self.cb_save_discussion)
dpg.add_button(label="+ Entry", callback=self.cb_disc_append_entry)
dpg.add_button(label="Clear All", callback=self.cb_disc_clear)
dpg.add_button(label="Save", callback=self.cb_disc_save)
dpg.add_separator()
with dpg.child_window(tag="disc_scroll", height=-1, border=False):
pass
with dpg.window(
label="Provider",
@@ -846,6 +936,7 @@ class App:
dpg.add_button(label="Gen + Send", callback=self.cb_generate_send)
dpg.add_button(label="MD Only", callback=self.cb_md_only)
dpg.add_button(label="Reset", callback=self.cb_reset_session)
dpg.add_button(label="-> History", callback=self.cb_append_message_to_history)
with dpg.window(
label="Response",
@@ -860,8 +951,10 @@ class App:
multiline=True,
readonly=True,
width=-1,
height=-1,
height=-48,
)
dpg.add_separator()
dpg.add_button(label="-> History", callback=self.cb_append_response_to_history)
with dpg.window(
label="Tool Calls",

View File

@@ -108,12 +108,11 @@ def log_tool_call(script: str, result: str, script_path: str | None):
ps1_path = None
ps1_name = f"(write error: {exc})"
# Append to the tool-call sequence log
# Append to the tool-call sequence log (script body omitted - see .ps1 file)
try:
_tool_fh.write(
f"## Call #{seq} [{ts_entry}]\n"
f"Script file: {ps1_path}\n\n"
f"```powershell\n{script}\n```\n\n"
f"### Result\n\n"
f"```\n{result}\n```\n\n"
f"---\n\n"