wip multi-project
This commit is contained in:
51
config.toml
51
config.toml
@@ -1,53 +1,8 @@
|
|||||||
[output]
|
[projects]
|
||||||
namespace = "manual_slop"
|
|
||||||
output_dir = "./md_gen"
|
|
||||||
|
|
||||||
[files]
|
|
||||||
base_dir = "C:/projects/manual_slop"
|
|
||||||
paths = [
|
paths = [
|
||||||
"config.toml",
|
"C:/projects/manual_slop/manual_slop.toml",
|
||||||
"ai_client.py",
|
|
||||||
"aggregate.py",
|
|
||||||
"gemini.py",
|
|
||||||
"gui.py",
|
|
||||||
"pyproject.toml",
|
|
||||||
"MainContext.md",
|
|
||||||
"C:/projects/manual_slop/shell_runner.py",
|
|
||||||
"C:/projects/manual_slop/session_logger.py",
|
|
||||||
"C:/projects/manual_slop/file_cache.py",
|
|
||||||
"C:/projects/manual_slop/theme.py",
|
|
||||||
"C:/projects/manual_slop/project_manager.py",
|
|
||||||
]
|
|
||||||
|
|
||||||
[screenshots]
|
|
||||||
base_dir = "C:/Users/Ed/scoop/apps/sharex/current/ShareX/Screenshots/2026-02"
|
|
||||||
paths = [
|
|
||||||
"C:/Users/Ed/scoop/apps/sharex/current/ShareX/Screenshots/2026-02/2026-02-21_19-17-10.png",
|
|
||||||
"C:/Users/Ed/scoop/apps/sharex/current/ShareX/Screenshots/2026-02/Code_-_Insiders_2026-02-21_19-17-23.png",
|
|
||||||
]
|
|
||||||
|
|
||||||
[discussion]
|
|
||||||
history = [
|
|
||||||
"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.",
|
|
||||||
"AI:\nGood. Now I have a clear picture. I'll do the full patch in one Python script to avoid PowerShell string escaping issues:\n(Finished the job)",
|
|
||||||
"User:\nOk so the implementation works but when switching fonts it tends to crash (FiraCode, MapleMono, etc).",
|
|
||||||
"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!)",
|
|
||||||
"User:\nLets add multiple project support. currently this project would be the \"manual_slop\" project. \nI need to make a separate \"manual_slop.toml\" for this and offload things that would be within a per project to it.\nI also want to have several discussion histories that I can label. \nThis would allow me to stash and continue separate effort's histories..\nIt would be important if these discussions saved the last git commit they were on. (I'll need a buton to manually update it).\nAnd timestamps on each discussion entry to give an idea of the duration involved and \nhow the project has evolved since the discussion stream's effort was last done.\nBasically preserve context so ai can use that as valuable information in its working memory.\n\nTo load a project I'll need to hoist it into the config by providing the path.\nMaybe allow multiple project tomls to be specified path wise in the array.",
|
|
||||||
"AI:\nAnnotated by user:\nNo response from AI, taking sceenshot of scripts run... and directory's state",
|
|
||||||
"User:\nHey it seems like you were not done want to continue?\n",
|
|
||||||
"AI:\nDid work, No response from AI (this is an annotated entry by the user), made project_manager.py, adding it to md context.",
|
|
||||||
]
|
|
||||||
roles = [
|
|
||||||
"User",
|
|
||||||
"AI",
|
|
||||||
"Vendor API",
|
|
||||||
"System",
|
|
||||||
]
|
]
|
||||||
|
active = "C:/projects/manual_slop/manual_slop.toml"
|
||||||
|
|
||||||
[ai]
|
[ai]
|
||||||
provider = "anthropic"
|
provider = "anthropic"
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ Collapsed=0
|
|||||||
|
|
||||||
[Window][###22]
|
[Window][###22]
|
||||||
Pos=0,0
|
Pos=0,0
|
||||||
Size=609,326
|
Size=394,326
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x0000002F,0
|
DockId=0x0000002F,0
|
||||||
|
|
||||||
[Window][###30]
|
[Window][###30]
|
||||||
Pos=0,654
|
Pos=0,654
|
||||||
Size=609,696
|
Size=394,696
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000027,0
|
DockId=0x00000027,0
|
||||||
|
|
||||||
@@ -62,8 +62,8 @@ Size=700,440
|
|||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
[Window][###126]
|
[Window][###126]
|
||||||
Pos=611,0
|
Pos=396,0
|
||||||
Size=1152,2137
|
Size=1367,2137
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000031,0
|
DockId=0x00000031,0
|
||||||
|
|
||||||
@@ -113,8 +113,8 @@ Collapsed=0
|
|||||||
DockId=0x00000012,0
|
DockId=0x00000012,0
|
||||||
|
|
||||||
[Window][###100]
|
[Window][###100]
|
||||||
Pos=611,0
|
Pos=396,0
|
||||||
Size=1152,2137
|
Size=1367,2137
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000031,1
|
DockId=0x00000031,1
|
||||||
|
|
||||||
@@ -217,7 +217,7 @@ DockId=0x00000012,0
|
|||||||
|
|
||||||
[Window][###75]
|
[Window][###75]
|
||||||
Pos=0,1352
|
Pos=0,1352
|
||||||
Size=609,785
|
Size=394,785
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000022,0
|
DockId=0x00000022,0
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ DockId=0x00000012,2
|
|||||||
|
|
||||||
[Window][###142]
|
[Window][###142]
|
||||||
Pos=0,328
|
Pos=0,328
|
||||||
Size=609,324
|
Size=394,324
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000030,0
|
DockId=0x00000030,0
|
||||||
|
|
||||||
@@ -565,8 +565,8 @@ Collapsed=0
|
|||||||
DockId=0x0000002C,0
|
DockId=0x0000002C,0
|
||||||
|
|
||||||
[Window][###120]
|
[Window][###120]
|
||||||
Pos=611,0
|
Pos=396,0
|
||||||
Size=1152,2137
|
Size=1367,2137
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
DockId=0x00000031,2
|
DockId=0x00000031,2
|
||||||
|
|
||||||
@@ -645,9 +645,19 @@ Pos=1578,868
|
|||||||
Size=700,440
|
Size=700,440
|
||||||
Collapsed=0
|
Collapsed=0
|
||||||
|
|
||||||
|
[Window][###15029]
|
||||||
|
Pos=1578,868
|
||||||
|
Size=700,440
|
||||||
|
Collapsed=0
|
||||||
|
|
||||||
|
[Window][###15189]
|
||||||
|
Pos=1578,868
|
||||||
|
Size=700,440
|
||||||
|
Collapsed=0
|
||||||
|
|
||||||
[Docking][Data]
|
[Docking][Data]
|
||||||
DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,0 Size=3840,2137 Split=X Selected=0x40484D8F
|
DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,0 Size=3840,2137 Split=X Selected=0x40484D8F
|
||||||
DockNode ID=0x00000003 Parent=0x7C6B3D9B SizeRef=609,1161 Split=Y Selected=0xEE087978
|
DockNode ID=0x00000003 Parent=0x7C6B3D9B SizeRef=394,1161 Split=Y Selected=0xEE087978
|
||||||
DockNode ID=0x00000005 Parent=0x00000003 SizeRef=235,354 Split=Y Selected=0xEE087978
|
DockNode ID=0x00000005 Parent=0x00000003 SizeRef=235,354 Split=Y Selected=0xEE087978
|
||||||
DockNode ID=0x0000002D Parent=0x00000005 SizeRef=374,239 Split=Y 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=0x0000002F Parent=0x0000002D SizeRef=374,326 Selected=0xEE087978
|
||||||
@@ -662,7 +672,7 @@ DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,0 Size=3
|
|||||||
DockNode ID=0x00000022 Parent=0x0000001D SizeRef=549,785 Selected=0x0CE534DB
|
DockNode ID=0x00000022 Parent=0x0000001D SizeRef=549,785 Selected=0x0CE534DB
|
||||||
DockNode ID=0x0000001E Parent=0x00000009 SizeRef=364,715 Selected=0xF475F06A
|
DockNode ID=0x0000001E Parent=0x00000009 SizeRef=364,715 Selected=0xF475F06A
|
||||||
DockNode ID=0x0000000A Parent=0x00000006 SizeRef=235,350 Selected=0x80199DAE
|
DockNode ID=0x0000000A Parent=0x00000006 SizeRef=235,350 Selected=0x80199DAE
|
||||||
DockNode ID=0x00000004 Parent=0x7C6B3D9B SizeRef=3229,1161 Split=X
|
DockNode ID=0x00000004 Parent=0x7C6B3D9B SizeRef=3444,1161 Split=X
|
||||||
DockNode ID=0x00000001 Parent=0x00000004 SizeRef=1060,1161 Split=Y Selected=0x40484D8F
|
DockNode ID=0x00000001 Parent=0x00000004 SizeRef=1060,1161 Split=Y Selected=0x40484D8F
|
||||||
DockNode ID=0x00000007 Parent=0x00000001 SizeRef=595,492 Selected=0xBA13FCDE
|
DockNode ID=0x00000007 Parent=0x00000001 SizeRef=595,492 Selected=0xBA13FCDE
|
||||||
DockNode ID=0x00000008 Parent=0x00000001 SizeRef=595,1643 Split=X Selected=0x40484D8F
|
DockNode ID=0x00000008 Parent=0x00000001 SizeRef=595,1643 Split=X Selected=0x40484D8F
|
||||||
@@ -674,11 +684,11 @@ DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,0 Size=3
|
|||||||
DockNode ID=0x00000017 Parent=0x00000013 SizeRef=1314,1749 Selected=0x4B454E0B
|
DockNode ID=0x00000017 Parent=0x00000013 SizeRef=1314,1749 Selected=0x4B454E0B
|
||||||
DockNode ID=0x00000018 Parent=0x00000013 SizeRef=1309,1749 Split=Y Selected=0x88A8C2FF
|
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=0x00000019 Parent=0x00000018 SizeRef=2440,1412 Split=X Selected=0x88A8C2FF
|
||||||
DockNode ID=0x00000023 Parent=0x00000019 SizeRef=1962,737 Split=Y Selected=0x4F935A1E
|
DockNode ID=0x00000023 Parent=0x00000019 SizeRef=2177,737 Split=Y Selected=0x4F935A1E
|
||||||
DockNode ID=0x0000001F Parent=0x00000023 SizeRef=2315,1191 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=0x00000025 Parent=0x0000001F SizeRef=2315,1244 Split=X Selected=0x4F935A1E
|
||||||
DockNode ID=0x00000029 Parent=0x00000025 SizeRef=853,1867 Split=X Selected=0xFDB3860E
|
DockNode ID=0x00000029 Parent=0x00000025 SizeRef=853,1867 Split=X Selected=0xFDB3860E
|
||||||
DockNode ID=0x00000031 Parent=0x00000029 SizeRef=1152,2137 Selected=0x4A209654
|
DockNode ID=0x00000031 Parent=0x00000029 SizeRef=1367,2137 Selected=0x4A209654
|
||||||
DockNode ID=0x00000032 Parent=0x00000029 SizeRef=808,2137 CentralNode=1 Selected=0xFDB3860E
|
DockNode ID=0x00000032 Parent=0x00000029 SizeRef=808,2137 CentralNode=1 Selected=0xFDB3860E
|
||||||
DockNode ID=0x0000002A Parent=0x00000025 SizeRef=890,1867 Selected=0x40484D8F
|
DockNode ID=0x0000002A Parent=0x00000025 SizeRef=890,1867 Selected=0x40484D8F
|
||||||
DockNode ID=0x00000026 Parent=0x0000001F SizeRef=2315,621 Selected=0x7D28643F
|
DockNode ID=0x00000026 Parent=0x0000001F SizeRef=2315,621 Selected=0x7D28643F
|
||||||
|
|||||||
61
manual_slop.toml
Normal file
61
manual_slop.toml
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
[project]
|
||||||
|
name = "manual_slop"
|
||||||
|
git_dir = "C:/projects/manual_slop"
|
||||||
|
|
||||||
|
[output]
|
||||||
|
namespace = "manual_slop"
|
||||||
|
output_dir = "./md_gen"
|
||||||
|
|
||||||
|
[files]
|
||||||
|
base_dir = "C:/projects/manual_slop"
|
||||||
|
paths = [
|
||||||
|
"config.toml",
|
||||||
|
"ai_client.py",
|
||||||
|
"aggregate.py",
|
||||||
|
"gemini.py",
|
||||||
|
"gui.py",
|
||||||
|
"pyproject.toml",
|
||||||
|
"MainContext.md",
|
||||||
|
"C:/projects/manual_slop/shell_runner.py",
|
||||||
|
"C:/projects/manual_slop/session_logger.py",
|
||||||
|
"C:/projects/manual_slop/file_cache.py",
|
||||||
|
"C:/projects/manual_slop/theme.py",
|
||||||
|
"C:/projects/manual_slop/project_manager.py",
|
||||||
|
]
|
||||||
|
|
||||||
|
[screenshots]
|
||||||
|
base_dir = "C:/Users/Ed/scoop/apps/sharex/current/ShareX/Screenshots/2026-02"
|
||||||
|
paths = [
|
||||||
|
"C:/Users/Ed/scoop/apps/sharex/current/ShareX/Screenshots/2026-02/2026-02-21_19-17-10.png",
|
||||||
|
"C:/Users/Ed/scoop/apps/sharex/current/ShareX/Screenshots/2026-02/Code_-_Insiders_2026-02-21_19-17-23.png",
|
||||||
|
]
|
||||||
|
|
||||||
|
[discussion]
|
||||||
|
roles = [
|
||||||
|
"User",
|
||||||
|
"AI",
|
||||||
|
"Vendor API",
|
||||||
|
"System",
|
||||||
|
]
|
||||||
|
active = "main"
|
||||||
|
|
||||||
|
[discussion.discussions.main]
|
||||||
|
git_commit = ""
|
||||||
|
last_updated = "2026-02-21T19:26:54"
|
||||||
|
history = [
|
||||||
|
"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.",
|
||||||
|
"AI:\nGood. Now I have a clear picture. I'll do the full patch in one Python script to avoid PowerShell string escaping issues:\n(Finished the job)",
|
||||||
|
"User:\nOk so the implementation works but when switching fonts it tends to crash (FiraCode, MapleMono, etc).",
|
||||||
|
"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!)",
|
||||||
|
"User:\nLets add multiple project support. currently this project would be the \"manual_slop\" project. \nI need to make a separate \"manual_slop.toml\" for this and offload things that would be within a per project to it.\nI also want to have several discussion histories that I can label. \nThis would allow me to stash and continue separate effort's histories..\nIt would be important if these discussions saved the last git commit they were on. (I'll need a buton to manually update it).\nAnd timestamps on each discussion entry to give an idea of the duration involved and \nhow the project has evolved since the discussion stream's effort was last done.\nBasically preserve context so ai can use that as valuable information in its working memory.\n\nTo load a project I'll need to hoist it into the config by providing the path.\nMaybe allow multiple project tomls to be specified path wise in the array.",
|
||||||
|
"AI:\nAnnotated by user:\nNo response from AI, taking sceenshot of scripts run... and directory's state",
|
||||||
|
"User:\nHey it seems like you were not done want to continue?\n",
|
||||||
|
"AI:\nDid work, No response from AI (this is an annotated entry by the user), made project_manager.py, adding it to md context.",
|
||||||
|
"User:\ncontinue. You made a file but no changes have been made yet to gui.py. Still in progress to add all the multiple project support wishlist from user message earlier.",
|
||||||
|
"AI:\nNo response yet again maybe the response box is broken (User annotated)",
|
||||||
|
]
|
||||||
@@ -1,31 +1,5 @@
|
|||||||
# project_manager.py
|
# project_manager.py
|
||||||
"""
|
import subprocess, datetime, tomllib, tomli_w, re
|
||||||
Project-level config management for manual_slop.
|
|
||||||
|
|
||||||
A project file (e.g. manual_slop.toml) stores everything project-specific:
|
|
||||||
[project] name, git_dir
|
|
||||||
[output] namespace, output_dir
|
|
||||||
[files] base_dir, paths
|
|
||||||
[screenshots] base_dir, paths
|
|
||||||
[discussion]
|
|
||||||
roles = [...]
|
|
||||||
active = "main"
|
|
||||||
[discussion.discussions.main]
|
|
||||||
git_commit = ""
|
|
||||||
last_updated = ""
|
|
||||||
history = [...] # each entry: "@ISO\\nRole:\\ncontent"
|
|
||||||
|
|
||||||
The global config.toml keeps:
|
|
||||||
[projects] paths = [...], active = "..."
|
|
||||||
[ai] provider, model
|
|
||||||
[theme] palette, font_path, font_size, scale
|
|
||||||
"""
|
|
||||||
|
|
||||||
import subprocess
|
|
||||||
import datetime
|
|
||||||
import tomllib
|
|
||||||
import tomli_w
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
TS_FMT = "%Y-%m-%dT%H:%M:%S"
|
TS_FMT = "%Y-%m-%dT%H:%M:%S"
|
||||||
@@ -33,13 +7,14 @@ TS_FMT = "%Y-%m-%dT%H:%M:%S"
|
|||||||
def now_ts() -> str:
|
def now_ts() -> str:
|
||||||
return datetime.datetime.now().strftime(TS_FMT)
|
return datetime.datetime.now().strftime(TS_FMT)
|
||||||
|
|
||||||
def parse_ts(s: str) -> datetime.datetime | None:
|
def parse_ts(s: str):
|
||||||
try:
|
try: return datetime.datetime.strptime(s, TS_FMT)
|
||||||
return datetime.datetime.strptime(s, TS_FMT)
|
except: return None
|
||||||
except Exception:
|
|
||||||
return None
|
# ── entry serialisation ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
def entry_to_str(entry: dict) -> str:
|
def entry_to_str(entry: dict) -> str:
|
||||||
|
"""Serialise a disc entry dict -> stored string. Always uses real newlines."""
|
||||||
ts = entry.get("ts", "")
|
ts = entry.get("ts", "")
|
||||||
role = entry.get("role", "User")
|
role = entry.get("role", "User")
|
||||||
content = entry.get("content", "")
|
content = entry.get("content", "")
|
||||||
@@ -48,99 +23,106 @@ def entry_to_str(entry: dict) -> str:
|
|||||||
return f"{role}:\n{content}"
|
return f"{role}:\n{content}"
|
||||||
|
|
||||||
def str_to_entry(raw: str, roles: list[str]) -> dict:
|
def str_to_entry(raw: str, roles: list[str]) -> dict:
|
||||||
|
"""Parse a stored string back to a disc entry dict."""
|
||||||
ts = ""
|
ts = ""
|
||||||
rest = raw
|
rest = raw
|
||||||
|
# Strip optional leading @timestamp line
|
||||||
if rest.startswith("@"):
|
if rest.startswith("@"):
|
||||||
nl = rest.find("\n")
|
nl = rest.find("\n")
|
||||||
if nl != -1:
|
if nl != -1:
|
||||||
ts = rest[1:nl]
|
ts = rest[1:nl]
|
||||||
rest = rest[nl + 1:]
|
rest = rest[nl + 1:]
|
||||||
known = roles if roles else ["User", "AI", "Vendor API", "System"]
|
known = roles or ["User", "AI", "Vendor API", "System"]
|
||||||
role_pat = re.compile(
|
role_pat = re.compile(
|
||||||
r"^(?:\\[)?(" + "|".join(re.escape(r) for r in known) + r")(?:\\])?:?\\s*$",
|
r"^(?:\[)?(" + "|".join(re.escape(r) for r in known) + r")(?:\])?:?\s*$",
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
lines = rest.split("\n", 1)
|
parts = rest.split("\n", 1)
|
||||||
matched_role = "User"
|
matched_role = "User"
|
||||||
content = rest.strip()
|
content = rest.strip()
|
||||||
if lines:
|
if parts:
|
||||||
m = role_pat.match(lines[0].strip())
|
m = role_pat.match(parts[0].strip())
|
||||||
if m:
|
if m:
|
||||||
raw_role = m.group(1)
|
raw_role = m.group(1)
|
||||||
matched_role = next((r for r in known if r.lower() == raw_role.lower()), raw_role)
|
matched_role = next((r for r in known if r.lower() == raw_role.lower()), raw_role)
|
||||||
content = lines[1].strip() if len(lines) > 1 else ""
|
content = parts[1].strip() if len(parts) > 1 else ""
|
||||||
return {"role": matched_role, "content": content, "collapsed": False, "ts": ts}
|
return {"role": matched_role, "content": content, "collapsed": False, "ts": ts}
|
||||||
|
|
||||||
|
# ── git helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def get_git_commit(git_dir: str) -> str:
|
def get_git_commit(git_dir: str) -> str:
|
||||||
try:
|
try:
|
||||||
r = subprocess.run(["git", "rev-parse", "HEAD"],
|
r = subprocess.run(["git", "rev-parse", "HEAD"],
|
||||||
capture_output=True, text=True, cwd=git_dir, timeout=5)
|
capture_output=True, text=True, cwd=git_dir, timeout=5)
|
||||||
if r.returncode == 0:
|
return r.stdout.strip() if r.returncode == 0 else ""
|
||||||
return r.stdout.strip()
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
return ""
|
||||||
return ""
|
|
||||||
|
|
||||||
def get_git_log(git_dir: str, n: int = 5) -> str:
|
def get_git_log(git_dir: str, n: int = 5) -> str:
|
||||||
try:
|
try:
|
||||||
r = subprocess.run(["git", "log", "--oneline", f"-{n}"],
|
r = subprocess.run(["git", "log", "--oneline", f"-{n}"],
|
||||||
capture_output=True, text=True, cwd=git_dir, timeout=5)
|
capture_output=True, text=True, cwd=git_dir, timeout=5)
|
||||||
if r.returncode == 0:
|
return r.stdout.strip() if r.returncode == 0 else ""
|
||||||
return r.stdout.strip()
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
return ""
|
||||||
return ""
|
|
||||||
|
# ── default structures ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def default_discussion() -> dict:
|
def default_discussion() -> dict:
|
||||||
return {"git_commit": "", "last_updated": now_ts(), "history": []}
|
return {"git_commit": "", "last_updated": now_ts(), "history": []}
|
||||||
|
|
||||||
def default_project(name: str = "unnamed") -> dict:
|
def default_project(name: str = "unnamed") -> dict:
|
||||||
return {
|
return {
|
||||||
"project": {"name": name, "git_dir": ""},
|
"project": {"name": name, "git_dir": ""},
|
||||||
"output": {"namespace": name, "output_dir": "./md_gen"},
|
"output": {"namespace": name, "output_dir": "./md_gen"},
|
||||||
"files": {"base_dir": ".", "paths": []},
|
"files": {"base_dir": ".", "paths": []},
|
||||||
"screenshots": {"base_dir": ".", "paths": []},
|
"screenshots": {"base_dir": ".", "paths": []},
|
||||||
"discussion": {
|
"discussion": {
|
||||||
"roles": ["User", "AI", "Vendor API", "System"],
|
"roles": ["User", "AI", "Vendor API", "System"],
|
||||||
"active": "main",
|
"active": "main",
|
||||||
"discussions": {"main": default_discussion()},
|
"discussions": {"main": default_discussion()},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def load_project(path: str | Path) -> dict:
|
# ── load / save ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def load_project(path) -> dict:
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
return tomllib.load(f)
|
return tomllib.load(f)
|
||||||
|
|
||||||
def save_project(proj: dict, path: str | Path):
|
def save_project(proj: dict, path):
|
||||||
with open(path, "wb") as f:
|
with open(path, "wb") as f:
|
||||||
tomli_w.dump(proj, f)
|
tomli_w.dump(proj, f)
|
||||||
|
|
||||||
|
# ── migration helper ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def migrate_from_legacy_config(cfg: dict) -> dict:
|
def migrate_from_legacy_config(cfg: dict) -> dict:
|
||||||
"""Build a project dict from a legacy config.toml. Does NOT save it."""
|
"""Build a fresh project dict from a legacy flat config.toml. Does NOT save."""
|
||||||
name = cfg.get("output", {}).get("namespace", "project")
|
name = cfg.get("output", {}).get("namespace", "project")
|
||||||
proj = default_project(name)
|
proj = default_project(name)
|
||||||
if "output" in cfg: proj["output"] = dict(cfg["output"])
|
for key in ("output", "files", "screenshots"):
|
||||||
if "files" in cfg: proj["files"] = dict(cfg["files"])
|
if key in cfg:
|
||||||
if "screenshots" in cfg: proj["screenshots"] = dict(cfg["screenshots"])
|
proj[key] = dict(cfg[key])
|
||||||
disc_cfg = cfg.get("discussion", {})
|
disc = cfg.get("discussion", {})
|
||||||
roles = disc_cfg.get("roles", ["User", "AI", "Vendor API", "System"])
|
proj["discussion"]["roles"] = disc.get("roles", ["User", "AI", "Vendor API", "System"])
|
||||||
old_history = disc_cfg.get("history", [])
|
main = proj["discussion"]["discussions"]["main"]
|
||||||
proj["discussion"]["roles"] = roles
|
main["history"] = disc.get("history", [])
|
||||||
proj["discussion"]["discussions"]["main"]["history"] = old_history
|
main["last_updated"] = now_ts()
|
||||||
proj["discussion"]["discussions"]["main"]["last_updated"] = now_ts()
|
|
||||||
return proj
|
return proj
|
||||||
|
|
||||||
|
# ── flat config for aggregate.run() ─────────────────────────────────────────
|
||||||
|
|
||||||
def flat_config(proj: dict, disc_name: str | None = None) -> dict:
|
def flat_config(proj: dict, disc_name: str | None = None) -> dict:
|
||||||
"""Return a flat config dict compatible with aggregate.run()."""
|
"""Return a flat config dict compatible with aggregate.run()."""
|
||||||
disc_section = proj.get("discussion", {})
|
disc_sec = proj.get("discussion", {})
|
||||||
name = disc_name or disc_section.get("active", "main")
|
name = disc_name or disc_sec.get("active", "main")
|
||||||
disc_data = disc_section.get("discussions", {}).get(name, {})
|
disc_data = disc_sec.get("discussions", {}).get(name, {})
|
||||||
return {
|
return {
|
||||||
"output": proj.get("output", {}),
|
"output": proj.get("output", {}),
|
||||||
"files": proj.get("files", {}),
|
"files": proj.get("files", {}),
|
||||||
"screenshots": proj.get("screenshots", {}),
|
"screenshots": proj.get("screenshots", {}),
|
||||||
"discussion": {
|
"discussion": {
|
||||||
"roles": disc_section.get("roles", []),
|
"roles": disc_sec.get("roles", []),
|
||||||
"history": disc_data.get("history", []),
|
"history": disc_data.get("history", []),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user