codee-agent 0.4.0__tar.gz → 0.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. {codee_agent-0.4.0 → codee_agent-0.4.1}/PKG-INFO +1 -1
  2. {codee_agent-0.4.0 → codee_agent-0.4.1}/pyproject.toml +7 -1
  3. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/admin.py +273 -17
  4. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/admin_cli.py +12 -2
  5. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/admin_service.py +244 -15
  6. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/executor.py +4 -20
  7. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/init_cli.py +5 -3
  8. codee_agent-0.4.1/src/codee/lib/mcp_config.py +112 -0
  9. codee_agent-0.4.1/src/codee/lib/test_mcp_config.py +134 -0
  10. codee_agent-0.4.1/src/codee/tasks_providers.py +25 -0
  11. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/test_admin_cli.py +32 -5
  12. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/test_admin_service.py +382 -1
  13. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/test_executor.py +3 -3
  14. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/test_init_cli.py +7 -5
  15. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/test_start_cli.py +2 -1
  16. codee_agent-0.4.1/src/codee_tasks_abstract/provider.py +151 -0
  17. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_tasks_azure_devops/oauth.py +17 -1
  18. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_tasks_azure_devops/provider.py +95 -5
  19. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_tasks_azure_devops/test.py +111 -0
  20. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_tasks_jira/provider.py +92 -13
  21. codee_agent-0.4.1/src/codee_tasks_jira/test.py +213 -0
  22. codee_agent-0.4.0/src/codee_tasks_abstract/provider.py +0 -58
  23. codee_agent-0.4.0/src/codee_tasks_jira/test.py +0 -75
  24. {codee_agent-0.4.0 → codee_agent-0.4.1}/LICENSE +0 -0
  25. {codee_agent-0.4.0 → codee_agent-0.4.1}/README.md +0 -0
  26. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/.gitignore +0 -0
  27. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/__init__.py +0 -0
  28. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/admin_api.py +0 -0
  29. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/lib/__init__.py +0 -0
  30. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/lib/cron_describe.py +0 -0
  31. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/lib/runs_db.py +0 -0
  32. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/lib/test_runs_db.py +0 -0
  33. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/lib/test_trigger_cron_skills.py +0 -0
  34. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/lib/test_trigger_issue_skills.py +0 -0
  35. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/lib/trigger_aws_sqs_skills.py +0 -0
  36. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/lib/trigger_cron_skills.py +0 -0
  37. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/lib/trigger_email_skills.py +0 -0
  38. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/lib/trigger_issue_skills.py +0 -0
  39. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/mail_server.py +0 -0
  40. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/start_cli.py +0 -0
  41. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/templates/AGENTS.md +0 -0
  42. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/templates/CLAUDE.md +0 -0
  43. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md +0 -0
  44. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md +0 -0
  45. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/templates/skills/story-code-reviewer/SKILL.md +0 -0
  46. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/templates/skills/story-developer/SKILL.md +0 -0
  47. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/templates/skills/story-planner/SKILL.md +0 -0
  48. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/templates/skills/story-planner/assets/readme-template.md +0 -0
  49. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/templates/skills/story-qa/SKILL.md +0 -0
  50. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/templates/skills/task-developer/SKILL.md +0 -0
  51. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/templates/skills/task-qa/SKILL.md +0 -0
  52. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/test_admin_api.py +0 -0
  53. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/test_memory_index.py +0 -0
  54. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee/workflow_graph.py +0 -0
  55. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_admin/__init__.py +0 -0
  56. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_admin/codee_admin.py +0 -0
  57. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_agent_abstract/__init__.py +0 -0
  58. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_agent_abstract/provider.py +0 -0
  59. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_agent_claude_code/__init__.py +0 -0
  60. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_agent_claude_code/provider.py +0 -0
  61. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_agent_github_copilot/__init__.py +0 -0
  62. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_agent_github_copilot/provider.py +0 -0
  63. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_agent_github_copilot/test.py +0 -0
  64. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_database/__init__.py +0 -0
  65. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_database/database.py +0 -0
  66. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_database/oauth_tokens.py +0 -0
  67. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_main_context/__init__.py +0 -0
  68. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_main_context/context.py +0 -0
  69. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_main_context/logging.py +0 -0
  70. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_main_context/test_logging.py +0 -0
  71. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_tasks_abstract/__init__.py +0 -0
  72. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_tasks_azure_devops/__init__.py +0 -0
  73. {codee_agent-0.4.0 → codee_agent-0.4.1}/src/codee_tasks_jira/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codee-agent
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: A virtual co-worker that picks up Jira and Azure DevOps tasks and solves them with coding agents.
5
5
  Keywords: ai,agent,jira,azure-devops,claude-code,github-copilot,automation
6
6
  Author: Denis Kibalko
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codee-agent"
3
- version = "0.4.0"
3
+ version = "0.4.1"
4
4
  description = "A virtual co-worker that picks up Jira and Azure DevOps tasks and solves them with coding agents."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -47,6 +47,12 @@ codee-init = "codee.init_cli:main"
47
47
  codee-admin = "codee.admin_cli:main"
48
48
  codee-executor = "codee.executor:main"
49
49
 
50
+ [tool.pytest.ini_options]
51
+ # The provider and agent packages keep their suites in `test.py`, which pytest's
52
+ # default `test_*.py` pattern skips — without this, `pytest src` silently ran
53
+ # 61 fewer tests than the repo has.
54
+ python_files = ["test_*.py", "test.py"]
55
+
50
56
  [tool.uv.build-backend]
51
57
  module-name = [
52
58
  "codee",
@@ -8,7 +8,8 @@ import reflex as rx
8
8
  from pydantic import BaseModel
9
9
 
10
10
  from codee.admin_api import api_app
11
- from codee.admin_service import AGENTS_FILE, AdminService, ISSUE_TYPES, SKILL_TYPES
11
+ from codee.admin_service import (
12
+ AGENTS_FILE, TASKS_CHECKS, AdminService, ISSUE_TYPES, SKILL_TYPES)
12
13
  from codee.workflow_graph import workflow_graph
13
14
 
14
15
  SERVICE = AdminService()
@@ -60,6 +61,35 @@ class ActiveJob(BaseModel):
60
61
  viewer_url: str
61
62
 
62
63
 
64
+ class CheckResult(BaseModel):
65
+ """One line of the tasks-provider check list on the settings page.
66
+
67
+ ``status`` is where it is rather than how it went: a check has to be on
68
+ screen before it has an answer, so "waiting" and "running" are states of the
69
+ same row that later becomes ``ok`` or ``failed``.
70
+ """
71
+
72
+ name: str
73
+ status: str
74
+ message: str = ""
75
+
76
+
77
+ def pending_checks(done: list[CheckResult]) -> list[CheckResult]:
78
+ """The finished checks, then the running one, then the ones still to come."""
79
+ rows = list(done)
80
+ for position, name in enumerate(TASKS_CHECKS[len(done):], start=len(done)):
81
+ rows.append(CheckResult(
82
+ name=name,
83
+ status="running" if position == len(done) else "waiting"))
84
+ return rows
85
+
86
+
87
+ def check_result(check: dict[str, Any]) -> CheckResult:
88
+ return CheckResult(name=check["name"],
89
+ status="ok" if check["ok"] else "failed",
90
+ message=check["message"])
91
+
92
+
63
93
  class RunRecord(BaseModel):
64
94
  skill_name: str
65
95
  trigger_type: str
@@ -143,6 +173,11 @@ class AdminState(rx.State):
143
173
  azure_account: str = ""
144
174
  azure_expires_label: str = ""
145
175
  azure_redirect_uri: str = ""
176
+ tasks_verifying: bool = False
177
+ tasks_checks: list[CheckResult] = []
178
+ mcp_setup_ok: bool = False
179
+ mcp_setup_message: str = ""
180
+ mcp_configured: bool = False
146
181
 
147
182
  @rx.var
148
183
  def filtered_skills(self) -> list[SkillSummary]:
@@ -541,9 +576,15 @@ class AdminState(rx.State):
541
576
  self.azure_tenant_id = azure.get("tenant_id", "")
542
577
  self.azure_client_id = azure.get("client_id", "")
543
578
  self.azure_client_secret = azure.get("client_secret", "")
579
+ self._drop_provider_results()
580
+ self.load_mcp_status()
544
581
  self.load_azure_connection()
545
582
  return self._azure_callback_toast()
546
583
 
584
+ def load_mcp_status(self) -> None:
585
+ """Read back whether the selected provider's MCP server is already installed."""
586
+ self.mcp_configured = SERVICE.tasks_mcp_configured(self.tasks_provider)
587
+
547
588
  def load_azure_connection(self) -> None:
548
589
  connection = SERVICE.azure_connection()
549
590
  self.azure_connected = connection["connected"]
@@ -562,8 +603,17 @@ class AdminState(rx.State):
562
603
  return rx.toast.success(message or "Connected to Azure DevOps")
563
604
  return rx.toast.error(message or "Could not connect to Azure DevOps")
564
605
 
606
+ def _drop_provider_results(self) -> None:
607
+ """Forget the last checks and MCP setup: they spoke for credentials that just changed."""
608
+ self.tasks_verifying = False
609
+ self.tasks_checks = []
610
+ self.mcp_setup_ok = False
611
+ self.mcp_setup_message = ""
612
+
565
613
  def set_tasks_provider(self, value: str) -> None:
566
614
  self.tasks_provider = value
615
+ self._drop_provider_results()
616
+ self.load_mcp_status()
567
617
 
568
618
  def set_coding_agent(self, value: str) -> None:
569
619
  self.coding_agent = value
@@ -573,27 +623,35 @@ class AdminState(rx.State):
573
623
 
574
624
  def set_jira_base_url(self, value: str) -> None:
575
625
  self.jira_base_url = value
626
+ self._drop_provider_results()
576
627
 
577
628
  def set_jira_account_email(self, value: str) -> None:
578
629
  self.jira_account_email = value
630
+ self._drop_provider_results()
579
631
 
580
632
  def set_jira_api_token(self, value: str) -> None:
581
633
  self.jira_api_token = value
634
+ self._drop_provider_results()
582
635
 
583
636
  def set_jira_project(self, value: str) -> None:
584
637
  self.jira_project = value
638
+ self._drop_provider_results()
585
639
 
586
640
  def set_azure_organization_url(self, value: str) -> None:
587
641
  self.azure_organization_url = value
642
+ self._drop_provider_results()
588
643
 
589
644
  def set_azure_tenant_id(self, value: str) -> None:
590
645
  self.azure_tenant_id = value
646
+ self._drop_provider_results()
591
647
 
592
648
  def set_azure_client_id(self, value: str) -> None:
593
649
  self.azure_client_id = value
650
+ self._drop_provider_results()
594
651
 
595
652
  def set_azure_client_secret(self, value: str) -> None:
596
653
  self.azure_client_secret = value
654
+ self._drop_provider_results()
597
655
 
598
656
  @rx.var
599
657
  def azure_can_connect(self) -> bool:
@@ -602,6 +660,101 @@ class AdminState(rx.State):
602
660
  self.azure_organization_url,
603
661
  self.azure_client_id, self.azure_client_secret))
604
662
 
663
+ @rx.var
664
+ def tasks_can_verify(self) -> bool:
665
+ """Whether a real pull is worth attempting with what's on the form.
666
+
667
+ Every field the provider reads has to be filled in, and Azure DevOps
668
+ additionally needs the OAuth consent — without it the check could only
669
+ ever report "not connected", which the status line above already says.
670
+ """
671
+ if self.tasks_provider == "jira":
672
+ return all(value.strip() for value in (
673
+ self.jira_base_url, self.jira_account_email,
674
+ self.jira_api_token, self.jira_project))
675
+ return self.azure_can_connect and self.azure_connected
676
+
677
+ @rx.var
678
+ def mcp_can_setup(self) -> bool:
679
+ """Whether the selected provider can describe its MCP server yet.
680
+
681
+ Less than the pull needs in both cases. mcp-atlassian doesn't take a
682
+ project key — the agent searches and updates issues wherever they live
683
+ rather than polling one project — and the Azure DevOps server takes only
684
+ the organization, since it signs in through the Azure CLI rather than
685
+ through the app registration.
686
+ """
687
+ if self.tasks_provider == "jira":
688
+ return all(value.strip() for value in (
689
+ self.jira_base_url, self.jira_account_email,
690
+ self.jira_api_token))
691
+ return bool(self.azure_organization_url.strip())
692
+
693
+ @rx.var
694
+ def mcp_provider_label(self) -> str:
695
+ return ("Azure DevOps" if self.tasks_provider == "azure_devops"
696
+ else "Jira")
697
+
698
+ @rx.var
699
+ def mcp_button_label(self) -> str:
700
+ """Re-running is how stale credentials in the file get replaced."""
701
+ return (f"Setup {self.mcp_provider_label} MCP again"
702
+ if self.mcp_configured
703
+ else f"Setup {self.mcp_provider_label} MCP")
704
+
705
+ @rx.var
706
+ def mcp_missing_hint(self) -> str:
707
+ if self.tasks_provider == "jira":
708
+ return "Fill in base URL, account email and API token first."
709
+ return "Fill in the organization URL first."
710
+
711
+ def setup_tasks_mcp(self) -> None:
712
+ """Write the provider's MCP server into the project's .mcp.json.
713
+
714
+ Synchronous, unlike the connection check: this only touches a local file,
715
+ and the result should be on screen by the time the click lands.
716
+ """
717
+ self.mcp_setup_ok, self.mcp_setup_message = SERVICE.setup_tasks_mcp(
718
+ self.tasks_provider, self._credentials())
719
+ self.load_mcp_status()
720
+
721
+ @rx.event(background=True)
722
+ async def verify_tasks_connection(self) -> Any:
723
+ """Run the provider checks with the credentials on the form.
724
+
725
+ In the background because none of it is quick: the provider allows
726
+ itself 30s per request, and the MCP check runs a whole coding agent. The
727
+ whole list goes up at once and each row is filled in as its check
728
+ finishes, so the wait is spent looking at what is still running rather
729
+ than at nothing. Nothing is saved — verifying shouldn't commit anything.
730
+ """
731
+ async with self:
732
+ if self.tasks_verifying or not self.tasks_can_verify:
733
+ return
734
+ self.tasks_verifying = True
735
+ self.tasks_checks = pending_checks([])
736
+ provider, credentials = self.tasks_provider, self._credentials()
737
+
738
+ # A generator, so nothing has run yet: each next() is one check, and the
739
+ # thread keeps the page responsive while it does.
740
+ checks = SERVICE.verify_tasks_connection(provider, credentials)
741
+ done: list[CheckResult] = []
742
+ failure = ""
743
+ try:
744
+ while (result := await asyncio.to_thread(next, checks, None)) is not None:
745
+ done.append(check_result(result))
746
+ async with self:
747
+ self.tasks_checks = pending_checks(done)
748
+ except Exception as error:
749
+ failure = str(error)
750
+ async with self:
751
+ self.tasks_verifying = False
752
+ # Whatever the generator stopped short of is dropped rather than
753
+ # left spinning: a run that yielded one check has only one to show.
754
+ self.tasks_checks = done + ([CheckResult(
755
+ name="Verification failed", status="failed", message=failure)]
756
+ if failure else [])
757
+
605
758
  def connect_azure_devops(self) -> Any:
