Skip to content

feat(webui): config editor with groups and sources form#31

Merged
jimpablo merged 3 commits into
mainfrom
feat/sidebar-groups-config
May 2, 2026
Merged

feat(webui): config editor with groups and sources form#31
jimpablo merged 3 commits into
mainfrom
feat/sidebar-groups-config

Conversation

@jimpablo
Copy link
Copy Markdown
Collaborator

@jimpablo jimpablo commented May 2, 2026

No description provided.

Copilot AI review requested due to automatic review settings May 2, 2026 19:50
jimpablo added 3 commits May 3, 2026 03:52
Signed-off-by: jimpablo <194239734+jimpablo@users.noreply.github.com>

feat(webui): config editor with YAML and form modes in sidebar

Signed-off-by: jimpablo <194239734+jimpablo@users.noreply.github.com>
Signed-off-by: jimpablo <194239734+jimpablo@users.noreply.github.com>
… card

Signed-off-by: jimpablo <194239734+jimpablo@users.noreply.github.com>
@jimpablo jimpablo force-pushed the feat/sidebar-groups-config branch from e2419ca to 8f12d73 Compare May 2, 2026 19:54
@jimpablo jimpablo merged commit c2191b8 into main May 2, 2026
3 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a WebUI configuration editor (YAML + form) and backend endpoints to read/write/reload config.yaml, plus introduces group-to-policy linking for policy fallback behavior.

Changes:

  • Added WebUI “Config” settings panel with a YAML editor and a structured form editor for daemon/agent/paths/groups/sources.
  • Implemented API endpoints for config.yaml management (GET/PUT /api/settings/config, POST /api/settings/config/reload) and a daemon restart endpoint (POST /api/daemon/restart).
  • Refactored groups to map group_name -> policy_basename and updated dispatcher fallback to load a group-linked policy action.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
tests/test_api_config.py New API tests covering config get/save/reload behavior.
loom/api_server.py Adds config management endpoints, group payload changes, and daemon restart endpoint.
loom/config.py Changes groups representation and adds config diff + restart-required field list.
loom/orchestrator/dispatcher.py Updates group fallback behavior to load policy action by name.
loom/orchestrator/policy.py Adds helper to load a “flat” policy action YAML by name.
loom/webui/frontend/src/pages/config/* New config UI: panel, YAML editor, form editor, mode tabs.
loom/webui/frontend/src/components/Sidebar.tsx Adds group-aware sidebar + new “Config” navigation entry.
loom/webui/frontend/src/lib/types.ts Extends frontend types for config + groups.
loom/webui/frontend/src/lib/config.ts Adds frontend client helpers for config endpoints + restart.
loom/webui/frontend/src/lib/api.ts Extends envelopes query to support group filter; adds group/policy helpers.
loom/webui/frontend/src/pages/SettingsPage.tsx Adds “Config” view routing in settings page.
loom/webui/frontend/src/App.tsx Adds groupFilter and passes it through to envelopes query + sidebar.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread loom/api_server.py
Comment on lines +539 to +558
# Persist to disk first so load_config() reads the new content
path = _config_path()
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content)

old_config = ctx.config
new_config = load_config()
changed = diff_config(old_config, new_config)

# Hot-swap ctx.config so subsequent envelopes use new groups, etc.
ctx.config = new_config

restart_required = [k for k in changed if k in RESTART_REQUIRED_FIELDS]
return {
"saved": True,
"changed": changed,
"restart_required": restart_required,
}


Comment thread loom/api_server.py
Comment on lines +587 to +603
relauncher = (
f"while [ -f {pid_path} ]; do sleep 0.2; done; exec {sys.executable} -m loom.daemon"
)
log_file = open(log_path, "a")
subprocess.Popen(
["sh", "-c", relauncher],
stdout=log_file,
stderr=log_file,
stdin=subprocess.DEVNULL,
start_new_session=True,
env={**os.environ},
)

# Trigger graceful shutdown of the current process shortly after responding.
loop = asyncio.get_event_loop()
loop.call_later(0.3, lambda: os.kill(os.getpid(), signal.SIGTERM))
return {"restarting": True}
Comment thread loom/api_server.py
Comment on lines 229 to 233
"unread": 0,
**({"prompt": gcfg.prompt, "model": gcfg.model} if gcfg else {}),
**({"policy": policy} if policy else {}),
}
groups[g]["sources"].append({k: v for k, v in src.items() if k != "group"})
groups[g]["unread"] += counts.get(src.get("kind", ""), 0)
Comment on lines +275 to +279
update({ ...config, groups: { ...rest, [newName]: g } })
}}
onRemove={() => {
const { [name]: _removed, ...rest } = config.groups
update({ ...config, groups: rest })
Comment on lines +148 to +170
def load_action_by_name(self, name: str, policy_dir: Path) -> PolicyAction | None:
"""Load a flat policy file (action fields only, no rules wrapper) by name."""
path = policy_dir / f"{name}.yaml"
if not path.exists():
return None
with open(path) as f:
data = yaml.safe_load(f)
if not data or not isinstance(data, dict):
return None
return PolicyAction(
priority=data.get("priority", 1),
agent=data.get("agent", ""),
prompt=data.get("prompt", ""),
auto_approve=data.get("auto_approve", False),
batch=data.get("batch", False),
batch_window=data.get("batch_window", ""),
tools=data.get("tools", []),
max_turns=data.get("max_turns"),
system_prompt=data.get("system_prompt", ""),
model=data.get("model", ""),
skills=data.get("skills", []),
cwd=data.get("cwd", ""),
)
Comment on lines +50 to +55
export const savePolicy = (name: string, content: string) =>
jsonFetch<{ saved: boolean }>(`/api/settings/policies/${encodeURIComponent(name)}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content }),
})
Comment on lines +123 to +130
{g.sources.map((s) => (
<SidebarRow
key={`${g.name}/${s.kind}`}
label={s.kind}
active={view === "inbox" && sourceFilter === s.kind}
onClick={() => onSourceFilter(s.kind)}
/>
))}
Comment on lines +50 to +51
prompt?: string
model?: string
Comment on lines +49 to +55
const obj = parsed as Record<string, unknown>
return {
daemon: (obj.daemon as ConfigShape["daemon"]) ?? {},
agent: (obj.agent as ConfigShape["agent"]) ?? {},
paths: (obj.paths as ConfigShape["paths"]) ?? {},
sources: (obj.sources as ConfigShape["sources"]) ?? [],
groups: Object.fromEntries(
Comment thread loom/config.py
Comment on lines +101 to +105
groups = {
name: str(policy)
for name, policy in raw.get("groups", {}).items()
if isinstance(policy, str)
}
@jimpablo jimpablo deleted the feat/sidebar-groups-config branch May 2, 2026 21:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants