@researai/deepscientist 1.5.6 → 1.5.8

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 (158) hide show
  1. package/README.md +32 -0
  2. package/bin/ds.js +274 -18
  3. package/docs/en/07_MEMORY_AND_MCP.md +40 -3
  4. package/docs/en/99_ACKNOWLEDGEMENTS.md +1 -0
  5. package/docs/zh/07_MEMORY_AND_MCP.md +40 -3
  6. package/docs/zh/99_ACKNOWLEDGEMENTS.md +1 -0
  7. package/install.sh +34 -0
  8. package/package.json +1 -1
  9. package/pyproject.toml +1 -1
  10. package/src/deepscientist/__init__.py +1 -1
  11. package/src/deepscientist/acp/envelope.py +1 -0
  12. package/src/deepscientist/artifact/metrics.py +813 -80
  13. package/src/deepscientist/artifact/schemas.py +1 -0
  14. package/src/deepscientist/artifact/service.py +1101 -99
  15. package/src/deepscientist/bash_exec/monitor.py +1 -1
  16. package/src/deepscientist/bash_exec/service.py +17 -9
  17. package/src/deepscientist/channels/qq.py +17 -0
  18. package/src/deepscientist/channels/relay.py +16 -0
  19. package/src/deepscientist/cli.py +1 -1
  20. package/src/deepscientist/config/models.py +12 -6
  21. package/src/deepscientist/config/service.py +75 -2
  22. package/src/deepscientist/connector_profiles.py +34 -6
  23. package/src/deepscientist/daemon/api/handlers.py +290 -15
  24. package/src/deepscientist/daemon/api/router.py +2 -0
  25. package/src/deepscientist/daemon/app.py +521 -23
  26. package/src/deepscientist/gitops/diff.py +6 -10
  27. package/src/deepscientist/mcp/server.py +188 -39
  28. package/src/deepscientist/prompts/builder.py +71 -22
  29. package/src/deepscientist/qq_profiles.py +19 -9
  30. package/src/deepscientist/quest/layout.py +1 -0
  31. package/src/deepscientist/quest/service.py +83 -34
  32. package/src/deepscientist/quest/stage_views.py +74 -29
  33. package/src/deepscientist/runners/codex.py +32 -14
  34. package/src/deepscientist/runners/runtime_overrides.py +46 -0
  35. package/src/deepscientist/skills/installer.py +7 -0
  36. package/src/prompts/connectors/qq.md +1 -1
  37. package/src/prompts/contracts/shared_interaction.md +14 -0
  38. package/src/prompts/system.md +134 -30
  39. package/src/skills/analysis-campaign/SKILL.md +34 -8
  40. package/src/skills/analysis-campaign/references/campaign-checklist-template.md +41 -0
  41. package/src/skills/analysis-campaign/references/campaign-plan-template.md +68 -0
  42. package/src/skills/baseline/SKILL.md +145 -32
  43. package/src/skills/baseline/references/baseline-checklist-template.md +57 -0
  44. package/src/skills/baseline/references/baseline-plan-template.md +105 -0
  45. package/src/skills/decision/SKILL.md +12 -8
  46. package/src/skills/experiment/SKILL.md +51 -9
  47. package/src/skills/experiment/references/main-experiment-checklist-template.md +52 -0
  48. package/src/skills/experiment/references/main-experiment-plan-template.md +79 -0
  49. package/src/skills/figure-polish/SKILL.md +1 -0
  50. package/src/skills/finalize/SKILL.md +3 -8
  51. package/src/skills/idea/SKILL.md +2 -8
  52. package/src/skills/intake-audit/SKILL.md +2 -8
  53. package/src/skills/rebuttal/SKILL.md +2 -8
  54. package/src/skills/review/SKILL.md +2 -8
  55. package/src/skills/scout/SKILL.md +2 -8
  56. package/src/skills/write/SKILL.md +52 -16
  57. package/src/skills/write/templates/DEEPSCIENTIST_NOTES.md +21 -0
  58. package/src/skills/write/templates/README.md +408 -0
  59. package/src/skills/write/templates/UPSTREAM_LICENSE.txt +21 -0
  60. package/src/skills/write/templates/aaai2026/README.md +534 -0
  61. package/src/skills/write/templates/aaai2026/aaai2026-unified-supp.tex +144 -0
  62. package/src/skills/write/templates/aaai2026/aaai2026-unified-template.tex +952 -0
  63. package/src/skills/write/templates/aaai2026/aaai2026.bib +111 -0
  64. package/src/skills/write/templates/aaai2026/aaai2026.bst +1493 -0
  65. package/src/skills/write/templates/aaai2026/aaai2026.sty +315 -0
  66. package/src/skills/write/templates/acl/README.md +50 -0
  67. package/src/skills/write/templates/acl/acl.sty +312 -0
  68. package/src/skills/write/templates/acl/acl_latex.tex +377 -0
  69. package/src/skills/write/templates/acl/acl_lualatex.tex +101 -0
  70. package/src/skills/write/templates/acl/acl_natbib.bst +1940 -0
  71. package/src/skills/write/templates/acl/anthology.bib.txt +26 -0
  72. package/src/skills/write/templates/acl/custom.bib +70 -0
  73. package/src/skills/write/templates/acl/formatting.md +326 -0
  74. package/src/skills/write/templates/asplos2027/main.tex +459 -0
  75. package/src/skills/write/templates/asplos2027/references.bib +135 -0
  76. package/src/skills/write/templates/colm2025/README.md +3 -0
  77. package/src/skills/write/templates/colm2025/colm2025_conference.bib +11 -0
  78. package/src/skills/write/templates/colm2025/colm2025_conference.bst +1440 -0
  79. package/src/skills/write/templates/colm2025/colm2025_conference.sty +218 -0
  80. package/src/skills/write/templates/colm2025/colm2025_conference.tex +305 -0
  81. package/src/skills/write/templates/colm2025/fancyhdr.sty +485 -0
  82. package/src/skills/write/templates/colm2025/math_commands.tex +508 -0
  83. package/src/skills/write/templates/colm2025/natbib.sty +1246 -0
  84. package/src/skills/write/templates/iclr2026/fancyhdr.sty +485 -0
  85. package/src/skills/write/templates/iclr2026/iclr2026_conference.bib +24 -0
  86. package/src/skills/write/templates/iclr2026/iclr2026_conference.bst +1440 -0
  87. package/src/skills/write/templates/iclr2026/iclr2026_conference.sty +246 -0
  88. package/src/skills/write/templates/iclr2026/iclr2026_conference.tex +414 -0
  89. package/src/skills/write/templates/iclr2026/math_commands.tex +508 -0
  90. package/src/skills/write/templates/iclr2026/natbib.sty +1246 -0
  91. package/src/skills/write/templates/icml2026/algorithm.sty +79 -0
  92. package/src/skills/write/templates/icml2026/algorithmic.sty +201 -0
  93. package/src/skills/write/templates/icml2026/example_paper.bib +75 -0
  94. package/src/skills/write/templates/icml2026/example_paper.tex +662 -0
  95. package/src/skills/write/templates/icml2026/fancyhdr.sty +864 -0
  96. package/src/skills/write/templates/icml2026/icml2026.bst +1443 -0
  97. package/src/skills/write/templates/icml2026/icml2026.sty +767 -0
  98. package/src/skills/write/templates/neurips2025/Makefile +36 -0
  99. package/src/skills/write/templates/neurips2025/extra_pkgs.tex +53 -0
  100. package/src/skills/write/templates/neurips2025/main.tex +38 -0
  101. package/src/skills/write/templates/neurips2025/neurips.sty +382 -0
  102. package/src/skills/write/templates/nsdi2027/main.tex +426 -0
  103. package/src/skills/write/templates/nsdi2027/references.bib +151 -0
  104. package/src/skills/write/templates/nsdi2027/usenix-2020-09.sty +83 -0
  105. package/src/skills/write/templates/osdi2026/main.tex +429 -0
  106. package/src/skills/write/templates/osdi2026/references.bib +150 -0
  107. package/src/skills/write/templates/osdi2026/usenix-2020-09.sty +83 -0
  108. package/src/skills/write/templates/sosp2026/main.tex +532 -0
  109. package/src/skills/write/templates/sosp2026/references.bib +148 -0
  110. package/src/tui/package.json +1 -1
  111. package/src/ui/dist/assets/{AiManusChatView-BGLArZRn.js → AiManusChatView-m2FNtwbn.js} +110 -14
  112. package/src/ui/dist/assets/{AnalysisPlugin-BgDGSigG.js → AnalysisPlugin-BMTF8EGL.js} +1 -1
  113. package/src/ui/dist/assets/{AutoFigurePlugin-B65HD7L4.js → AutoFigurePlugin-DxPdMUNb.js} +5 -5
  114. package/src/ui/dist/assets/{CliPlugin-CUqgsFHC.js → CliPlugin-BEOWgxCI.js} +9 -9
  115. package/src/ui/dist/assets/{CodeEditorPlugin-CF5EdvaS.js → CodeEditorPlugin-BCXvjqmb.js} +8 -8
  116. package/src/ui/dist/assets/{CodeViewerPlugin-DEeU063D.js → CodeViewerPlugin-DaJcy3nD.js} +5 -5
  117. package/src/ui/dist/assets/{DocViewerPlugin-Df-FuDlZ.js → DocViewerPlugin-ByfeIq4K.js} +3 -3
  118. package/src/ui/dist/assets/{GitDiffViewerPlugin-RAnNaRxM.js → GitDiffViewerPlugin-Cksf3VZ-.js} +830 -86
  119. package/src/ui/dist/assets/{ImageViewerPlugin-DXJ0ZJGg.js → ImageViewerPlugin-CFz-OsTS.js} +5 -5
  120. package/src/ui/dist/assets/{LabCopilotPanel-BlO-sKsj.js → LabCopilotPanel-CJ1cJzoX.js} +10 -10
  121. package/src/ui/dist/assets/{LabPlugin-BajPZW5v.js → LabPlugin-BF3dVJwa.js} +1 -1
  122. package/src/ui/dist/assets/{LatexPlugin-F1OEol8D.js → LatexPlugin-DDkwZ6Sj.js} +7 -7
  123. package/src/ui/dist/assets/{MarkdownViewerPlugin-MhUupqwT.js → MarkdownViewerPlugin-HAuvurcT.js} +4 -4
  124. package/src/ui/dist/assets/{MarketplacePlugin-DxhIEsv0.js → MarketplacePlugin-BtoTYy2C.js} +3 -3
  125. package/src/ui/dist/assets/{index-B-2scqCJ.js → NotebookEditor-CSJYx7b-.js} +12 -155
  126. package/src/ui/dist/assets/{NotebookEditor-q7TkhewC.js → NotebookEditor-DQgRezm_.js} +1 -1
  127. package/src/ui/dist/assets/{PdfLoader-B8ZOTKFc.js → PdfLoader-DPa_-fv6.js} +1 -1
  128. package/src/ui/dist/assets/{PdfMarkdownPlugin-xFPvzvWh.js → PdfMarkdownPlugin-BZpXOEjm.js} +3 -3
  129. package/src/ui/dist/assets/{PdfViewerPlugin-EjEcsIB8.js → PdfViewerPlugin-BT8a6wGR.js} +10 -10
  130. package/src/ui/dist/assets/{SearchPlugin-ixY-1lgW.js → SearchPlugin-D_blveZi.js} +1 -1
  131. package/src/ui/dist/assets/{Stepper-gYFK2Pgz.js → Stepper-DH2k75Vo.js} +1 -1
  132. package/src/ui/dist/assets/{TextViewerPlugin-Cym6pv_n.js → TextViewerPlugin-Btx0M3hX.js} +4 -4
  133. package/src/ui/dist/assets/{VNCViewer-BPmIHcmK.js → VNCViewer-DImJO4rO.js} +9 -9
  134. package/src/ui/dist/assets/{bibtex-Btv6Wi7f.js → bibtex-B-Hqu0Sg.js} +1 -1
  135. package/src/ui/dist/assets/{code-BlG7g85c.js → code-BUfXGJSl.js} +1 -1
  136. package/src/ui/dist/assets/{file-content-DBT5OfTZ.js → file-content-VqamwI3X.js} +1 -1
  137. package/src/ui/dist/assets/{file-diff-panel-BWXYzqHk.js → file-diff-panel-C_wOoS7a.js} +1 -1
  138. package/src/ui/dist/assets/{file-socket-wDlx6byM.js → file-socket-D2bTuMVP.js} +1 -1
  139. package/src/ui/dist/assets/{file-utils-Ba3nJmH0.js → file-utils--zJCPN1i.js} +1 -1
  140. package/src/ui/dist/assets/{image-BwtCyguk.js → image-BZkGJ4mM.js} +1 -1
  141. package/src/ui/dist/assets/{index-CfRpE209.js → index-CxkvSeKw.js} +2 -2
  142. package/src/ui/dist/assets/{index-DcqvKzeJ.js → index-D9QIGcmc.js} +1 -1
  143. package/src/ui/dist/assets/{index-DpMZw8aM.css → index-DXZ1daiJ.css} +163 -34
  144. package/src/ui/dist/assets/index-DdRW6RMJ.js +159 -0
  145. package/src/ui/dist/assets/{index-Bz5AaWL7.js → index-DjggJovS.js} +2948 -1565
  146. package/src/ui/dist/assets/{message-square-BnlyWVH0.js → message-square-FUIPIhU2.js} +1 -1
  147. package/src/ui/dist/assets/{monaco-CXe0pAVe.js → monaco-DHMc7kKM.js} +1 -1
  148. package/src/ui/dist/assets/{popover-BCHmVhHj.js → popover-B85oCgCS.js} +1 -1
  149. package/src/ui/dist/assets/{project-sync-Brk6kaOD.js → project-sync-DOMCcPac.js} +1 -1
  150. package/src/ui/dist/assets/{sigma-D72eSUep.js → sigma-BO2rQrl3.js} +1 -1
  151. package/src/ui/dist/assets/{tooltip-BMWd0dqX.js → tooltip-B1OspAkx.js} +1 -1
  152. package/src/ui/dist/assets/{trash-BIt_eWIS.js → trash-BsVEH_dV.js} +1 -1
  153. package/src/ui/dist/assets/{useCliAccess-N1hkTRrR.js → useCliAccess-b8L6JuZm.js} +1 -1
  154. package/src/ui/dist/assets/{useFileDiffOverlay-DPRPv6rv.js → useFileDiffOverlay-BY7uA9hV.js} +1 -1
  155. package/src/ui/dist/assets/{wrap-text-E5-UheyP.js → wrap-text-BwyVuUIK.js} +1 -1
  156. package/src/ui/dist/assets/{zoom-out-D4TR-ZZ_.js → zoom-out-RDpLugQP.js} +1 -1
  157. package/src/ui/dist/index.html +5 -2
  158. /package/src/ui/dist/assets/{index-CccQYZjX.css → NotebookEditor-CccQYZjX.css} +0 -0
