discussion history improvemnts

This commit is contained in:
2026-02-21 19:08:15 -05:00
parent 1d9c5a9b07
commit c0535bc9a6
4 changed files with 235 additions and 99 deletions

View File

@@ -130,3 +130,7 @@ Status line and colour legend live at the top of the Comms History window (above
- Discussion history excerpts could be individually toggleable for inclusion in the generated md
- `MAX_TOOL_ROUNDS` in `ai_client.py` caps agentic loops at 5 rounds; adjustable
- `COMMS_CLAMP_CHARS` in `gui.py` controls the character threshold for clamping heavy payload fields in the Comms History panel
**Discussion History panel (updated):**
- **Discussion History** - structured block editor; each entry has a collapse toggle (-/+), a role combo (populated from disc_roles config list), and a multiline content field; per-entry buttons: Ins (insert before), Del (remove); global toolbar buttons: + Entry, -All (collapse all), +All (expand all), Clear All, Save; collapsible **Roles** sub-section lets you add/remove role names which are persisted to config.toml [discussion] roles; -> History buttons on Message and Response panels append the current message/response as a new entry (collapsed=False)

View File

@@ -63,7 +63,6 @@ history = [
"AI:\nThe file has `\\r\\n` line endings mixed in. I'll write the replacement using a Python one-liner to avoid PowerShell line-ending issues:",
"User:\nSure, if you want also enforce LF if you want on all files. I manually attempted so in vscode.",
"AI:\nDoes work... (Noted by user)\n- Reponse:\nNow clean up the temp patch scripts and do a quick sanity check:",
"User:\n",
"User:\nLets add some theming to this. Add options to change the ui dpi scale and the font used, also add the ability to change the palette I guess. We can reference my 10x editor settings for inspiration:\n```\n[Omitted as its already been utilized]\n\n```",
"AI:\nGood. Now I'll patch `gui.py` — adding the Theme window and wiring it up. I'll make targeted insertions rather than a full rewrite:\n\n1. Add `import theme` at the top\n2. Add `_build_theme_window()` and its callbacks to `App`\n3. Call `theme.load_from_config()` at startup (in `run()`)\n4. Call `theme.save_to_config()` in `_flush_to_config()`\n5. Add `win_theme` to `_build_ui()`",
"User:\nSure lets patch gui.py.",
@@ -72,6 +71,8 @@ history = [
"AI:\nThe em-dash in the docstring differs. Let me match by a more robust regex:",
"User:\nOk it seems fixed now... (themeing for fonts, nice job). \nNow lets do those adjustments to discussion history:\nIt'd also be nice if we can collapse the discussion entries indvidual and with a collapse/expand all button.\nAlso there is no way right now for me to define new roles for the discussion other than by editing the source.\nShould at least be configurable frm the config.toml...",
"AI:\nGood, I have a clear picture. Here's the plan:\n\n**Changes needed:**\n1. **`config.toml`** — add `[discussion] roles = [...]` so roles are configurable\n2. **`gui.py`** — 4 targeted edits:\n - `DISC_ROLES` becomes a module-level fallback; the real list comes from config at load time and lives on `App`\n - `_parse_history_entries()` takes `roles` as a parameter\n - `_rebuild_disc_list()` — add collapse/expand per entry (using a `bool` `collapsed` flag in each dict), Collapse All / Expand All buttons in the toolbar\n - Add a \"Roles\" sub-section in the Discussion window for adding/removing role names from the config list, and wire it into `_flush_to_config()`\n\nI'll do this as a Python patch script to avoid PowerShell string escaping issues:",
"User:\nSure, go ahead and make to those change patches and apply them.",
"AI:\nAll 16 checks pass. Now update `MainContext.md` and clean up the patch file:\n(Note from user: the AI did so!)",
]
[ai]

View File

@@ -10,13 +10,13 @@ Collapsed=0
[Window][###22]
Pos=0,0
Size=374,239
Size=609,326
Collapsed=0
DockId=0x0000002D,0
DockId=0x0000002F,0
[Window][###30]
Pos=0,654
Size=374,696
Size=609,696
Collapsed=0
DockId=0x00000027,0
@@ -62,9 +62,10 @@ Size=700,440
Collapsed=0
[Window][###126]
Pos=1578,868
Size=700,440
Pos=611,0
Size=699,2137
Collapsed=0
DockId=0x00000031,0
[Window][###147]
Pos=1578,868
@@ -103,7 +104,7 @@ DockId=0x00000014,0
Pos=2531,0
Size=1309,1690
Collapsed=0
DockId=0x00000029,0
DockId=0x00000032,0
[Window][###106]
Pos=601,0
@@ -112,10 +113,10 @@ Collapsed=0
DockId=0x00000012,0
[Window][###100]
Pos=2687,1049
Size=1153,1088
Pos=611,0
Size=699,2137
Collapsed=0
DockId=0x0000002C,0
DockId=0x00000031,1
[Window][###133]
Pos=1306,785
@@ -164,7 +165,7 @@ DockId=0x00000015,0
Pos=551,0
Size=1060,2137
Collapsed=0
DockId=0x00000029,1
DockId=0x00000032,1
[Window][###110]
Pos=2438,0
@@ -216,15 +217,15 @@ DockId=0x00000012,0
[Window][###75]
Pos=0,1352
Size=374,785
Size=609,785
Collapsed=0
DockId=0x00000022,0
[Window][###85]
Pos=1320,0
Size=1365,2137
Pos=1312,0
Size=1187,2137
Collapsed=0
DockId=0x00000029,0
DockId=0x00000032,0
[Window][###92]
Pos=376,0
@@ -245,9 +246,10 @@ Collapsed=0
DockId=0x00000012,2
[Window][###142]
Pos=1578,868
Size=700,440
Pos=0,328
Size=609,324
Collapsed=0
DockId=0x00000030,0
[Window][###244]
Pos=1578,868
@@ -300,7 +302,7 @@ DockId=0x00000028,0
Pos=998,0
Size=853,602
Collapsed=0
DockId=0x00000029,0
DockId=0x00000032,0
[Window][###89]
Pos=351,0
@@ -470,8 +472,8 @@ Size=700,440
Collapsed=0
[Window][###108]
Pos=2687,0
Size=1153,1047
Pos=2501,0
Size=1339,1331
Collapsed=0
DockId=0x0000002B,0
@@ -531,11 +533,50 @@ Pos=1578,868
Size=700,440
Collapsed=0
[Window][###2077]
Pos=1578,868
Size=1586,1128
Collapsed=0
[Window][###2533]
Pos=1578,868
Size=1286,1051
Collapsed=0
[Window][###2657]
Pos=1578,868
Size=700,440
Collapsed=0
[Window][###2785]
Pos=1578,868
Size=700,440
Collapsed=0
[Window][###2914]
Pos=1578,868
Size=700,440
Collapsed=0
[Window][###116]
Pos=2501,1333
Size=1339,804
Collapsed=0
DockId=0x0000002C,0
[Window][###120]
Pos=611,0
Size=699,2137
Collapsed=0
DockId=0x00000031,2
[Docking][Data]
DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,0 Size=3840,2137 Split=X Selected=0x40484D8F
DockNode ID=0x00000003 Parent=0x7C6B3D9B SizeRef=374,1161 Split=Y Selected=0xEE087978
DockNode ID=0x00000003 Parent=0x7C6B3D9B SizeRef=609,1161 Split=Y Selected=0xEE087978
DockNode ID=0x00000005 Parent=0x00000003 SizeRef=235,354 Split=Y Selected=0xEE087978
DockNode ID=0x0000002D Parent=0x00000005 SizeRef=374,239 Selected=0xEE087978
DockNode ID=0x0000002D Parent=0x00000005 SizeRef=374,239 Split=Y Selected=0xEE087978
DockNode ID=0x0000002F Parent=0x0000002D SizeRef=374,326 Selected=0xEE087978
DockNode ID=0x00000030 Parent=0x0000002D SizeRef=374,324 Selected=0x69F9D389
DockNode ID=0x0000002E Parent=0x00000005 SizeRef=374,411 Selected=0xFBBC1691
DockNode ID=0x00000006 Parent=0x00000003 SizeRef=235,805 Split=Y Selected=0x5F94F9BD
DockNode ID=0x00000009 Parent=0x00000006 SizeRef=235,453 Split=Y Selected=0x5F94F9BD
@@ -546,7 +587,7 @@ DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,0 Size=384
DockNode ID=0x00000022 Parent=0x0000001D SizeRef=549,785 Selected=0x0CE534DB
DockNode ID=0x0000001E Parent=0x00000009 SizeRef=364,715 Selected=0xF475F06A
DockNode ID=0x0000000A Parent=0x00000006 SizeRef=235,350 Selected=0x80199DAE
DockNode ID=0x00000004 Parent=0x7C6B3D9B SizeRef=3464,1161 Split=X
DockNode ID=0x00000004 Parent=0x7C6B3D9B SizeRef=3229,1161 Split=X
DockNode ID=0x00000001 Parent=0x00000004 SizeRef=1060,1161 Split=Y Selected=0x40484D8F
DockNode ID=0x00000007 Parent=0x00000001 SizeRef=595,492 Selected=0xBA13FCDE
DockNode ID=0x00000008 Parent=0x00000001 SizeRef=595,1643 Split=X Selected=0x40484D8F
@@ -558,16 +599,18 @@ DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,0 Size=384
DockNode ID=0x00000017 Parent=0x00000013 SizeRef=1314,1749 Selected=0x4B454E0B
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=1365,737 Split=Y Selected=0x4F935A1E
DockNode ID=0x00000023 Parent=0x00000019 SizeRef=1888,737 Split=Y Selected=0x4F935A1E
DockNode ID=0x0000001F Parent=0x00000023 SizeRef=2315,1191 Split=Y Selected=0x4F935A1E
DockNode ID=0x00000025 Parent=0x0000001F SizeRef=2315,1244 Split=X Selected=0x4F935A1E
DockNode ID=0x00000029 Parent=0x00000025 SizeRef=853,1867 CentralNode=1 Selected=0xFDB3860E
DockNode ID=0x00000029 Parent=0x00000025 SizeRef=853,1867 Split=X Selected=0xFDB3860E
DockNode ID=0x00000031 Parent=0x00000029 SizeRef=699,2137 Selected=0xC56063F4
DockNode ID=0x00000032 Parent=0x00000029 SizeRef=1187,2137 CentralNode=1 Selected=0xFDB3860E
DockNode ID=0x0000002A Parent=0x00000025 SizeRef=890,1867 Selected=0x40484D8F
DockNode ID=0x00000026 Parent=0x0000001F SizeRef=2315,621 Selected=0x7D28643F
DockNode ID=0x00000020 Parent=0x00000023 SizeRef=2315,944 Selected=0x4C2F06CB
DockNode ID=0x00000024 Parent=0x00000019 SizeRef=1153,737 Split=Y Selected=0x88A8C2FF
DockNode ID=0x0000002B Parent=0x00000024 SizeRef=1153,1047 Selected=0xB8D8893E
DockNode ID=0x0000002C Parent=0x00000024 SizeRef=1153,1088 Selected=0x88A8C2FF
DockNode ID=0x00000024 Parent=0x00000019 SizeRef=1339,737 Split=Y Selected=0xB8D8893E
DockNode ID=0x0000002B Parent=0x00000024 SizeRef=1153,1331 Selected=0xB8D8893E
DockNode ID=0x0000002C Parent=0x00000024 SizeRef=1153,804 Selected=0xCCB4E4FA
DockNode ID=0x0000001A Parent=0x00000018 SizeRef=2440,723 Selected=0x3A881EEF
DockNode ID=0x00000014 Parent=0x00000010 SizeRef=1967,445 Selected=0xC36FF36B
DockNode ID=0x00000002 Parent=0x00000004 SizeRef=2227,1161 Split=X Selected=0x714F2F7B

122
gui.py
View File

@@ -287,36 +287,38 @@ class ConfirmDialog:
DISC_ROLES = ["User", "AI", "Vendor API", "System"]
def _parse_history_entries(history: list[str]) -> list[dict]:
def _parse_history_entries(history: list[str], roles: list[str] | None = None) -> list[dict]:
"""
Convert the raw TOML string array into a flat list of {role, content} dicts.
Convert the raw TOML string array into a flat list of {role, content, collapsed} dicts.
Each TOML string is one excerpt (may contain multiple role blocks separated
by lines like "Role:" or "[Role]"). We detect the common patterns:
"User:\n..." "AI:\n..." "[User]\n..." "[AI]\n..."
and split accordingly. Unrecognised text becomes a User entry.
roles: list of known role strings (defaults to module DISC_ROLES if None).
"""
import re
known = roles if roles is not None else DISC_ROLES
entries: list[dict] = []
role_pattern = re.compile(
r'^(?:\[)?(' + '|'.join(re.escape(r) for r in DISC_ROLES) + r')(?:\])?:?\s*$',
r'^(?:\[)?(' + '|'.join(re.escape(r) for r in known) + r')(?:\])?:?\s*$',
re.IGNORECASE | re.MULTILINE,
)
for excerpt in history:
# Find all role header positions
splits = [(m.start(), m.end(), m.group(1).capitalize()) for m in role_pattern.finditer(excerpt)]
splits = [(m.start(), m.end(), m.group(1)) for m in role_pattern.finditer(excerpt)]
if not splits:
# No role headers found - treat whole excerpt as User content
text = excerpt.strip()
if text:
entries.append({"role": "User", "content": text})
entries.append({"role": "User", "content": text, "collapsed": False})
continue
# Extract content between headers
for idx, (start, end, role) in enumerate(splits):
next_start = splits[idx + 1][0] if idx + 1 < len(splits) else len(excerpt)
content = excerpt[end:next_start].strip()
# Normalise role capitalisation to match DISC_ROLES
matched = next((r for r in DISC_ROLES if r.lower() == role.lower()), role)
entries.append({"role": matched, "content": content})
# Normalise role capitalisation to match known list
matched = next((r for r in known if r.lower() == role.lower()), role)
entries.append({"role": matched, "content": content, "collapsed": False})
return entries
class App:
@@ -330,7 +332,10 @@ class App:
self.config.get("discussion", {}).get("history", [])
)
self.disc_entries: list[dict] = _parse_history_entries(self.history)
self.disc_roles: list[str] = list(
self.config.get("discussion", {}).get("roles", list(DISC_ROLES))
)
self.disc_entries: list[dict] = _parse_history_entries(self.history, self.disc_roles)
ai_cfg = self.config.get("ai", {})
self.current_provider: str = ai_cfg.get("provider", "gemini")
@@ -455,7 +460,7 @@ class App:
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["discussion"] = {"history": self.history, "roles": self.disc_roles}
self.config["ai"] = {
"provider": self.current_provider,
"model": self.current_model,
@@ -711,23 +716,38 @@ class App:
return
dpg.delete_item("disc_scroll", children_only=True)
for i, entry in enumerate(self.disc_entries):
collapsed = entry.get("collapsed", False)
preview = entry["content"].replace("\n", " ")[:60]
if len(entry["content"]) > 60:
preview += "..."
with dpg.group(parent="disc_scroll"):
with dpg.group(horizontal=True):
dpg.add_button(
tag=f"disc_toggle_{i}",
label="+" if collapsed else "-",
width=24,
callback=self._make_disc_toggle_cb(i),
)
dpg.add_combo(
tag=f"disc_role_{i}",
items=DISC_ROLES,
items=self.disc_roles,
default_value=entry["role"],
width=120,
width=160,
callback=self._make_disc_role_cb(i),
)
if collapsed:
dpg.add_button(
label="Insert Before",
label="Ins",
width=36,
callback=self._make_disc_insert_cb(i),
)
dpg.add_button(
label="Remove",
label="Del",
width=36,
callback=self._make_disc_remove_cb(i),
)
dpg.add_text(preview, color=(160, 160, 150))
with dpg.group(tag=f"disc_body_{i}", show=not collapsed):
dpg.add_input_text(
tag=f"disc_content_{i}",
default_value=entry["content"],
@@ -764,8 +784,62 @@ class App:
self._rebuild_disc_list()
return cb
def _make_disc_toggle_cb(self, idx: int):
def cb():
if idx < len(self.disc_entries):
tag = f"disc_content_{idx}"
if dpg.does_item_exist(tag):
self.disc_entries[idx]["content"] = dpg.get_value(tag)
self.disc_entries[idx]["collapsed"] = not self.disc_entries[idx].get("collapsed", False)
self._rebuild_disc_list()
return cb
def cb_disc_collapse_all(self):
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)
entry["collapsed"] = True
self._rebuild_disc_list()
def cb_disc_expand_all(self):
for entry in self.disc_entries:
entry["collapsed"] = False
self._rebuild_disc_list()
def _rebuild_disc_roles_list(self):
if not dpg.does_item_exist("disc_roles_scroll"):
return
dpg.delete_item("disc_roles_scroll", children_only=True)
for i, role in enumerate(self.disc_roles):
with dpg.group(horizontal=True, parent="disc_roles_scroll"):
dpg.add_button(
label="x", width=24,
callback=self._make_disc_remove_role_cb(i),
)
dpg.add_text(role)
def _make_disc_remove_role_cb(self, idx: int):
def cb():
if idx < len(self.disc_roles):
self.disc_roles.pop(idx)
self._rebuild_disc_roles_list()
self._rebuild_disc_list()
return cb
def cb_disc_add_role(self):
if not dpg.does_item_exist("disc_new_role_input"):
return
name = dpg.get_value("disc_new_role_input").strip()
if name and name not in self.disc_roles:
self.disc_roles.append(name)
dpg.set_value("disc_new_role_input", "")
self._rebuild_disc_roles_list()
self._rebuild_disc_list()
def cb_disc_append_entry(self):
self.disc_entries.append({"role": "User", "content": ""})
default_role = self.disc_roles[0] if self.disc_roles else "User"
self.disc_entries.append({"role": default_role, "content": "", "collapsed": False})
self._rebuild_disc_list()
def cb_disc_clear(self):
@@ -785,13 +859,13 @@ class App:
def cb_append_message_to_history(self):
msg = dpg.get_value("ai_input")
if msg:
self.disc_entries.append({"role": "User", "content": msg})
self.disc_entries.append({"role": "User", "content": msg, "collapsed": False})
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.disc_entries.append({"role": "AI", "content": resp, "collapsed": False})
self._rebuild_disc_list()
@@ -971,9 +1045,22 @@ class App:
):
with dpg.group(horizontal=True):
dpg.add_button(label="+ Entry", callback=self.cb_disc_append_entry)
dpg.add_button(label="-All", callback=self.cb_disc_collapse_all)
dpg.add_button(label="+All", callback=self.cb_disc_expand_all)
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.collapsing_header(label="Roles", default_open=False):
with dpg.child_window(tag="disc_roles_scroll", height=96, border=True):
pass
with dpg.group(horizontal=True):
dpg.add_input_text(
tag="disc_new_role_input",
hint="New role name",
width=-72,
)
dpg.add_button(label="Add", callback=self.cb_disc_add_role)
dpg.add_separator()
with dpg.child_window(tag="disc_scroll", height=-1, border=False):
pass
@@ -1100,6 +1187,7 @@ class App:
self._build_ui()
theme.load_from_config(self.config)
self._rebuild_disc_list()
self._rebuild_disc_roles_list()
self._fetch_models(self.current_provider)
while dpg.is_dearpygui_running():