Skip to content

feat: Modernize webui#1016

Merged
Henry-811 merged 9 commits intodev/v0.1.67from
modernize_webui
Mar 23, 2026
Merged

feat: Modernize webui#1016
Henry-811 merged 9 commits intodev/v0.1.67from
modernize_webui

Conversation

@ncrispino
Copy link
Collaborator

@ncrispino ncrispino commented Mar 23, 2026

PR Title Format

Your PR title must follow the format: <type>: <brief description>

Valid types:

  • fix: - Bug fixes
  • feat: - New features
  • breaking: - Breaking changes
  • docs: - Documentation updates
  • refactor: - Code refactoring
  • test: - Test additions/modifications
  • chore: - Maintenance tasks
  • perf: - Performance improvements
  • style: - Code style changes
  • ci: - CI/CD configuration changes

Examples:

  • fix: resolve memory leak in data processing
  • feat: add export to CSV functionality
  • breaking: change API response format
  • docs: update installation guide

Description

Brief description of the changes in this PR

Type of change

  • Bug fix (fix:) - Non-breaking change which fixes an issue
  • New feature (feat:) - Non-breaking change which adds functionality
  • Breaking change (breaking:) - Fix or feature that would cause existing functionality to not work as expected
  • Documentation (docs:) - Documentation updates
  • Code refactoring (refactor:) - Code changes that neither fix a bug nor add a feature
  • Tests (test:) - Adding missing tests or correcting existing tests
  • Chore (chore:) - Maintenance tasks, dependency updates, etc.
  • Performance improvement (perf:) - Code changes that improve performance
  • Code style (style:) - Changes that do not affect the meaning of the code (formatting, missing semi-colons, etc.)
  • CI/CD (ci:) - Changes to CI/CD configuration files and scripts

Checklist

  • I have run pre-commit on my changed files and all checks pass
  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Pre-commit status

# Paste the output of running pre-commit on your changed files:
# uv run pre-commit install
# git diff --name-only HEAD~1 | xargs uv run pre-commit run --files # for last commit
# git diff --name-only origin/<base branch>...HEAD | xargs uv run pre-commit run --files # for all commits in PR
# git add <your file> # if any fixes were applied
# git commit -m "chore: apply pre-commit fixes"
# git push origin <branch-name>

How to Test

Add test method for this PR.

Test CLI Command

Write down the test bash command. If there is pre-requests, please emphasize.

Expected Results

Description/screenshots of expected results.

Additional context

Add any other context about the PR here.

Summary by CodeRabbit

New Features

  • Step mode execution for controlled single-action agent runs
  • Optional prompt improver pre-collaboration capability
  • Evaluator personas tool for agent diversity
  • Session replay for historical sessions
  • WebUI state persistence and expanded API endpoints

Bug Fixes

  • Improved MCP tool result display and normalization
  • Better workspace management with automatic agent ID anonymization

Documentation

  • Added step mode specification guide
  • Updated coordination parameter extension rules
  • Enhanced configuration examples

@coderabbitai
Copy link

coderabbitai bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

The pull request adds prompt improvement and evaluator persona coordination features, introduces step mode documentation, implements workspace path rewriting and agent ID scrubbing utilities, and enhances the web and terminal UIs with parallel pre-collaboration phase orchestration, improved prompt display, historical session replay, and mode override propagation.

Changes

Cohort / File(s) Summary
Documentation
AGENTS.md, CLAUDE.md, docs/modules/step_mode.md
Added guidelines for extending YAML coordination parameters and comprehensive step mode specification covering session state persistence, per-agent progression, consensus logic, and output data flow.
Configuration & Agent Setup
massgen/agent_config.py, massgen/configs/features/round_evaluator_example.yaml
Introduced PromptImproverConfig and enable_evaluator_personas fields to CoordinationConfig; expanded example configuration with additional evaluation criteria selection options, execution trace analyzer, and extended subagent pool.
CLI & Backend
massgen/cli.py, massgen/backend/codex.py
Added parsing/wiring of prompt_improver and enable_evaluator_personas configurations, step mode finalization hook, updated status file paths; introduced _stringify_mcp_result() helper and checklist specs path persistence in backend.
Core Coordination
massgen/coordination_tracker.py, massgen/evaluation_criteria_generator.py, massgen/events.py
Added regenerate_path_tokens() method to refresh anonymous agent tokens per round; refactored criteria discovery to delegate to shared precollab_utils helpers; added PRE_COLLAB_BATCH_ANNOUNCED event type constant.
Filesystem Management
massgen/filesystem_manager/__init__.py, massgen/filesystem_manager/_filesystem_manager.py, massgen/filesystem_manager/_path_rewriter.py
Exported path rewriting utilities; enhanced snapshot copying with agent ID scrubbing and framework metadata exclusion; added new replace_stale_paths_in_workspace() and scrub_agent_ids_in_snapshot() functions for workspace state synchronization and anonymization during copying.
Terminal UI
massgen/frontend/displays/textual_terminal_display.py, massgen/frontend/displays/textual_widgets/subagent_screen.py, massgen/frontend/displays/textual_widgets/tab_bar.py
Implemented unified parallel pre-collab screen orchestration with event-driven batch announcement; added notify_prompt_improved() callback; enhanced subagent headers/panels with multi-tab support, agent persona extraction, and namespaced widget IDs; extended session info modal to display original and improved prompts.
Web Display & Server
massgen/frontend/displays/web_display.py, massgen/frontend/web/server.py
Added event history tracking and structured event forwarding to WebSocket; implemented subagent spawn notifications and phase change events; added 1100+ lines for historical session replay, per-agent workspace resolution, mode override propagation, broadcast response handling, and new API endpoints for config/state/events management.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • feat: Step mode #1011: Implements the same step mode feature with CLI, orchestrator, and session-dir I/O handling.
  • feat: v0.1.61 #985: Modifies coordination configuration and subagent coordination features including round_evaluator setup in agent_config.py and CLI parsing.
  • feat: Improve quality rounds #969: Adjusts coordination_tracker.py token handling and CoordinationConfig fields alongside this PR's coordination enhancements.

Suggested reviewers

  • a5507203
🚥 Pre-merge checks | ✅ 3 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is entirely a template copy-paste with no actual content filled in; it lacks a concrete summary of changes, specific type-of-change checkboxes marked, any testing information, or implementation details. Replace the template text with a concrete description of the actual changes made, mark relevant checkboxes, document test procedures, and provide additional context about the web UI modernization work.
Documentation Updated ⚠️ Warning Critical documentation gaps exist: yaml_schema.rst lacks entries for orchestrator.coordination.prompt_improver and enable_evaluator_personas; no user guide updates document these features; design documentation missing from dev_notes. Update yaml_schema.rst with new coordination parameters, create user guide page for prompt improvement/evaluator personas features, add design document to dev_notes, and address review-flagged validation and serialization issues.
Config Parameter Sync ⚠️ Warning PR adds two new coordination YAML parameters to CoordinationConfig but fails to add them to the exclusion methods in base.py and _api_params_handler_base.py, violating documented synchronization requirements. Add prompt_improver and enable_evaluator_personas to get_base_excluded_config_params() methods in both base.py and _api_params_handler_base.py within the Coordination parameters section.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Modernize webui' directly aligns with the changeset's primary focus on modernizing the web UI infrastructure, including comprehensive WebDisplay updates, web server enhancements, and frontend display refactoring.
Docstring Coverage ✅ Passed Docstring coverage is 90.08% which is sufficient. The required threshold is 80.00%.
Capabilities Registry Check ✅ Passed The pull request does not introduce any new backend providers or models. Changes focus on coordination configuration, MCP result handling, web UI, filesystem utilities, and display enhancements only.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch modernize_webui

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
massgen/filesystem_manager/_filesystem_manager.py (1)

2342-2375: ⚠️ Potential issue | 🟠 Major

Add .claude to _WORKSPACE_METADATA_DIRS too.

This block now classifies .claude as framework metadata when copying temp snapshots, but has_meaningful_content() still treats .claude/ as deliverable content. A workspace containing only .claude/ can therefore overwrite/favor an effectively empty snapshot instead of falling back to snapshot_storage.

💡 Minimal fix
-_WORKSPACE_METADATA_DIRS = frozenset({".git", ".codex", ".gemini", ".massgen", "memory"})
+_WORKSPACE_METADATA_DIRS = frozenset({".git", ".codex", ".gemini", ".claude", ".massgen", "memory"})

As per coding guidelines, Backends that create config directories in the workspace (e.g., .codex/ for Codex) must add those directory names to the _metadata_dirs set in FilesystemManager.save_snapshot().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@massgen/filesystem_manager/_filesystem_manager.py` around lines 2342 - 2375,
The snapshot-copy logic already excludes ".claude" via _snapshot_exclude_dirs
but has_meaningful_content() still treats ".claude/" as deliverable content;
update the workspace metadata set used when deciding meaningful snapshots by
adding ".claude" to _WORKSPACE_METADATA_DIRS (the set referenced in
FilesystemManager.save_snapshot() / has_meaningful_content()) so that snapshots
containing only ".claude/" are considered metadata-only and will fall back to
snapshot_storage; locate the _WORKSPACE_METADATA_DIRS (or equivalent
_metadata_dirs) definition and append "'.claude'" to it and ensure any tests
expecting metadata behavior are updated accordingly.
🧹 Nitpick comments (3)
massgen/frontend/displays/web_display.py (1)

124-151: Consider adding debug-level logging for swallowed exceptions.

While the defensive try-except-pass pattern is intentional here (EventEmitter integration is optional and failures shouldn't crash the display), completely silent exception handling can make debugging difficult when things go wrong.

♻️ Proposed improvement: Add debug logging
     def _register_event_listener(self) -> None:
         """Register as a listener on the EventEmitter to forward structured events."""
         try:
             from massgen.logger_config import get_event_emitter
 
             emitter = get_event_emitter()
             if emitter:
                 self._event_listener = self._on_structured_event
                 emitter.add_listener(self._event_listener)
-        except Exception:
-            pass
+        except Exception as e:
+            import logging
+            logging.getLogger(__name__).debug(
+                "WebDisplay: EventEmitter registration skipped: %s", e
+            )
 
     def _on_structured_event(self, event: Any) -> None:
         """Forward a structured MassGenEvent over WebSocket."""
         if self._closed:
             return
         try:
             self._emit(
                 "structured_event",
                 {
                     "event_type": event.event_type,
                     "agent_id": event.agent_id,
                     "round_number": event.round_number,
                     "data": event.data,
                 },
             )
-        except Exception:
-            pass
+        except Exception as e:
+            import logging
+            logging.getLogger(__name__).debug(
+                "WebDisplay: Failed to forward structured event: %s", e
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@massgen/frontend/displays/web_display.py` around lines 124 - 151, Replace the
silent "except Exception: pass" blocks in _register_event_listener and
_on_structured_event with debug-level logging so swallowed exceptions are
recorded for troubleshooting: catch the exception as e (e.g., "except Exception
as e"), import or use the module logger (or massgen.logger_config's logger) and
call logger.debug with a short message that includes the function name and
exception details (for example reference _register_event_listener,
_event_listener, get_event_emitter, and in _on_structured_event reference _emit
and _closed) so failures remain non-fatal but are visible in debug logs.
massgen/frontend/displays/textual_terminal_display.py (1)

6454-6565: Add Google-style docstrings to the new pre-collab helpers.

This block introduces the core orchestration helpers for the unified pre-collab flow, but their docstrings still omit Args: / side-effect details. Please align them with the repository docstring rule while these APIs are still fresh.

As per coding guidelines "For new or changed functions, include Google-style docstrings".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@massgen/frontend/displays/textual_terminal_display.py` around lines 6454 -
6565, Update the four new helper methods (_try_open_unified_precollab_screen,
_open_unified_precollab_screen, _wait_and_open_unified_screen,
set_precollab_batch) to include Google-style docstrings that document Args
(including types and defaults for attempt and subagents/pre_collab_ids), the
return value (None), and important side effects (mutating
self._parallel_precollab_expected, self._parallel_precollab_screen_opened,
timers via set_timer, pushing screens via push_screen, and callbacks used like
_subagent_message_callback/_subagent_continue_callback); also note any
timeouts/retry behavior and conditions under which the method exits early (e.g.,
when screen already opened or no subagents) so callers and maintainers have
clear behavioral contracts.
massgen/frontend/web/server.py (1)

120-219: Use Google-style docstrings for the new replay helpers.

These new/changed functions still use summary-only docstrings, so they miss the required Args: / Returns: sections for repo Python code.

As per coding guidelines, **/*.py: For new or changed functions, include Google-style docstrings.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@massgen/frontend/web/server.py` around lines 120 - 219, The three helper
functions (_find_latest_attempt, _scan_log_dirs, _scan_log_dirs_cached)
currently have summary-only docstrings; update each to Google-style docstrings
including an "Args:" section describing parameters (e.g., log_dir: Path or
logs_root: Path) and a "Returns:" section describing the return type and meaning
(e.g., Path | None for _find_latest_attempt, list[dict[str, Any]] for the
scanners), and where helpful a short "Raises:" note if exceptions are expected;
keep the existing summary and examples of keys (session_id, question, config,
etc.) in the _scan_log_dirs Returns description to match repo docstring style.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@massgen/agent_config.py`:
- Around line 220-222: AgentConfig currently drops prompt_improver and
enable_evaluator_personas when serializing and rehydrates prompt_improver as a
plain dict; update AgentConfig.to_dict to include the prompt_improver field
(serialize it properly, e.g., call its to_dict or similar) and include
enable_evaluator_personas in the returned dict, and update AgentConfig.from_dict
to detect if prompt_improver is a dict and instantiate a PromptImproverConfig
from it (or accept an existing PromptImproverConfig), ensuring
enable_evaluator_personas is read back into the AgentConfig instance so
saved/resumed configs preserve these values; reference the prompt_improver
attribute and the AgentConfig.to_dict / AgentConfig.from_dict methods while
making the changes.

In `@massgen/cli.py`:
- Around line 10078-10086: The ACTION and STATUS machine-readable lines are
printed after calling orchestrator.finalize_step_mode, which can delay or fail
the handoff; move the _automation_print calls (using action_data['action'] and
the STATUS path built from args.session_dir and real_agent_id) to occur before
orchestrator.finalize_step_mode(log_session_dir) so the step result is emitted
immediately (ensure you still call finalize_step_mode afterwards to save extra
artifacts).
- Around line 11743-11750: The code appends "/?v=2" to browser_url which breaks
existing query params (variable browser_url in massgen/cli.py); instead parse
browser_url with urllib.parse (urlparse/parse_qs), update the query dict to set
"v"="2" and add "setup"="open" or "wizard"="open" when args.setup or
args.quickstart are set, then rebuild the URL with urlencode and urlunparse so
existing params (e.g., prompt, config) are preserved; modify the block that
checks getattr(args, "setup"/"quickstart") to perform this parse-modify-rebuild
flow rather than string concatenation.
- Around line 3507-3514: coord_cfg may contain non-mapping values for
"prompt_improver" which makes pi_cfg not support .get and crashes; update the
parsing logic that builds prompt_improver_config to validate that
coord_cfg["prompt_improver"] is a mapping (e.g., isinstance(pi_cfg, dict) or
collections.abc.Mapping) before calling pi_cfg.get(...), and if it is not a
mapping raise a clear configuration error (ValueError or custom) that surfaces
the bad config; keep using PromptImproverConfig(enabled=...,
persist_across_turns=...) when the value is valid and fall back to the default
PromptImproverConfig() otherwise.

In `@massgen/evaluation_criteria_generator.py`:
- Around line 1052-1062: The current blanket except in the block that reads
criteria_file and calls _parse_criteria_response hides unexpected errors; change
criteria_file.read_text() to explicitly read as UTF-8
(read_text(encoding="utf-8")) and replace the broad "except Exception" with
catching only expected errors (e.g., UnicodeDecodeError, json.JSONDecodeError
from the parser, and file-related errors like FileNotFoundError/PermissionError)
so those are suppressed/logged as before, but re-raise any other exceptions so
they surface to the caller; keep references to criteria_file,
_parse_criteria_response, and logger.debug to locate and update the code.

In `@massgen/frontend/displays/textual_terminal_display.py`:
- Around line 4969-4972: The handler for event.event_type ==
"pre_collab_batch_announced" is clobbering the pending/open guard by always
resetting _parallel_precollab_screen_opened = False; change it to preserve an
already-pending/opened state by only setting _parallel_precollab_screen_opened
to False when the attribute doesn't already indicate a pending/open screen
(e.g., if not getattr(self, "_parallel_precollab_screen_opened", False):
self._parallel_precollab_screen_opened = False) or better: avoid assigning at
all when self._parallel_precollab_screen_opened is True; apply the same
conditional-preservation fix to the other two handlers referenced (the blocks
updating _parallel_precollab_expected/_parallel_precollab_screen_opened at the
other occurrences).
- Around line 1226-1230: notify_prompt_improved currently only updates the live
UI via _call_app_method("update_improved_prompt", ...) so replayed sessions
never get the improved prompt; modify notify_prompt_improved to also persist the
change to the replayable event/state stream (e.g., emit an
"improved_prompt_updated" event with the improved_prompt payload to whatever
event feeder or state appender is used elsewhere), and update the replay/restore
logic that rebuilds viewer state (the handler that consumes events during
replay) to handle that event and populate the session-info modal's
improved_prompt field during rebuild; ensure the same persisted event is
produced in the other call sites mentioned (around the blocks at/near 4173-4179
and 7924-7927) so both live UI and replay restore receive the improved prompt.

In `@massgen/frontend/displays/textual_widgets/subagent_screen.py`:
- Around line 121-125: The title builder incorrectly uses self._multi (tab
count) to decide the "Preparation" label; change _build_title to only return
"Preparation" when an explicit preparation/pre-collab signal is present (e.g.,
check a dedicated flag like self._subagent.is_preparation or
self._is_preparation_mode) and otherwise compute label from
getattr(self._subagent, "subagent_type", None) or self._subagent.id; also update
where _multi is set/used so tab count does not drive the preparation title.
- Around line 1647-1663: The current patch rebuilds a SubagentDisplayData
instance but only copies a subset of fields, dropping fields like context_paths
and context_paths_labeled; fix by constructing the new SubagentDisplayData from
the existing new_data while only overriding subagent_type (e.g., use a dataclass
replace/constructor that copies all existing fields from new_data and sets
subagent_type=sa.subagent_type) so new_data, sa and SubagentDisplayData retain
all present fields.
- Around line 2619-2634: The code is currently prepending the entire system
prompt from _inner_agent_system_prompts (via persona variable) into the content,
which leaks unrelated hidden instructions; instead, parse persona to extract
only the evaluator persona text (the substring after "Evaluator Persona:" up to
the next newline or end) and prepend that extracted evaluator persona (not the
full system_prompt) to content; keep the existing title logic that reads
"Evaluator Persona:" to derive persona_label, and use the same parsing logic in
the content-building block that currently adds persona so only the evaluator
persona snippet is inserted when present.

In `@massgen/frontend/web/server.py`:
- Around line 693-704: The code registers the transient WebSocket session_id
into SessionRegistry causing duplicate entries because _save_session_metadata()
later uses session_dir.name as the canonical ID; update the register flow so it
records the same canonical ID used by _save_session_metadata (use
session_dir.name or a canonical_session_id variable) instead of the transient
session_id when calling SessionRegistry.register_session, keep the same
config_path, description (question[:100] if present) and
status="completed"/source="webui", and ensure any subsequent references use that
canonical ID consistently.
- Around line 2364-2418: The delete_session handler currently only removes
completed_sessions, displays, tasks, orchestrators and some session maps; also
remove any live connection and workspace indexes and the historical log-dir
reference so deleted sessions don't reappear: in delete_session, check and
remove session_id from manager.active_connections (and close/terminate any
connection object if applicable), remove session_id entries from
workspace_manager.workspace_connections, and ensure
manager.session_log_dirs[session_id] is deleted (it may already be present in
code but ensure it's removed before returning); perform these removals before
calling SessionRegistry().delete_session and keep the same warning/JSONResponse
behavior.
- Around line 907-913: The endpoint currently reads any filesystem path via
config_path = Path(path) and safe_loads it, widening file disclosure; restrict
reads by reapplying the same config-path allowlist logic used in
/api/config/content: resolve config_path (config_path.resolve()), check it is
inside the approved config directories or matches allowed filenames (compare
against the allowlist set or ensure any(allowed.resolve() in
config_path.resolve().parents or config_path.resolve()==allowed.resolve() for
allowed in ALLOWED_CONFIG_PATHS)), and return JSONResponse 404 if the check
fails; keep the existing YAML loading (raw = _yaml.safe_load(...)) only after
the allowlist/containment check.
- Around line 1769-1789: The current code dumps the post-processed config into
original_yaml which leaks env-expanded values; instead call load_config_file to
capture the raw representation and the parsed config (e.g. raw, config =
load_config_file(...)), use yaml.dump on the raw value for original_yaml (or
re-read resolved_path as raw text), then call _apply_mode_overrides(config,
mode_overrides) and yaml.dump that processed config into resolved_yaml while
keeping the parsed config object returned for further use.
- Around line 5791-5799: The plan_mode branch currently only sets flags and can
leave stale booleans on coord (created via orch.setdefault("coordination", {}));
update the logic to first reset all planning-related flags
(enable_agent_task_planning, task_planning_filesystem_mode, spec_mode,
analysis_mode) to False on coord before applying the new overrides["plan_mode"]
values so selecting "normal" clears prior modes and switching modes won't leave
stale flags; modify the block that reads overrides.get("plan_mode") to
explicitly assign False to each of those keys on coord then set the appropriate
ones to True based on overrides["plan_mode"].
- Around line 917-930: The agent listing only reads model from
backend.get("model") so configs that store the model under backend_params.model
are returned blank; update the loop that builds agents_result (the
agents_list/agent_cfg/backend block) to compute model by checking
backend.get("model") first and falling back to backend.get("backend_params", {})
.get("model") (or vice‑versa if preferred), then use that computed model value
for the "model" field while keeping provider from backend.get("type"). Ensure
backend is still normalized to a dict as currently done.
- Around line 2351-2356: The merged session sort fails when comparing start_time
(string) and completed_at (float); update the _sort_key used by sessions.sort to
normalize timestamps to a numeric value before comparison: in the _sort_key
function (used when sorting sessions), coerce s.get("start_time") and
s.get("completed_at") into a float (e.g., parse start_time if it's a string,
fallback to completed_at, and return 0.0 on missing/invalid values) so the sort
always compares floats and cannot raise TypeError.

---

Outside diff comments:
In `@massgen/filesystem_manager/_filesystem_manager.py`:
- Around line 2342-2375: The snapshot-copy logic already excludes ".claude" via
_snapshot_exclude_dirs but has_meaningful_content() still treats ".claude/" as
deliverable content; update the workspace metadata set used when deciding
meaningful snapshots by adding ".claude" to _WORKSPACE_METADATA_DIRS (the set
referenced in FilesystemManager.save_snapshot() / has_meaningful_content()) so
that snapshots containing only ".claude/" are considered metadata-only and will
fall back to snapshot_storage; locate the _WORKSPACE_METADATA_DIRS (or
equivalent _metadata_dirs) definition and append "'.claude'" to it and ensure
any tests expecting metadata behavior are updated accordingly.

---

Nitpick comments:
In `@massgen/frontend/displays/textual_terminal_display.py`:
- Around line 6454-6565: Update the four new helper methods
(_try_open_unified_precollab_screen, _open_unified_precollab_screen,
_wait_and_open_unified_screen, set_precollab_batch) to include Google-style
docstrings that document Args (including types and defaults for attempt and
subagents/pre_collab_ids), the return value (None), and important side effects
(mutating self._parallel_precollab_expected,
self._parallel_precollab_screen_opened, timers via set_timer, pushing screens
via push_screen, and callbacks used like
_subagent_message_callback/_subagent_continue_callback); also note any
timeouts/retry behavior and conditions under which the method exits early (e.g.,
when screen already opened or no subagents) so callers and maintainers have
clear behavioral contracts.

In `@massgen/frontend/displays/web_display.py`:
- Around line 124-151: Replace the silent "except Exception: pass" blocks in
_register_event_listener and _on_structured_event with debug-level logging so
swallowed exceptions are recorded for troubleshooting: catch the exception as e
(e.g., "except Exception as e"), import or use the module logger (or
massgen.logger_config's logger) and call logger.debug with a short message that
includes the function name and exception details (for example reference
_register_event_listener, _event_listener, get_event_emitter, and in
_on_structured_event reference _emit and _closed) so failures remain non-fatal
but are visible in debug logs.

In `@massgen/frontend/web/server.py`:
- Around line 120-219: The three helper functions (_find_latest_attempt,
_scan_log_dirs, _scan_log_dirs_cached) currently have summary-only docstrings;
update each to Google-style docstrings including an "Args:" section describing
parameters (e.g., log_dir: Path or logs_root: Path) and a "Returns:" section
describing the return type and meaning (e.g., Path | None for
_find_latest_attempt, list[dict[str, Any]] for the scanners), and where helpful
a short "Raises:" note if exceptions are expected; keep the existing summary and
examples of keys (session_id, question, config, etc.) in the _scan_log_dirs
Returns description to match repo docstring style.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5b0e30fd-25df-420f-9c9c-7e84bf9b5891

📥 Commits

Reviewing files that changed from the base of the PR and between e12fb70 and a17fde3.

⛔ Files ignored due to path filters (109)
  • massgen/frontend/web/static/assets/_baseUniq-DHPGDDgx.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/_baseUniq-DHPGDDgx.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/arc-CokCsZM6.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/arc-CokCsZM6.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/architectureDiagram-VXUJARFQ-CFWMw_rK.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/architectureDiagram-VXUJARFQ-CFWMw_rK.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/base-80a1f760-0bJi2RFl.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/base-80a1f760-0bJi2RFl.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/blockDiagram-VD42YOAC-Co5-MpGL.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/blockDiagram-VD42YOAC-Co5-MpGL.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/c4Diagram-YG6GDRKO-DYPuNmEF.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/c4Diagram-YG6GDRKO-DYPuNmEF.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/channel-D1dpymym.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/channel-D1dpymym.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/channel-DTOQwt0x.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/chunk-4BX2VUAB-CebFxQ-R.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/chunk-4BX2VUAB-CebFxQ-R.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/chunk-55IACEB6-DiNHxzzY.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/chunk-55IACEB6-DiNHxzzY.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/chunk-B4BG7PRW-CoPQlgSP.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/chunk-B4BG7PRW-CoPQlgSP.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/chunk-DI55MBZ5-BtSW5YBQ.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/chunk-DI55MBZ5-BtSW5YBQ.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/chunk-FMBD7UC4-wkPVgmHa.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/chunk-FMBD7UC4-wkPVgmHa.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/chunk-QN33PNHL-CZNl_8Ek.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/chunk-QN33PNHL-CZNl_8Ek.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/chunk-QZHKN3VN-BwbdsYhL.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/chunk-QZHKN3VN-BwbdsYhL.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/chunk-TZMSLE5B-CxyG-zuc.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/chunk-TZMSLE5B-CxyG-zuc.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/classDiagram-2ON5EDUG--rHFO9II.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/classDiagram-2ON5EDUG-CqUIDWJ9.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/classDiagram-2ON5EDUG-CqUIDWJ9.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/classDiagram-v2-WZHVMYZB--rHFO9II.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/classDiagram-v2-WZHVMYZB-CqUIDWJ9.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/classDiagram-v2-WZHVMYZB-CqUIDWJ9.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/clone-Cs1GzyJB.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/clone-r7v9nELZ.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/clone-r7v9nELZ.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/consoleHook-59e792cb-CJy5coaP.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/consoleHook-59e792cb-CJy5coaP.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/cose-bilkent-S5V4N54A-TTlzxN-U.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/cose-bilkent-S5V4N54A-TTlzxN-U.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/dagre-6UL2VRFP-BxPbgi2q.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/dagre-6UL2VRFP-BxPbgi2q.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/diagram-PSM6KHXK-d1E9v6HU.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/diagram-PSM6KHXK-d1E9v6HU.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/diagram-QEK2KX5R-BrdW4vHb.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/diagram-QEK2KX5R-BrdW4vHb.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/diagram-S2PKOQOG-Dl5n2kz6.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/diagram-S2PKOQOG-Dl5n2kz6.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/erDiagram-Q2GNP2WA-DoTyzorr.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/erDiagram-Q2GNP2WA-DoTyzorr.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/flowDiagram-NV44I4VS-BaelXQjr.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/flowDiagram-NV44I4VS-BaelXQjr.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/ganttDiagram-JELNMOA3-BfHDONdS.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/ganttDiagram-JELNMOA3-BfHDONdS.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/gitGraphDiagram-NY62KEGX--ygv52PS.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/gitGraphDiagram-NY62KEGX--ygv52PS.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/graph-BB7zubDf.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/graph-BB7zubDf.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/index-599aeaf7-Beoqm9RB.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/index-599aeaf7-Beoqm9RB.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/index-BXbvd5or.css is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/index-C2qniOIu.css is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/index-CGMkk2HX.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/index-CGMkk2HX.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/index-CoEp30O1.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/index-CoEp30O1.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/index-DBXMMTf0.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/index-DBXMMTf0.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/index-JoaRpYIu.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/infoDiagram-WHAUD3N6-C29jQaQb.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/infoDiagram-WHAUD3N6-C29jQaQb.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/journeyDiagram-XKPGCS4Q-Bm72xyLG.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/journeyDiagram-XKPGCS4Q-Bm72xyLG.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/kanban-definition-3W4ZIXB7-CTKr6jqq.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/kanban-definition-3W4ZIXB7-CTKr6jqq.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/layout-bA2TPHVZ.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/layout-bA2TPHVZ.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/linear-97fYS-uu.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/linear-97fYS-uu.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/min-De6IWHqk.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/min-De6IWHqk.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/mindmap-definition-VGOIOE7T-DJMLKmyE.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/mindmap-definition-VGOIOE7T-DJMLKmyE.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/pieDiagram-ADFJNKIX-C6vPU95a.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/pieDiagram-ADFJNKIX-C6vPU95a.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/quadrantDiagram-AYHSOK5B-BPAyAJBT.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/quadrantDiagram-AYHSOK5B-BPAyAJBT.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/requirementDiagram-UZGBJVZJ-CQUDmrBe.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/requirementDiagram-UZGBJVZJ-CQUDmrBe.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/sankeyDiagram-TZEHDZUN-Bypvbwc1.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/sankeyDiagram-TZEHDZUN-Bypvbwc1.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/sequenceDiagram-WL72ISMW-CLtEGs2b.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/sequenceDiagram-WL72ISMW-CLtEGs2b.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/stateDiagram-FKZM4ZOC-Cwn7jp1i.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/stateDiagram-FKZM4ZOC-Cwn7jp1i.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/stateDiagram-v2-4FDKWEC3-BTZICaqL.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/stateDiagram-v2-4FDKWEC3-BhsNILxt.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/stateDiagram-v2-4FDKWEC3-BhsNILxt.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/timeline-definition-IT6M3QCI-Cuuo442X.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/timeline-definition-IT6M3QCI-Cuuo442X.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/treemap-KMMF4GRG-CvlYEM91.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/treemap-KMMF4GRG-CvlYEM91.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • massgen/frontend/web/static/assets/xychartDiagram-PRI3JC2R-g0uoLKrB.js is excluded by !massgen/frontend/web/static/assets/**
  • massgen/frontend/web/static/assets/xychartDiagram-PRI3JC2R-g0uoLKrB.js.map is excluded by !**/*.map, !massgen/frontend/web/static/assets/**, !**/*.map
  • webui/package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
📒 Files selected for processing (145)
  • AGENTS.md
  • CLAUDE.md
  • docs/modules/step_mode.md
  • massgen/agent_config.py
  • massgen/backend/codex.py
  • massgen/cli.py
  • massgen/configs/features/round_evaluator_example.yaml
  • massgen/coordination_tracker.py
  • massgen/evaluation_criteria_generator.py
  • massgen/events.py
  • massgen/filesystem_manager/__init__.py
  • massgen/filesystem_manager/_filesystem_manager.py
  • massgen/filesystem_manager/_path_rewriter.py
  • massgen/frontend/displays/textual_terminal_display.py
  • massgen/frontend/displays/textual_widgets/subagent_screen.py
  • massgen/frontend/displays/textual_widgets/tab_bar.py
  • massgen/frontend/displays/web_display.py
  • massgen/frontend/web/server.py
  • massgen/frontend/web/static/index.html
  • massgen/mcp_tools/checklist_tools_server.py
  • massgen/message_templates.py
  • massgen/orchestrator.py
  • massgen/persona_generator.py
  • massgen/precollab_utils.py
  • massgen/prompt_improver.py
  • massgen/step_mode.py
  • massgen/subagent/manager.py
  • massgen/subagent/models.py
  • massgen/subagent_types/regression_guard/SUBAGENT.md
  • massgen/subagent_types/round_evaluator/SUBAGENT.md
  • massgen/system_message_builder.py
  • massgen/system_prompt_sections.py
  • massgen/task_decomposer.py
  • massgen/tests/frontend/test_subagent_screen_input.py
  • massgen/tests/frontend/test_subagent_screen_wiring.py
  • massgen/tests/integration/test_orchestrator_hooks_broadcast_subagents.py
  • massgen/tests/test_answer_count_increment.py
  • massgen/tests/test_claude_code_context_sharing.py
  • massgen/tests/test_codex_mcp_tool_visibility.py
  • massgen/tests/test_decomposition_mode.py
  • massgen/tests/test_evaluator_personas.py
  • massgen/tests/test_path_rewriter.py
  • massgen/tests/test_precollab_utils.py
  • massgen/tests/test_prompt_improver.py
  • massgen/tests/test_round_evaluator_loop.py
  • massgen/tests/test_specialized_subagents.py
  • massgen/tests/test_step_mode.py
  • massgen/tests/test_step_mode_live.py
  • massgen/tests/test_subagent_docker_logs.py
  • massgen/tests/test_unified_precollab.py
  • massgen/tests/test_web_mode_overrides.py
  • massgen/tests/test_web_quickstart_cli.py
  • massgen/tests/test_web_setup_status_api.py
  • massgen/tests/test_web_workspace_connection.py
  • massgen/tests/test_write_mode_scratch.py
  • massgen/tests/unit/test_coordination_tracker.py
  • massgen/tests/unit/test_orchestrator_unit.py
  • webui/index.html
  • webui/package.json
  • webui/src/App.tsx
  • webui/src/components/InlineArtifactPreview.tsx
  • webui/src/components/setup/ApiKeysSection.tsx
  • webui/src/components/setup/DockerSection.tsx
  • webui/src/components/setup/SkillsSection.tsx
  • webui/src/components/v2/channel/AgentChannel.tsx
  • webui/src/components/v2/channel/FinalAnswerSection.tsx
  • webui/src/components/v2/channel/ModeBar.test.tsx
  • webui/src/components/v2/channel/ModeBar.tsx
  • webui/src/components/v2/channel/StreamingIndicator.tsx
  • webui/src/components/v2/channel/TaskPlanPanel.test.tsx
  • webui/src/components/v2/channel/TaskPlanPanel.tsx
  • webui/src/components/v2/channel/messages/AnswerMessageView.test.tsx
  • webui/src/components/v2/channel/messages/AnswerMessageView.tsx
  • webui/src/components/v2/channel/messages/BroadcastMessageView.tsx
  • webui/src/components/v2/channel/messages/CompletionMessageView.tsx
  • webui/src/components/v2/channel/messages/ContentMessageView.tsx
  • webui/src/components/v2/channel/messages/MessageRenderer.tsx
  • webui/src/components/v2/channel/messages/RoundDividerView.tsx
  • webui/src/components/v2/channel/messages/StatusMessageView.tsx
  • webui/src/components/v2/channel/messages/SubagentMessageView.tsx
  • webui/src/components/v2/channel/messages/ToolBatchView.test.tsx
  • webui/src/components/v2/channel/messages/ToolBatchView.tsx
  • webui/src/components/v2/channel/messages/ToolCallMessageView.tsx
  • webui/src/components/v2/channel/messages/VoteMessageView.test.tsx
  • webui/src/components/v2/channel/messages/VoteMessageView.tsx
  • webui/src/components/v2/layout/AgentMentionAutocomplete.tsx
  • webui/src/components/v2/layout/AppShell.test.tsx
  • webui/src/components/v2/layout/AppShell.tsx
  • webui/src/components/v2/layout/ConfigViewerModal.tsx
  • webui/src/components/v2/layout/FinalAnswerOverlay.tsx
  • webui/src/components/v2/layout/GlobalInputBar.test.tsx
  • webui/src/components/v2/layout/GlobalInputBar.tsx
  • webui/src/components/v2/layout/LaunchIndicator.test.tsx
  • webui/src/components/v2/layout/LaunchIndicator.tsx
  • webui/src/components/v2/layout/ModeConfigBar.test.tsx
  • webui/src/components/v2/layout/ModeConfigBar.tsx
  • webui/src/components/v2/layout/V2QuickstartWizard.test.tsx
  • webui/src/components/v2/layout/V2QuickstartWizard.tsx
  • webui/src/components/v2/layout/V2SetupOverlay.test.tsx
  • webui/src/components/v2/layout/V2SetupOverlay.tsx
  • webui/src/components/v2/sidebar/ChannelSection.tsx
  • webui/src/components/v2/sidebar/QuickAccessSection.tsx
  • webui/src/components/v2/sidebar/SessionSection.tsx
  • webui/src/components/v2/sidebar/Sidebar.tsx
  • webui/src/components/v2/sidebar/SidebarFooter.tsx
  • webui/src/components/v2/sidebar/SidebarHeader.tsx
  • webui/src/components/v2/sidebar/ThreadSection.tsx
  • webui/src/components/v2/tiles/EmptyState.tsx
  • webui/src/components/v2/tiles/FileViewerTile.tsx
  • webui/src/components/v2/tiles/OrientationToggle.tsx
  • webui/src/components/v2/tiles/PromptBanner.tsx
  • webui/src/components/v2/tiles/SubagentTile.tsx
  • webui/src/components/v2/tiles/TileContainer.test.tsx
  • webui/src/components/v2/tiles/TileContainer.tsx
  • webui/src/components/v2/tiles/TileDragContext.tsx
  • webui/src/components/v2/tiles/TileDragHandle.tsx
  • webui/src/components/v2/tiles/TileWrapper.tsx
  • webui/src/components/v2/tiles/TimelineTile.tsx
  • webui/src/components/v2/tiles/V2TimelineView.tsx
  • webui/src/components/v2/tiles/VoteResultsTile.tsx
  • webui/src/components/v2/tiles/WorkspaceBrowserTile.test.tsx
  • webui/src/components/v2/tiles/WorkspaceBrowserTile.tsx
  • webui/src/hooks/useV2KeyboardShortcuts.ts
  • webui/src/hooks/useWebSocket.test.tsx
  • webui/src/hooks/useWebSocket.ts
  • webui/src/hooks/useWorkspaceConnection.test.tsx
  • webui/src/hooks/useWorkspaceConnection.ts
  • webui/src/main.tsx
  • webui/src/pages/SetupPage.tsx
  • webui/src/stores/agentStore.ts
  • webui/src/stores/setupStore.ts
  • webui/src/stores/v2/messageStore.test.ts
  • webui/src/stores/v2/messageStore.ts
  • webui/src/stores/v2/modeStore.test.ts
  • webui/src/stores/v2/modeStore.ts
  • webui/src/stores/v2/tileStore.test.ts
  • webui/src/stores/v2/tileStore.ts
  • webui/src/stores/wizardStore.ts
  • webui/src/styles/globals.css
  • webui/src/test/setup.ts
  • webui/src/types/index.ts
  • webui/src/utils/broadcastTargets.test.ts
  • webui/src/utils/broadcastTargets.ts
  • webui/tailwind.config.js
  • webui/vite.config.ts

Comment on lines +220 to +222
prompt_improver: PromptImproverConfig = field(
default_factory=PromptImproverConfig,
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Round-tripping these new coordination fields is currently broken.

AgentConfig.to_dict() still drops both prompt_improver and enable_evaluator_personas, and AgentConfig.from_dict() would rehydrate prompt_improver as a plain dict if you start serializing it. Any saved/resumed config will silently lose these settings or later fail on attribute access like prompt_improver.enabled.

💡 Minimal fix
         result["coordination_config"] = {
             "enable_planning_mode": self.coordination_config.enable_planning_mode,
             "planning_mode_instruction": self.coordination_config.planning_mode_instruction,
             "max_orchestration_restarts": self.coordination_config.max_orchestration_restarts,
             "drift_conflict_policy": self.coordination_config.drift_conflict_policy,
             "round_evaluator_transformation_pressure": self.coordination_config.round_evaluator_transformation_pressure,
+            "prompt_improver": {
+                "enabled": self.coordination_config.prompt_improver.enabled,
+                "persist_across_turns": self.coordination_config.prompt_improver.persist_across_turns,
+            },
+            "enable_evaluator_personas": self.coordination_config.enable_evaluator_personas,
         }
...
         coordination_config = CoordinationConfig()
         coordination_data = data.get("coordination_config", {})
         if coordination_data:
+            coordination_data = coordination_data.copy()
+            prompt_improver_data = coordination_data.get("prompt_improver")
+            if isinstance(prompt_improver_data, dict):
+                coordination_data["prompt_improver"] = PromptImproverConfig(**prompt_improver_data)
             coordination_config = CoordinationConfig(**coordination_data)

Also applies to: 250-250

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@massgen/agent_config.py` around lines 220 - 222, AgentConfig currently drops
prompt_improver and enable_evaluator_personas when serializing and rehydrates
prompt_improver as a plain dict; update AgentConfig.to_dict to include the
prompt_improver field (serialize it properly, e.g., call its to_dict or similar)
and include enable_evaluator_personas in the returned dict, and update
AgentConfig.from_dict to detect if prompt_improver is a dict and instantiate a
PromptImproverConfig from it (or accept an existing PromptImproverConfig),
ensuring enable_evaluator_personas is read back into the AgentConfig instance so
saved/resumed configs preserve these values; reference the prompt_improver
attribute and the AgentConfig.to_dict / AgentConfig.from_dict methods while
making the changes.

Comment on lines +3507 to +3514
# Parse prompt_improver config if present
prompt_improver_config = PromptImproverConfig()
if "prompt_improver" in coord_cfg:
pi_cfg = coord_cfg["prompt_improver"]
prompt_improver_config = PromptImproverConfig(
enabled=pi_cfg.get("enabled", False),
persist_across_turns=pi_cfg.get("persist_across_turns", False),
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate prompt_improver as a mapping.

prompt_improver: with no nested keys loads as None in YAML, and prompt_improver: true loads as a bool. Both cases hit pi_cfg.get(...) and crash startup with AttributeError instead of surfacing a config error.

Suggested fix
     # Parse prompt_improver config if present
     prompt_improver_config = PromptImproverConfig()
-    if "prompt_improver" in coord_cfg:
-        pi_cfg = coord_cfg["prompt_improver"]
+    pi_cfg = coord_cfg.get("prompt_improver")
+    if pi_cfg is not None:
+        if not isinstance(pi_cfg, dict):
+            raise ConfigurationError(
+                "orchestrator.coordination.prompt_improver must be a mapping"
+            )
         prompt_improver_config = PromptImproverConfig(
             enabled=pi_cfg.get("enabled", False),
             persist_across_turns=pi_cfg.get("persist_across_turns", False),
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@massgen/cli.py` around lines 3507 - 3514, coord_cfg may contain non-mapping
values for "prompt_improver" which makes pi_cfg not support .get and crashes;
update the parsing logic that builds prompt_improver_config to validate that
coord_cfg["prompt_improver"] is a mapping (e.g., isinstance(pi_cfg, dict) or
collections.abc.Mapping) before calling pi_cfg.get(...), and if it is not a
mapping raise a clear configuration error (ValueError or custom) that surfaces
the bad config; keep using PromptImproverConfig(enabled=...,
persist_across_turns=...) when the value is valid and fall back to the default
PromptImproverConfig() otherwise.

Comment on lines +10078 to +10086
# Save post-coordination artifacts (final/, coordination_events.json, etc.)
from massgen.logger_config import get_log_session_dir

log_session_dir = get_log_session_dir()
if log_session_dir:
orchestrator.finalize_step_mode(log_session_dir)

_automation_print(f"ACTION: {action_data['action']}")
_automation_print(f"STATUS: {Path(args.session_dir).resolve() / 'last_action.json'}")
_automation_print(f"STATUS: {Path(args.session_dir).resolve() / 'agents' / real_agent_id / 'last_action.json'}")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Print the step result before finalizing extra artifacts.

save_step_mode_output() has already written last_action.json, but finalize_step_mode() now runs before the ACTION / STATUS lines are emitted. Any slow copy or exception there delays or suppresses the machine-readable handoff the parent process is waiting for.

Suggested fix
-                # Save post-coordination artifacts (final/, coordination_events.json, etc.)
-                from massgen.logger_config import get_log_session_dir
-
-                log_session_dir = get_log_session_dir()
-                if log_session_dir:
-                    orchestrator.finalize_step_mode(log_session_dir)
-
                 _automation_print(f"ACTION: {action_data['action']}")
                 _automation_print(f"STATUS: {Path(args.session_dir).resolve() / 'agents' / real_agent_id / 'last_action.json'}")
+
+                try:
+                    from massgen.logger_config import get_log_session_dir
+
+                    log_session_dir = get_log_session_dir()
+                    if log_session_dir:
+                        orchestrator.finalize_step_mode(log_session_dir)
+                except Exception as exc:
+                    logger.warning(f"Failed to finalize step mode artifacts: {exc}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Save post-coordination artifacts (final/, coordination_events.json, etc.)
from massgen.logger_config import get_log_session_dir
log_session_dir = get_log_session_dir()
if log_session_dir:
orchestrator.finalize_step_mode(log_session_dir)
_automation_print(f"ACTION: {action_data['action']}")
_automation_print(f"STATUS: {Path(args.session_dir).resolve() / 'last_action.json'}")
_automation_print(f"STATUS: {Path(args.session_dir).resolve() / 'agents' / real_agent_id / 'last_action.json'}")
_automation_print(f"ACTION: {action_data['action']}")
_automation_print(f"STATUS: {Path(args.session_dir).resolve() / 'agents' / real_agent_id / 'last_action.json'}")
try:
from massgen.logger_config import get_log_session_dir
log_session_dir = get_log_session_dir()
if log_session_dir:
orchestrator.finalize_step_mode(log_session_dir)
except Exception as exc:
logger.warning(f"Failed to finalize step mode artifacts: {exc}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@massgen/cli.py` around lines 10078 - 10086, The ACTION and STATUS
machine-readable lines are printed after calling
orchestrator.finalize_step_mode, which can delay or fail the handoff; move the
_automation_print calls (using action_data['action'] and the STATUS path built
from args.session_dir and real_agent_id) to occur before
orchestrator.finalize_step_mode(log_session_dir) so the step result is emitted
immediately (ensure you still call finalize_step_mode afterwards to save extra
artifacts).

Comment on lines +11743 to +11750
# Default to v2 UI; --quickstart opens the wizard overlay,
# --setup opens the setup overlay in v2.
if getattr(args, "setup", False):
browser_url += "/setup"
browser_url += "/?v=2&setup=open"
elif getattr(args, "quickstart", False):
browser_url += "/?wizard=open"
browser_url += "/?v=2&wizard=open"
else:
browser_url += "/?v=2"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Preserve existing query params when forcing v=2.

When auto_url is already something like http://host:port/?prompt=...&config=..., appending /?v=2 turns v=2 into part of the last query value. The browser opens a malformed URL and loses the prefilled prompt/config state.

Suggested fix
-                browser_url = auto_url if auto_url else f"http://{args.web_host}:{args.web_port}"
-                # Remove trailing slash to avoid double slashes
-                browser_url = browser_url.rstrip("/")
-
-                # Default to v2 UI; --quickstart opens the wizard overlay,
-                # --setup opens the setup overlay in v2.
-                if getattr(args, "setup", False):
-                    browser_url += "/?v=2&setup=open"
-                elif getattr(args, "quickstart", False):
-                    browser_url += "/?v=2&wizard=open"
-                else:
-                    browser_url += "/?v=2"
+                split_url = urllib.parse.urlsplit(auto_url or base_url)
+                params = dict(
+                    urllib.parse.parse_qsl(
+                        split_url.query,
+                        keep_blank_values=True,
+                    )
+                )
+                params["v"] = "2"
+                if getattr(args, "setup", False):
+                    params["setup"] = "open"
+                elif getattr(args, "quickstart", False):
+                    params["wizard"] = "open"
+
+                browser_url = urllib.parse.urlunsplit(
+                    (
+                        split_url.scheme,
+                        split_url.netloc,
+                        split_url.path or "/",
+                        urllib.parse.urlencode(params),
+                        split_url.fragment,
+                    )
+                )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@massgen/cli.py` around lines 11743 - 11750, The code appends "/?v=2" to
browser_url which breaks existing query params (variable browser_url in
massgen/cli.py); instead parse browser_url with urllib.parse
(urlparse/parse_qs), update the query dict to set "v"="2" and add "setup"="open"
or "wizard"="open" when args.setup or args.quickstart are set, then rebuild the
URL with urlencode and urlunparse so existing params (e.g., prompt, config) are
preserved; modify the block that checks getattr(args, "setup"/"quickstart") to
perform this parse-modify-rebuild flow rather than string concatenation.

Comment on lines +1052 to +1062
try:
content = criteria_file.read_text()
criteria = _parse_criteria_response(
content,
min_criteria,
max_criteria,
)
if criteria:
return criteria
except Exception as e:
logger.debug(f"Failed to parse {criteria_file}: {e}")
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Don't silently downgrade to defaults on unexpected criteria-file failures.

This path now does a single artifact read, and the blanket except Exception turns decode/parser regressions into a quiet fallback with only a debug log. That makes a broken criteria.json path look the same as “no usable artifact found.” Read the file as UTF-8 and only suppress expected file/JSON errors here; let unexpected exceptions surface so the caller logs them loudly.

🧰 Tools
🪛 Ruff (0.15.6)

[warning] 1061-1061: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@massgen/evaluation_criteria_generator.py` around lines 1052 - 1062, The
current blanket except in the block that reads criteria_file and calls
_parse_criteria_response hides unexpected errors; change
criteria_file.read_text() to explicitly read as UTF-8
(read_text(encoding="utf-8")) and replace the broad "except Exception" with
catching only expected errors (e.g., UnicodeDecodeError, json.JSONDecodeError
from the parser, and file-related errors like FileNotFoundError/PermissionError)
so those are suppressed/logged as before, but re-raise any other exceptions so
they surface to the caller; keep references to criteria_file,
_parse_criteria_response, and logger.debug to locate and update the code.

Comment on lines +917 to +930
agents_list = raw.get("agents", [])
agents_result = []
for agent_cfg in agents_list:
if not isinstance(agent_cfg, dict):
continue
backend = agent_cfg.get("backend", {})
if not isinstance(backend, dict):
backend = {}
agents_result.append(
{
"id": agent_cfg.get("id", ""),
"provider": backend.get("type", None),
"model": backend.get("model", None),
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Read model IDs from both supported config shapes.

This only looks at backend.model, but the rest of this file already handles configs that store the model in backend_params.model. Those valid configs will come back with blank model labels in the WebUI.

Suggested fix
             agents_list = raw.get("agents", [])
             agents_result = []
             for agent_cfg in agents_list:
                 if not isinstance(agent_cfg, dict):
                     continue
                 backend = agent_cfg.get("backend", {})
                 if not isinstance(backend, dict):
                     backend = {}
+                backend_params = agent_cfg.get("backend_params", {})
+                if not isinstance(backend_params, dict):
+                    backend_params = {}
                 agents_result.append(
                     {
                         "id": agent_cfg.get("id", ""),
                         "provider": backend.get("type", None),
-                        "model": backend.get("model", None),
+                        "model": backend.get("model") or backend_params.get("model"),
                     },
                 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@massgen/frontend/web/server.py` around lines 917 - 930, The agent listing
only reads model from backend.get("model") so configs that store the model under
backend_params.model are returned blank; update the loop that builds
agents_result (the agents_list/agent_cfg/backend block) to compute model by
checking backend.get("model") first and falling back to
backend.get("backend_params", {}) .get("model") (or vice‑versa if preferred),
then use that computed model value for the "model" field while keeping provider
from backend.get("type"). Ensure backend is still normalized to a dict as
currently done.

Comment on lines +1769 to +1789
config, _ = load_config_file(str(resolved_path))
original_yaml = yaml.dump(
copy.deepcopy(config),
default_flow_style=False,
sort_keys=False,
allow_unicode=True,
)

_apply_mode_overrides(config, mode_overrides)

resolved_yaml = yaml.dump(
config,
default_flow_style=False,
sort_keys=False,
allow_unicode=True,
)

return {
"original_yaml": original_yaml,
"resolved_yaml": resolved_yaml,
"config": config,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't serialize the resolved config as original_yaml.

load_config_file() returns a raw/safe representation elsewhere in this file for metadata logging. Dumping config here makes the “original” preview inaccurate and can leak env-expanded values to the browser. Use the raw return value (or reread the file) for original_yaml, and keep config only for the post-override preview.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@massgen/frontend/web/server.py` around lines 1769 - 1789, The current code
dumps the post-processed config into original_yaml which leaks env-expanded
values; instead call load_config_file to capture the raw representation and the
parsed config (e.g. raw, config = load_config_file(...)), use yaml.dump on the
raw value for original_yaml (or re-read resolved_path as raw text), then call
_apply_mode_overrides(config, mode_overrides) and yaml.dump that processed
config into resolved_yaml while keeping the parsed config object returned for
further use.

Comment on lines +2351 to +2356
# Sort by start_time/completed_at descending, limit to 50
def _sort_key(s: dict) -> str:
return s.get("start_time") or s.get("completed_at") or ""

sessions.sort(key=_sort_key, reverse=True)
return {"sessions": sessions[:50]}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Normalize timestamps before sorting the merged session list.

completed_at is stored as a float (time.time()), while historical start_time values are strings. Python 3 cannot compare float and str, so once both sources are present /api/sessions can fail with TypeError instead of returning results.

Suggested fix
-        def _sort_key(s: dict) -> str:
-            return s.get("start_time") or s.get("completed_at") or ""
+        from datetime import datetime
+
+        def _sort_key(s: dict) -> float:
+            for value in (s.get("completed_at"), s.get("start_time")):
+                if isinstance(value, (int, float)):
+                    return float(value)
+                if isinstance(value, str) and value:
+                    try:
+                        return datetime.fromisoformat(
+                            value.replace("Z", "+00:00"),
+                        ).timestamp()
+                    except ValueError:
+                        try:
+                            return float(value)
+                        except ValueError:
+                            continue
+            return 0.0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@massgen/frontend/web/server.py` around lines 2351 - 2356, The merged session
sort fails when comparing start_time (string) and completed_at (float); update
the _sort_key used by sessions.sort to normalize timestamps to a numeric value
before comparison: in the _sort_key function (used when sorting sessions),
coerce s.get("start_time") and s.get("completed_at") into a float (e.g., parse
start_time if it's a string, fallback to completed_at, and return 0.0 on
missing/invalid values) so the sort always compares floats and cannot raise
TypeError.

Comment on lines +2364 to +2418
@app.delete("/api/sessions/{session_id}")
async def delete_session(session_id: str):
"""Delete a session and clean up its resources."""
removed = False

# Remove from completed sessions
if session_id in manager.completed_sessions:
del manager.completed_sessions[session_id]
removed = True

# Remove display
if session_id in manager.displays:
del manager.displays[session_id]
removed = True

# Cancel and remove task
if session_id in manager.tasks:
task = manager.tasks[session_id]
if not task.done():
task.cancel()
del manager.tasks[session_id]
removed = True

# Remove orchestrator
if session_id in manager.orchestrators:
del manager.orchestrators[session_id]
removed = True

# Remove log dir reference
if session_id in manager.session_log_dirs:
del manager.session_log_dirs[session_id]

# Remove config reference
if session_id in manager.session_configs:
del manager.session_configs[session_id]

# Remove from persistent registry
try:
from massgen.session import SessionRegistry

SessionRegistry().delete_session(session_id)
except Exception:
logger.warning(
"Failed to delete session %s from registry",
session_id,
exc_info=True,
)

if not removed:
return JSONResponse(
{"error": "Session not found"},
status_code=404,
)

return JSONResponse({"status": "deleted", "session_id": session_id})
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

DELETE doesn't clear all session indexes.

This only removes task/display/orchestrator metadata. It leaves manager.active_connections, workspace_manager.workspace_connections, and the historical log-directory source intact, so a “deleted” session can still appear as active or reappear from log scanning.

🧰 Tools
🪛 Ruff (0.15.6)

[warning] 2405-2405: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@massgen/frontend/web/server.py` around lines 2364 - 2418, The delete_session
handler currently only removes completed_sessions, displays, tasks,
orchestrators and some session maps; also remove any live connection and
workspace indexes and the historical log-dir reference so deleted sessions don't
reappear: in delete_session, check and remove session_id from
manager.active_connections (and close/terminate any connection object if
applicable), remove session_id entries from
workspace_manager.workspace_connections, and ensure
manager.session_log_dirs[session_id] is deleted (it may already be present in
code but ensure it's removed before returning); perform these removals before
calling SessionRegistry().delete_session and keep the same warning/JSONResponse
behavior.

@Henry-811 Henry-811 changed the base branch from main to dev/v0.1.67 March 23, 2026 16:38
@Henry-811 Henry-811 merged commit 0a71487 into dev/v0.1.67 Mar 23, 2026
22 checks passed
This was referenced Mar 23, 2026
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