@@ -14,7 +14,7 @@ from ... import __version__ as DEEPSCIENTIST_VERSION
14
14
  from ...gitops import commit_detail, compare_refs, diff_file_between_refs, diff_file_for_commit, export_git_graph, list_branch_canvas, log_ref_history
15
15
  from ...memory import MemoryService
16
16
  from ...quest import QuestService
17
- from ...shared import generate_id, read_text, resolve_within, sha256_text, utc_now
17
+ from ...shared import generate_id, read_json, read_text, resolve_within, run_command, sha256_text, utc_now
18
18
  from ...runners import RunRequest
19
19
 
20
20
 
@@ -171,9 +171,9 @@ npm --prefix src/ui run build</pre>
171
171
 
172
172
  def cli_health(self) -> dict:
173
173
  online_channels = [
174
- channel.status()
175
- for channel in self.app.channels.values()
176
- if channel.status().get("enabled") is not False
174
+ snapshot
175
+ for snapshot in self.app.list_connector_statuses()
176
+ if snapshot.get("enabled") is not False
177
177
  ]
178
178
  return {
179
179
  "status": "ok",
@@ -220,6 +220,9 @@ npm --prefix src/ui run build</pre>
220
220
  def qq_inbound(self, body: dict) -> dict:
221
221
  return self.app.handle_qq_inbound(body)
222
222
 
223
+ def connector_profile_delete(self, connector: str, profile_id: str) -> dict | tuple[int, dict]:
224
+ return self.app.delete_connector_profile(connector, profile_id)
225
+
223
226
  def connector_inbound(self, connector: str, body: dict) -> dict:
224
227
  return self.app.handle_connector_inbound(connector, body)
225
228
 
@@ -245,12 +248,15 @@ npm --prefix src/ui run build</pre>
245
248
  else []
246
249
  )