606
759
  """Save the app registration, then hand the browser to Entra ID for consent.
607
760
 
@@ -622,35 +775,37 @@ class AdminState(rx.State):
622
775
 
623
776
  def disconnect_azure_devops(self) -> Any:
624
777
  SERVICE.disconnect_azure()
778
+ self._drop_provider_results()
625
779
  self.load_azure_connection()
626
780
  return rx.toast.success("Disconnected from Azure DevOps")
627
781
 
782
+ def _credentials(self) -> dict[str, str]:
783
+ """The selected provider's credentials as they stand on the form."""
784
+ if self.tasks_provider == "jira":
785
+ return {
786
+ "base_url": self.jira_base_url,
787
+ "account_email": self.jira_account_email,
788
+ "api_token": self.jira_api_token,
789
+ "project": self.jira_project,
790
+ }
791
+ return {
792
+ "organization_url": self.azure_organization_url,
793
+ "tenant_id": self.azure_tenant_id,
794
+ "client_id": self.azure_client_id,
795
+ "client_secret": self.azure_client_secret,
796
+ }
797
+
628
798
  def _persist_settings(self) -> str:
629
799
  """Write the settings to disk. Returns an error message, or '' when saved."""
630
800
  try:
631
801
  parallel_agents = int(self.max_parallel_agents)
632
802
  except ValueError:
633
803
  return "Max parallel tasks must be a number"