247
250
  force_connector_rebind_raw = body.get("force_connector_rebind")
248
- force_connector_rebind = bool(force_connector_rebind_raw) and str(force_connector_rebind_raw).strip().lower() not in {
249
- "0",
250
- "false",
251
- "no",
252
- "off",
253
- }
251
+ if force_connector_rebind_raw is None:
252
+ force_connector_rebind = True
253
+ else:
254
+ force_connector_rebind = bool(force_connector_rebind_raw) and str(force_connector_rebind_raw).strip().lower() not in {
255
+ "0",
256
+ "false",
257
+ "no",
258
+ "off",
259
+ }
254
260
  requested_baseline_ref = body.get("requested_baseline_ref")
255
261
  startup_contract = body.get("startup_contract")
256
262
  auto_start = body.get("auto_start") is True
@@ -642,8 +648,6 @@ npm --prefix src/ui run build</pre>
642
648
  session = self.app.bash_exec_service.get_session(quest_root, session_id)
643
649
  except FileNotFoundError:
644
650
  return 404, {"ok": False, "message": f"Unknown terminal session `{session_id}`."}
645
- if str(session.get("kind") or "").lower() != "terminal":
646
- return 400, {"ok": False, "message": "not_terminal_session"}
647
651
  if str(session.get("status") or "").lower() in {"completed", "failed", "terminated"}:
648
652
  return 409, {"ok": False, "message": "terminal_session_inactive", "session": session}
649
653
  try:
@@ -744,6 +748,12 @@ npm --prefix src/ui run build</pre>
744
748
  def git_branches(self, quest_id: str) -> dict:
745
749
  quest_root = self._fresh_quest_service()._quest_root(quest_id)
746
750
  payload = list_branch_canvas(quest_root, quest_id=quest_id)
751
+ research_state = self.app.quest_service.read_research_state(quest_root)
752
+ active_workspace_branch = str(research_state.get("current_workspace_branch") or "").strip() or None
753
+ research_head_branch = str(research_state.get("research_head_branch") or "").strip() or None
754
+ payload["active_workspace_ref"] = active_workspace_branch
755
+ payload["research_head_ref"] = research_head_branch
756
+ payload["workspace_mode"] = str(research_state.get("workspace_mode") or "quest").strip() or "quest"
747
757
  try:
748
758
  branch_summary = self.app.artifact_service.list_research_branches(quest_root)
749
759
  except Exception:
@@ -758,6 +768,8 @@ npm --prefix src/ui run build</pre>
758
768
  if not ref:
759
769
  continue
760
770
  summary = branch_summary_by_name.get(ref)
771
+ node["active_workspace"] = ref == active_workspace_branch
772
+ node["research_head"] = ref == research_head_branch
761
773
  if not isinstance(summary, dict):
762
774
  continue
763
775
  node["branch_no"] = summary.get("branch_no")
@@ -815,6 +827,126 @@ npm --prefix src/ui run build</pre>
815
827
  quest_root = self._fresh_quest_service()._quest_root(quest_id)
816
828
  return diff_file_between_refs(quest_root, base=base, head=head, path=file_path)
817
829
 
830
+ def file_change_diff(self, quest_id: str, path: str) -> dict:
831
+ query = self.parse_query(path)
832
+ run_id = ((query.get("run_id") or [""])[0] or "").strip()
833
+ event_id = ((query.get("event_id") or [""])[0] or "").strip() or None
834
+ raw_path = ((query.get("path") or [""])[0] or "").strip()
835
+ if not run_id or not raw_path:
836
+ return self._file_change_diff_unavailable(
837
+ raw_path=raw_path,
838
+ run_id=run_id or None,
839
+ event_id=event_id,
840
+ message="`run_id` and `path` are required.",
841
+ ok=False,
842
+ )
843
+
844
+ quest_root = self._fresh_quest_service()._quest_root(quest_id)
845
+ run_artifact = read_json(quest_root / ".ds" / "runs" / run_id / "artifact.json", default={})
846
+ if not isinstance(run_artifact, dict):
847
+ return self._file_change_diff_unavailable(
848
+ raw_path=raw_path,
849
+ run_id=run_id,
850
+ event_id=event_id,
851
+ message="Historical patch unavailable. Run artifact metadata is missing.",
852
+ ok=False,
853
+ )
854
+
855
+ record = run_artifact.get("record") if isinstance(run_artifact.get("record"), dict) else {}
856
+ checkpoint = run_artifact.get("checkpoint") if isinstance(run_artifact.get("checkpoint"), dict) else {}
857
+ base = str(record.get("head_commit") or "").strip()
858
+ head = str(checkpoint.get("head") or "").strip()
859
+ branch = str(record.get("branch") or "").strip() or None
860
+ workspace_root = self._file_change_workspace_root(run_artifact, record)
861
+ relative_path = None
862
+ display_path = None
863
+
864
+ if not base or not head:
865
+ relative_path = self._relative_file_change_path(None, raw_path, workspace_root)
866
+ display_path = self._display_file_change_path(quest_root, raw_path, relative_path=relative_path)
867
+ return self._file_change_diff_unavailable(
868
+ raw_path=raw_path,
869
+ run_id=run_id,
870
+ event_id=event_id,
871
+ base=base,
872
+ head=head,
873
+ branch=branch,
874
+ relative_path=relative_path,
875
+ display_path=display_path,
876
+ message="Historical patch unavailable. Run artifact metadata is missing the recorded commit range.",
877
+ ok=True,
878
+ )
879
+
880
+ repo_root = self._resolve_git_repo_root_for_file_change(raw_path, workspace_root)
881
+ relative_path = self._relative_file_change_path(repo_root, raw_path, workspace_root)
882
+ display_path = self._display_file_change_path(quest_root, raw_path, relative_path=relative_path)
883
+
884
+ if repo_root is None or not relative_path:
885
+ return self._file_change_diff_unavailable(
886
+ raw_path=raw_path,
887
+ run_id=run_id,
888
+ event_id=event_id,
889
+ base=base,
890
+ head=head,
891
+ branch=branch,
892
+ relative_path=relative_path,
893
+ display_path=display_path,
894
+ message="Historical patch unavailable. DeepScientist could not map this file to a git worktree.",
895
+ ok=True,
896
+ )
897
+
898
+ try:
899
+ diff = diff_file_between_refs(repo_root, base=base, head=head, path=relative_path)
900
+ except Exception:
901
+ return self._file_change_diff_unavailable(
902
+ raw_path=raw_path,
903
+ run_id=run_id,
904
+ event_id=event_id,
905
+ base=base,
906
+ head=head,
907
+ branch=branch,
908
+ relative_path=relative_path,
909
+ display_path=display_path,
910
+ message=(
911
+ "Historical patch unavailable. The saved run checkpoint does not match the git repository "
912
+ "that currently owns this file."
913
+ ),
914
+ ok=True,
915
+ )
916
+
917
+ if (
918
+ not diff.get("binary")
919
+ and not diff.get("lines")
920
+ and int(diff.get("added") or 0) == 0
921
+ and int(diff.get("removed") or 0) == 0
922
+ ):
923
+ return self._file_change_diff_unavailable(
924
+ raw_path=raw_path,
925
+ run_id=run_id,
926
+ event_id=event_id,
927
+ base=base,
928
+ head=head,
929
+ branch=branch,
930
+ relative_path=relative_path,
931
+ display_path=display_path,
932
+ message=(
933
+ "Historical patch unavailable. This event only preserved file-level metadata or the edit "
934
+ "did not land in the run's final checkpoint."
935
+ ),
936
+ ok=True,
937
+ )
938
+
939
+ return {
940
+ **diff,
941
+ "available": True,
942
+ "source": "run_range",
943
+ "display_path": display_path or relative_path,
944
+ "run_id": run_id,
945
+ "event_id": event_id,
946
+ "branch": branch,
947
+ "message": None,
948
+ }
949
+
818
950
  def git_commit_file(self, quest_id: str, path: str) -> dict:
819
951
  query = self.parse_query(path)
820
952
  sha = ((query.get("sha") or [""])[0] or "").strip()
@@ -1188,7 +1320,7 @@ npm --prefix src/ui run build</pre>
1188
1320
  def run_create(self, quest_id: str, body: dict) -> dict:
1189
1321
  quest_root = self.app.quest_service._quest_root(quest_id)
1190
1322
  config = self.app.config_manager.load_named("config")
1191
- runners = self.app.config_manager.load_named("runners")
1323
+ runners = self.app.config_manager.load_runners_config()
1192
1324
  snapshot = self.app.quest_service.snapshot(quest_id)
1193
1325
  runner_name = str(body.get("runner") or snapshot.get("runner") or config.get("default_runner", "codex")).strip().lower()
1194
1326
  runner_cfg = runners.get(runner_name, {})
@@ -1204,6 +1336,16 @@ npm --prefix src/ui run build</pre>
1204
1336
  "ok": False,
1205
1337
  "message": str(exc),
1206
1338
  }
1339
+ raw_reasoning_effort = (
1340
+ body.get("model_reasoning_effort")
1341
+ if "model_reasoning_effort" in body
1342
+ else runner_cfg.get("model_reasoning_effort")
1343
+ )
1344
+ reasoning_effort = (
1345
+ str(raw_reasoning_effort).strip()
1346
+ if raw_reasoning_effort is not None and str(raw_reasoning_effort).strip()
1347
+ else ("xhigh" if raw_reasoning_effort is None else None)
1348
+ )
1207
1349
  request = RunRequest(
1208
1350
  quest_id=quest_id,
1209
1351
  quest_root=quest_root,
@@ -1215,8 +1357,7 @@ npm --prefix src/ui run build</pre>
1215
1357
  approval_policy=runner_cfg.get("approval_policy", "on-request"),
1216
1358
  sandbox_mode=runner_cfg.get("sandbox_mode", "workspace-write"),
1217
1359
  turn_reason=body.get("turn_reason") or "user_message",
1218
- reasoning_effort=body.get("model_reasoning_effort")
1219
- or runner_cfg.get("model_reasoning_effort", "xhigh"),
1360
+ reasoning_effort=reasoning_effort,
1220
1361
  )