634
- credentials = (
635
- {
636
- "base_url": self.jira_base_url,
637
- "account_email": self.jira_account_email,
638
- "api_token": self.jira_api_token,
639
- "project": self.jira_project,
640
- }
641
- if self.tasks_provider == "jira"
642
- else {
643
- "organization_url": self.azure_organization_url,
644
- "tenant_id": self.azure_tenant_id,
645
- "client_id": self.azure_client_id,
646
- "client_secret": self.azure_client_secret,
647
- }
648
- )
649
804
  SERVICE.save_settings(
650
805
  self.tasks_provider,
651
806
  self.coding_agent,
652
807
  parallel_agents,
653
- credentials,
808
+ self._credentials(),
654
809
  )
655
810
  return ""
656
811
 
@@ -1627,6 +1782,105 @@ def azure_fields() -> rx.Component:
1627
1782
  spacing="4", width="100%")
1628
1783
 
1629
1784
 
1785
+ def mcp_setup_button() -> rx.Component:
1786
+ """The setup button, built fresh per branch so each can sit in its own row."""
1787
+ return rx.button(
1788
+ rx.icon("file-cog", size=16), AdminState.mcp_button_label,
1789
+ type="button", variant="outline",
1790
+ disabled=~AdminState.mcp_can_setup,
1791
+ on_click=AdminState.setup_tasks_mcp)
1792
+
1793
+
1794
+ def tasks_mcp_setup() -> rx.Component:
1795
+ """Hand the provider's credentials to the coding agent as an MCP server."""
1796
+ return rx.vstack(
1797
+ rx.cond(
1798
+ AdminState.mcp_configured,
1799
+ # Already installed: the state is the sentence, and the button that
1800
+ # rewrites it with whatever the fields now say sits beside it.
1801
+ rx.hstack(
1802
+ rx.icon("circle-check", size=16, color="var(--green-9)"),
1803
+ rx.text("MCP config already set up.", font_size="0.85rem"),
1804
+ mcp_setup_button(),
1805
+ spacing="3", align="center", width="100%"),
1806
+ rx.hstack(
1807
+ mcp_setup_button(),
1808
+ rx.cond(
1809
+ AdminState.mcp_can_setup,
1810
+ rx.text("Writes .mcp.json so the coding agent can read and "
1811
+ "update tasks itself.",
1812
+ color=MUTED, font_size="0.8rem"),
1813
+ rx.text(AdminState.mcp_missing_hint,
1814
+ color=MUTED, font_size="0.8rem")),
1815
+ spacing="3", align="center", width="100%")),
1816
+ rx.cond(
1817
+ AdminState.mcp_setup_message != "",
1818
+ rx.cond(
1819
+ AdminState.mcp_setup_ok,
1820
+ rx.callout(AdminState.mcp_setup_message, icon="circle-check",
1821
+ size="1", color_scheme="green", width="100%"),
1822
+ rx.callout(AdminState.mcp_setup_message, icon="circle-alert",
1823
+ size="1", color_scheme="red", width="100%")),
1824
+ ),
1825
+ spacing="3", width="100%")
1826
+
1827
+
1828
+ def tasks_check_row(check: CheckResult) -> rx.Component:
1829
+ """One check: where it stands as an icon, its name, and what it found."""
1830
+ marker = {"flex_shrink": "0", "margin_top": "0.15rem"}
1831
+ return rx.hstack(
1832
+ rx.match(
1833
+ check.status,
1834
+ ("running", rx.spinner(size="2", **marker)),
1835
+ ("ok", rx.icon("circle-check", size=16,
1836
+ color="var(--green-9)", **marker)),
1837
+ ("failed", rx.icon("circle-alert", size=16,
1838
+ color="var(--red-9)", **marker)),
1839
+ # Still queued: named so the list is complete from the first frame,
1840
+ # but visibly not its turn yet.
1841
+ rx.icon("circle-dashed", size=16, color=MUTED, **marker)),
1842
+ rx.vstack(
1843
+ rx.text(check.name, font_size="0.85rem", weight="medium",
1844
+ color=rx.cond(check.status == "waiting", MUTED, TEXT)),
1845
+ rx.cond(
1846
+ check.message != "",
1847
+ rx.text(check.message, color=MUTED, font_size="0.8rem")),
1848
+ spacing="1", align="start", width="100%"),
1849
+ spacing="2", align="start", width="100%")
1850
+
1851
+
1852
+ def tasks_verification() -> rx.Component:
1853
+ """Run the provider checks and print what each one found."""
1854
+ return rx.vstack(
1855
+ rx.hstack(
1856
+ rx.button(
1857
+ rx.cond(AdminState.tasks_verifying,
1858
+ rx.spinner(size="2"), rx.icon("plug-zap", size=16)),
1859
+ rx.cond(AdminState.tasks_verifying,
1860
+ "Verifying…", "Verify connection"),
1861
+ type="button", variant="outline",
1862
+ disabled=AdminState.tasks_verifying | ~AdminState.tasks_can_verify,
1863
+ on_click=AdminState.verify_tasks_connection),
1864
+ rx.cond(
1865
+ AdminState.tasks_can_verify,
1866
+ # Worth saying up front: the second check spends a coding-agent
1867
+ # run and leaves a closed task behind in the real backend.
1868
+ rx.text("Pulls tasks, then has the coding agent create and "
1869
+ "close one task through MCP. Takes a minute.",
1870
+ color=MUTED, font_size="0.8rem"),
1871
+ rx.text("Fill in every field above to check the connection.",
1872
+ color=MUTED, font_size="0.8rem")),
1873
+ spacing="3", align="center", width="100%"),
1874
+ rx.cond(
1875
+ AdminState.tasks_checks,
1876
+ rx.vstack(rx.foreach(AdminState.tasks_checks, tasks_check_row),
1877
+ spacing="3", width="100%",
1878
+ padding="0.85rem", border=BORDER,
1879
+ background=PAGE_BACKGROUND),
1880
+ ),
1881
+ spacing="3", width="100%")
1882
+
1883
+
1630
1884
  def settings_page() -> rx.Component:
1631
1885
  jira_fields = rx.vstack(
1632
1886
  field("Base URL", rx.input(value=AdminState.jira_base_url,
@@ -1657,6 +1911,8 @@ def settings_page() -> rx.Component:
1657
1911
  on_change=AdminState.set_tasks_provider, width="100%")),
1658
1912
  rx.box(rx.cond(AdminState.tasks_provider == "jira",
1659
1913
  jira_fields, azure_fields()), margin_top="1rem"),
1914
+ rx.box(tasks_mcp_setup(), margin_top="1.25rem"),
1915
+ rx.box(tasks_verification(), margin_top="1.25rem"),
1660
1916
  padding="1.25rem", background=SURFACE, border=BORDER, width="100%"),
1661
1917
  rx.button(rx.icon("save", size=16), "Save settings",
1662
1918
  on_click=AdminState.save_settings),
@@ -4,7 +4,7 @@ import os
4
4
  import sys
5
5
  from pathlib import Path
6
6
 
7
- from codee.admin_service import DEFAULT_ADMIN_PORT
7
+ from codee.admin_service import DEFAULT_ADMIN_PORT, public_base_url
8
8
  from codee_main_context.context import data_dir, project_root
9
9
  from codee_main_context.logging import (
10
10
  configure_logging, enable_debug, get_logger, verbose_debug_enabled)
@@ -34,6 +34,16 @@ config = rx.Config(
34
34
  """
35
35
 
36
36
 
37
+ def _api_url(port: int) -> str:
38
+ """Origin the browser uses for the backend websocket and uploads.
39
+
40
+ Reflex bakes this into the compiled frontend, so behind a reverse proxy it
41
+ has to be the public origin rather than the loopback address the server
42
+ binds to -- otherwise the page loads and then fails to open its socket.
43
+ """
44
+ return public_base_url() or f"http://127.0.0.1:{port}"
45
+
46
+
37
47
  def _runtime_directory(root: Path) -> Path:
38
48
  runtime = data_dir(root) / "reflex"
39
49
  runtime.mkdir(parents=True, exist_ok=True)
@@ -59,7 +69,7 @@ def main() -> int:
59
69
  codee_data = data_dir(root).resolve()
60
70
  os.environ["CODEE_PROJECT_ROOT"] = str(root)
61
71
  os.environ["CODEE_DATA_DIR"] = str(codee_data)
62
- os.environ["REFLEX_API_URL"] = f"http://127.0.0.1:{arguments.port}"
72
+ os.environ["REFLEX_API_URL"] = _api_url(arguments.port)
63
73
  os.chdir(_runtime_directory(root))
64
74
 
65
75
  from reflex.reflex import cli