1221
1362
  result = runner.run(request)
1222
1363
  if result.output_text:
@@ -1352,8 +1493,12 @@ npm --prefix src/ui run build</pre>
1352
1493
  result = self.app.config_manager.save_named_payload(name, body["structured"])
1353
1494
  else:
1354
1495
  result = self.app.config_manager.save_named_text(name, body.get("content", ""))
1496
+ if result.get("ok") and name == "config":
1497
+ result["runtime_reload"] = self.app.reload_runtime_config()
1355
1498
  if result.get("ok") and name == "connectors":
1356
1499
  result["runtime_reload"] = self.app.reload_connectors_config()
1500
+ if result.get("ok") and name == "runners":
1501
+ result["runtime_reload"] = self.app.reload_runners_config()
1357
1502
  return result
1358
1503
 
1359
1504
  def config_validate(self, body: dict | None = None) -> dict:
@@ -1406,6 +1551,136 @@ npm --prefix src/ui run build</pre>
1406
1551
  return {}
1407
1552
  return json.loads(raw.decode("utf-8"))
1408
1553
 
1554
+ @staticmethod
1555
+ def _file_change_workspace_root(run_artifact: dict, record: dict) -> Path | None:
1556
+ workspace_root = str(run_artifact.get("workspace_root") or record.get("workspace_root") or "").strip()
1557
+ if not workspace_root:
1558
+ return None
1559
+ return Path(workspace_root).expanduser()
1560
+
1561
+ @staticmethod
1562
+ def _file_change_path_candidates(raw_path: str, workspace_root: Path | None) -> list[Path]:
1563
+ text = str(raw_path or "").strip()
1564
+ if not text:
1565
+ return []
1566
+ raw_candidate = Path(text).expanduser()
1567
+ candidates: list[Path] = []
1568
+ if raw_candidate.is_absolute():
1569
+ candidates.append(raw_candidate)
1570
+ elif workspace_root is not None:
1571
+ candidates.append(workspace_root / raw_candidate)
1572
+ else:
1573
+ candidates.append(raw_candidate)
1574
+ if workspace_root is not None:
1575
+ candidates.append(workspace_root)
1576
+
1577
+ resolved: list[Path] = []
1578
+ seen: set[str] = set()
1579
+ for candidate in candidates:
1580
+ for variant in (candidate, candidate.resolve(strict=False)):
1581
+ key = str(variant)
1582
+ if key in seen:
1583
+ continue
1584
+ seen.add(key)
1585
+ resolved.append(variant)
1586
+ return resolved
1587
+
1588
+ @staticmethod
1589
+ def _git_probe_root(candidate: Path) -> Path:
1590
+ if candidate.exists():
1591
+ return candidate if candidate.is_dir() else candidate.parent
1592
+ for parent in candidate.parents:
1593
+ if parent.exists():
1594
+ return parent
1595
+ return candidate.parent
1596
+
1597
+ def _resolve_git_repo_root_for_file_change(self, raw_path: str, workspace_root: Path | None) -> Path | None:
1598
+ for candidate in self._file_change_path_candidates(raw_path, workspace_root):
1599
+ probe_root = self._git_probe_root(candidate)
1600
+ if not str(probe_root).strip():
1601
+ continue
1602
+ result = run_command(["git", "rev-parse", "--show-toplevel"], cwd=probe_root, check=False)
1603
+ if result.returncode != 0:
1604
+ continue
1605
+ top_level = result.stdout.strip()
1606
+ if top_level:
1607
+ return Path(top_level).resolve()
1608
+ return None
1609
+
1610
+ def _relative_file_change_path(self, repo_root: Path | None, raw_path: str, workspace_root: Path | None) -> str | None:
1611
+ if repo_root is None:
1612
+ if Path(str(raw_path or "").strip()).is_absolute():
1613
+ return None
1614
+ normalized = str(raw_path or "").strip().replace("\\", "/").lstrip("/")
1615
+ return normalized or None
1616
+
1617
+ repo_root_resolved = repo_root.resolve()
1618
+ for candidate in self._file_change_path_candidates(raw_path, workspace_root):
1619
+ try:
1620
+ return candidate.relative_to(repo_root_resolved).as_posix()
1621
+ except ValueError:
1622
+ continue
1623
+ if Path(str(raw_path or "").strip()).is_absolute():
1624
+ return None
1625
+ normalized = str(raw_path or "").strip().replace("\\", "/").lstrip("/")
1626
+ return normalized or None
1627
+
1628
+ @staticmethod
1629
+ def _display_file_change_path(quest_root: Path, raw_path: str, *, relative_path: str | None = None) -> str:
1630
+ quest_root_resolved = quest_root.resolve()
1631
+ for candidate in [Path(str(raw_path or "").strip()).expanduser()]:
1632
+ for variant in (candidate, candidate.resolve(strict=False)):
1633
+ try:
1634
+ relative = variant.relative_to(quest_root_resolved)
1635
+ except ValueError:
1636
+ continue
1637
+ parts = relative.parts
1638
+ if len(parts) >= 3 and parts[0] == ".ds" and parts[1] == "worktrees":
1639
+ branch_root = parts[2]
1640
+ remainder = Path(*parts[3:]).as_posix() if len(parts) > 3 else ""
1641
+ return f"{branch_root}/{remainder}" if remainder else branch_root
1642
+ return relative.as_posix()
1643
+ if relative_path:
1644
+ return relative_path
1645
+ text = str(raw_path or "").strip()
1646
+ return Path(text).name or text
1647
+
1648
+ @staticmethod
1649
+ def _file_change_diff_unavailable(
1650
+ *,
1651
+ raw_path: str,
1652
+ run_id: str | None,
1653
+ event_id: str | None,
1654
+ message: str,
1655
+ ok: bool,
1656
+ base: str = "",
1657
+ head: str = "",
1658
+ branch: str | None = None,
1659
+ relative_path: str | None = None,
1660
+ display_path: str | None = None,
1661
+ ) -> dict:
1662
+ normalized_path = relative_path or str(raw_path or "").strip()
1663
+ return {
1664
+ "ok": ok,
1665
+ "available": False,
1666
+ "source": "unavailable",
1667
+ "run_id": run_id,
1668
+ "event_id": event_id,
1669
+ "branch": branch,
1670
+ "display_path": display_path or normalized_path,
1671
+ "base": base,
1672
+ "head": head,
1673
+ "path": normalized_path,
1674
+ "old_path": None,
1675
+ "status": "modified",
1676
+ "binary": False,
1677
+ "added": 0,
1678
+ "removed": 0,
1679
+ "lines": [],
1680
+ "truncated": False,
1681
+ "message": message,
1682
+ }
1683
+
1409
1684
  @staticmethod
1410
1685
  def _markdown_title(path: Path) -> str:
1411
1686
  content = read_text(path)
@@ -17,6 +17,7 @@ ROUTES: list[tuple[str, re.Pattern[str], str]] = [
17
17
  ("GET", re.compile(r"^/api/connectors/availability$"), "connectors_availability"),
18
18
  ("GET", re.compile(r"^/api/connectors/qq/bindings$"), "qq_bindings"),
19
19
  ("POST", re.compile(r"^/api/connectors/qq/inbound$"), "qq_inbound"),
20
+ ("DELETE", re.compile(r"^/api/connectors/(?P<connector>[^/]+)/profiles/(?P<profile_id>[^/]+)$"), "connector_profile_delete"),
20
21
  ("GET", re.compile(r"^/api/connectors/(?P<connector>[^/]+)/bindings$"), "connector_bindings"),
21
22
  ("POST", re.compile(r"^/api/connectors/(?P<connector>[^/]+)/inbound$"), "connector_inbound"),
22
23
  ("GET", re.compile(r"^/api/baselines$"), "baselines"),
@@ -59,6 +60,7 @@ ROUTES: list[tuple[str, re.Pattern[str], str]] = [
59
60
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/commit$"), "git_commit"),
60
61
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/diff-file$"), "git_diff_file"),
61
62
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/git/commit-file$"), "git_commit_file"),
63
+ ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/operations/file-change-diff$"), "file_change_diff"),
62
64
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/runs$"), "runs"),
63
65
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/memory$"), "quest_memory"),
64
66
  ("GET", re.compile(r"^/api/quests/(?P<quest_id>[^/]+)/documents$"), "documents"),