@researai/deepscientist 1.5.14 → 1.5.16

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 (225) hide show
  1. package/README.md +336 -90
  2. package/assets/branding/logo-raster.png +0 -0
  3. package/bin/ds.js +816 -131
  4. package/docs/en/00_QUICK_START.md +36 -15
  5. package/docs/en/01_SETTINGS_REFERENCE.md +53 -4
  6. package/docs/en/02_START_RESEARCH_GUIDE.md +7 -0
  7. package/docs/en/03_QQ_CONNECTOR_GUIDE.md +19 -0
  8. package/docs/en/05_TUI_GUIDE.md +6 -0
  9. package/docs/en/06_RUNTIME_AND_CANVAS.md +4 -3
  10. package/docs/en/09_DOCTOR.md +11 -5
  11. package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
  12. package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +65 -13
  13. package/docs/en/15_CODEX_PROVIDER_SETUP.md +25 -8
  14. package/docs/en/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
  15. package/docs/en/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
  16. package/docs/en/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
  17. package/docs/en/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  18. package/docs/en/19_LOCAL_BROWSER_AUTH.md +70 -0
  19. package/docs/en/20_WORKSPACE_MODES_GUIDE.md +250 -0
  20. package/docs/en/README.md +24 -0
  21. package/docs/zh/00_QUICK_START.md +36 -15
  22. package/docs/zh/01_SETTINGS_REFERENCE.md +53 -4
  23. package/docs/zh/02_START_RESEARCH_GUIDE.md +7 -0
  24. package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +19 -0
  25. package/docs/zh/05_TUI_GUIDE.md +6 -0
  26. package/docs/zh/09_DOCTOR.md +11 -5
  27. package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
  28. package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +65 -13
  29. package/docs/zh/15_CODEX_PROVIDER_SETUP.md +25 -8
  30. package/docs/zh/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
  31. package/docs/zh/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
  32. package/docs/zh/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
  33. package/docs/zh/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
  34. package/docs/zh/19_LOCAL_BROWSER_AUTH.md +68 -0
  35. package/docs/zh/20_WORKSPACE_MODES_GUIDE.md +251 -0
  36. package/docs/zh/README.md +24 -0
  37. package/install.sh +2 -0
  38. package/package.json +1 -1
  39. package/pyproject.toml +1 -1
  40. package/src/deepscientist/__init__.py +1 -1
  41. package/src/deepscientist/acp/envelope.py +6 -0
  42. package/src/deepscientist/artifact/charts.py +567 -0
  43. package/src/deepscientist/artifact/guidance.py +50 -10
  44. package/src/deepscientist/artifact/metrics.py +228 -5
  45. package/src/deepscientist/artifact/schemas.py +3 -0
  46. package/src/deepscientist/artifact/service.py +4276 -308
  47. package/src/deepscientist/bash_exec/models.py +23 -0
  48. package/src/deepscientist/bash_exec/monitor.py +147 -67
  49. package/src/deepscientist/bash_exec/runtime.py +218 -156
  50. package/src/deepscientist/bash_exec/service.py +309 -69
  51. package/src/deepscientist/bash_exec/shells.py +87 -0
  52. package/src/deepscientist/bridges/connectors.py +51 -2
  53. package/src/deepscientist/cli.py +115 -19
  54. package/src/deepscientist/codex_cli_compat.py +232 -0
  55. package/src/deepscientist/config/models.py +8 -4
  56. package/src/deepscientist/config/service.py +38 -11
  57. package/src/deepscientist/connector/weixin_support.py +122 -1
  58. package/src/deepscientist/daemon/api/handlers.py +199 -9
  59. package/src/deepscientist/daemon/api/router.py +5 -0
  60. package/src/deepscientist/daemon/app.py +1458 -289
  61. package/src/deepscientist/doctor.py +51 -0
  62. package/src/deepscientist/file_lock.py +48 -0
  63. package/src/deepscientist/gitops/__init__.py +10 -1
  64. package/src/deepscientist/gitops/diff.py +296 -1
  65. package/src/deepscientist/gitops/service.py +4 -1
  66. package/src/deepscientist/mcp/server.py +212 -5
  67. package/src/deepscientist/process_control.py +161 -0
  68. package/src/deepscientist/prompts/builder.py +501 -453
  69. package/src/deepscientist/quest/layout.py +15 -2
  70. package/src/deepscientist/quest/service.py +2539 -195
  71. package/src/deepscientist/quest/stage_views.py +177 -1
  72. package/src/deepscientist/runners/base.py +2 -0
  73. package/src/deepscientist/runners/codex.py +169 -31
  74. package/src/deepscientist/runners/runtime_overrides.py +17 -1
  75. package/src/deepscientist/skills/__init__.py +2 -2
  76. package/src/deepscientist/skills/installer.py +196 -5
  77. package/src/deepscientist/skills/registry.py +66 -0
  78. package/src/prompts/connectors/qq.md +18 -8
  79. package/src/prompts/connectors/weixin.md +16 -6
  80. package/src/prompts/contracts/shared_interaction.md +24 -4
  81. package/src/prompts/system.md +921 -72
  82. package/src/prompts/system_copilot.md +43 -0
  83. package/src/skills/analysis-campaign/SKILL.md +32 -2
  84. package/src/skills/analysis-campaign/references/artifact-orchestration.md +1 -1
  85. package/src/skills/analysis-campaign/references/writing-facing-slice-examples.md +65 -0
  86. package/src/skills/baseline/SKILL.md +10 -0
  87. package/src/skills/decision/SKILL.md +27 -2
  88. package/src/skills/experiment/SKILL.md +16 -2
  89. package/src/skills/figure-polish/SKILL.md +1 -0
  90. package/src/skills/finalize/SKILL.md +19 -0
  91. package/src/skills/idea/SKILL.md +79 -0
  92. package/src/skills/idea/references/idea-generation-playbook.md +100 -0
  93. package/src/skills/idea/references/outline-seeding-example.md +60 -0
  94. package/src/skills/intake-audit/SKILL.md +9 -1
  95. package/src/skills/mentor/SKILL.md +217 -0
  96. package/src/skills/mentor/references/correction-rules.md +210 -0
  97. package/src/skills/mentor/references/knowledge-profile.md +91 -0
  98. package/src/skills/mentor/references/persona-profile.md +138 -0
  99. package/src/skills/mentor/references/taste-profile.md +128 -0
  100. package/src/skills/mentor/references/thought-style-profile.md +138 -0
  101. package/src/skills/mentor/references/work-profile.md +289 -0
  102. package/src/skills/mentor/references/workflow-profile.md +240 -0
  103. package/src/skills/optimize/SKILL.md +1645 -0
  104. package/src/skills/rebuttal/SKILL.md +3 -1
  105. package/src/skills/review/SKILL.md +3 -1
  106. package/src/skills/scout/SKILL.md +8 -0
  107. package/src/skills/write/SKILL.md +81 -12
  108. package/src/skills/write/references/outline-evidence-contract-example.md +107 -0
  109. package/src/tui/dist/app/AppContainer.js +22 -11
  110. package/src/tui/dist/index.js +4 -1
  111. package/src/tui/dist/lib/api.js +33 -3
  112. package/src/tui/package.json +1 -1
  113. package/src/ui/dist/assets/AiManusChatView-COFACy7V.js +204 -0
  114. package/src/ui/dist/assets/AnalysisPlugin-DnSm0GZn.js +1 -0
  115. package/src/ui/dist/assets/CliPlugin-CvwCmDQ5.js +109 -0
  116. package/src/ui/dist/assets/CodeEditorPlugin-cOqSa0xq.js +2 -0
  117. package/src/ui/dist/assets/CodeViewerPlugin-itb0tltR.js +270 -0
  118. package/src/ui/dist/assets/DocViewerPlugin-DqKkiCI6.js +7 -0
  119. package/src/ui/dist/assets/GitCommitViewerPlugin-DVgNHBCS.js +1 -0
  120. package/src/ui/dist/assets/GitDiffViewerPlugin-DxL2ezFG.js +6 -0
  121. package/src/ui/dist/assets/GitSnapshotViewer-B_RQm1YZ.js +30 -0
  122. package/src/ui/dist/assets/ImageViewerPlugin-tHqlXY3n.js +26 -0
  123. package/src/ui/dist/assets/LabCopilotPanel-ClMbq5Yu.js +14 -0
  124. package/src/ui/dist/assets/LabPlugin-L_SuE8ow.js +22 -0
  125. package/src/ui/dist/assets/LatexPlugin-B495DTXC.js +25 -0
  126. package/src/ui/dist/assets/MarkdownViewerPlugin-DG28-61B.js +128 -0
  127. package/src/ui/dist/assets/MarketplacePlugin-BiOGT-Kj.js +13 -0
  128. package/src/ui/dist/assets/{NotebookEditor-CccQYZjX.css → NotebookEditor-BHH8rdGj.css} +1 -1
  129. package/src/ui/dist/assets/NotebookEditor-BOr3x3Ej.css +1 -0
  130. package/src/ui/dist/assets/NotebookEditor-C-4Kt1p9.js +81 -0
  131. package/src/ui/dist/assets/NotebookEditor-CVsj8h_T.js +361 -0
  132. package/src/ui/dist/assets/PdfLoader-CASDQmxJ.js +16 -0
  133. package/src/ui/dist/assets/PdfLoader-Cy5jtWrr.css +1 -0
  134. package/src/ui/dist/assets/PdfMarkdownPlugin-BFhwoKsY.js +1 -0
  135. package/src/ui/dist/assets/PdfViewerPlugin-DcOzU9vd.js +17 -0
  136. package/src/ui/dist/assets/PdfViewerPlugin-nwwE-fjJ.css +1 -0
  137. package/src/ui/dist/assets/SearchPlugin-CHj7M58O.js +16 -0
  138. package/src/ui/dist/assets/SearchPlugin-DA4en4hK.css +1 -0
  139. package/src/ui/dist/assets/TextViewerPlugin-CB4DYfWO.js +54 -0
  140. package/src/ui/dist/assets/VNCViewer-CjlbyCB3.js +11 -0
  141. package/src/ui/dist/assets/bot-CFkZY-JP.js +6 -0
  142. package/src/ui/dist/assets/browser-CTB2jwNe.js +8 -0
  143. package/src/ui/dist/assets/chevron-up-Dq5ofbht.js +6 -0
  144. package/src/ui/dist/assets/code-DLC6G24T.js +6 -0
  145. package/src/ui/dist/assets/file-content-Dv4LoZec.js +1 -0
  146. package/src/ui/dist/assets/file-diff-panel-Denq-lC3.js +1 -0
  147. package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +1 -0
  148. package/src/ui/dist/assets/file-socket-Cu4Qln7Y.js +1 -0
  149. package/src/ui/dist/assets/git-commit-horizontal-BUh6G52n.js +6 -0
  150. package/src/ui/dist/assets/image-B9HUUddG.js +6 -0
  151. package/src/ui/dist/assets/index-B2B1sg-M.js +1 -0
  152. package/src/ui/dist/assets/index-Cgla8biy.css +33 -0
  153. package/src/ui/dist/assets/index-DRyx7vAc.js +1 -0
  154. package/src/ui/dist/assets/index-Gbl53BNp.js +2496 -0
  155. package/src/ui/dist/assets/index-wQ7RIIRd.js +11 -0
  156. package/src/ui/dist/assets/monaco-CiHMMNH_.js +1 -0
  157. package/src/ui/dist/assets/pdf-effect-queue-ZtnHFCAi.js +6 -0
  158. package/src/ui/dist/assets/plugin-monaco-C8UgLomw.js +19 -0
  159. package/src/ui/dist/assets/plugin-notebook-HbW2K-1c.js +169 -0
  160. package/src/ui/dist/assets/plugin-pdf-CR8hgQBV.js +357 -0
  161. package/src/ui/dist/assets/plugin-terminal-MXFIPun8.js +227 -0
  162. package/src/ui/dist/assets/popover-DL6h35vr.js +1 -0
  163. package/src/ui/dist/assets/project-sync-CsX08Qno.js +1 -0
  164. package/src/ui/dist/assets/select-DvmXt1yY.js +11 -0
  165. package/src/ui/dist/assets/sigma-7jpXazui.js +6 -0
  166. package/src/ui/dist/assets/trash-xA7kFt8i.js +11 -0
  167. package/src/ui/dist/assets/useCliAccess-DsMwDjOp.js +1 -0
  168. package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +1 -0
  169. package/src/ui/dist/assets/wrap-text-CwMn-iqb.js +11 -0
  170. package/src/ui/dist/assets/zoom-out-R-GWEhzS.js +11 -0
  171. package/src/ui/dist/index.html +5 -2
  172. package/src/ui/dist/assets/AiManusChatView-DaF9Nge_.js +0 -26597
  173. package/src/ui/dist/assets/AnalysisPlugin-BSVx6dXE.js +0 -123
  174. package/src/ui/dist/assets/CliPlugin-C9gzJX41.js +0 -5905
  175. package/src/ui/dist/assets/CodeEditorPlugin-DU9G0Tox.js +0 -427
  176. package/src/ui/dist/assets/CodeViewerPlugin-DoX_fI9l.js +0 -905
  177. package/src/ui/dist/assets/DocViewerPlugin-C4FWIXuU.js +0 -278
  178. package/src/ui/dist/assets/GitDiffViewerPlugin-BgfFMgtf.js +0 -2661
  179. package/src/ui/dist/assets/ImageViewerPlugin-tcPkfY_x.js +0 -500
  180. package/src/ui/dist/assets/LabCopilotPanel-_dKV60Bf.js +0 -4104
  181. package/src/ui/dist/assets/LabPlugin-Bje0ayoC.js +0 -2677
  182. package/src/ui/dist/assets/LatexPlugin-CVsBzAln.js +0 -1792
  183. package/src/ui/dist/assets/MarkdownViewerPlugin-xjmrqv_8.js +0 -308
  184. package/src/ui/dist/assets/MarketplacePlugin-mMM2A8wP.js +0 -413
  185. package/src/ui/dist/assets/NotebookEditor-3kVDSOBo.js +0 -4214
  186. package/src/ui/dist/assets/NotebookEditor-C3VQ7ylN.css +0 -1405
  187. package/src/ui/dist/assets/NotebookEditor-SoJ8X-MO.js +0 -84873
  188. package/src/ui/dist/assets/PdfLoader-C-Y707R3.css +0 -49
  189. package/src/ui/dist/assets/PdfLoader-DElVuHl9.js +0 -25468
  190. package/src/ui/dist/assets/PdfMarkdownPlugin-Bq88XT4G.js +0 -409
  191. package/src/ui/dist/assets/PdfViewerPlugin-CsCXMo9S.js +0 -3095
  192. package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +0 -3627
  193. package/src/ui/dist/assets/SearchPlugin-DDMrGDkh.css +0 -379
  194. package/src/ui/dist/assets/SearchPlugin-oUPvy19k.js +0 -741
  195. package/src/ui/dist/assets/TextViewerPlugin-CRkT9yNy.js +0 -472
  196. package/src/ui/dist/assets/VNCViewer-BgbuvWhR.js +0 -18821
  197. package/src/ui/dist/assets/awareness-C0NPR2Dj.js +0 -292
  198. package/src/ui/dist/assets/bot-v_RASACv.js +0 -21
  199. package/src/ui/dist/assets/browser-BAcuE0Xj.js +0 -2895
  200. package/src/ui/dist/assets/code-5hC9d0VH.js +0 -17
  201. package/src/ui/dist/assets/file-content-D1PxfOrp.js +0 -377
  202. package/src/ui/dist/assets/file-diff-panel-DG1oT_Hj.js +0 -92
  203. package/src/ui/dist/assets/file-jump-queue-r5XKgJEV.js +0 -16
  204. package/src/ui/dist/assets/file-socket-BmdFYQlk.js +0 -58
  205. package/src/ui/dist/assets/function-B5QZkkHC.js +0 -1895
  206. package/src/ui/dist/assets/image-Dqe2X2tW.js +0 -18
  207. package/src/ui/dist/assets/index-BQG-1s2o.css +0 -12553
  208. package/src/ui/dist/assets/index-DVsMKK_y.js +0 -25
  209. package/src/ui/dist/assets/index-Duvz8Ip0.js +0 -159
  210. package/src/ui/dist/assets/index-Nt9hS4ck.js +0 -244829
  211. package/src/ui/dist/assets/index-RDlNXXx1.js +0 -120
  212. package/src/ui/dist/assets/monaco-DIXge1CP.js +0 -623
  213. package/src/ui/dist/assets/pdf-effect-queue-BBTTQaO-.js +0 -47
  214. package/src/ui/dist/assets/pdf_viewer-e0g1is2C.js +0 -8206
  215. package/src/ui/dist/assets/popover-BWlolyxo.js +0 -476
  216. package/src/ui/dist/assets/project-sync-BM5PkFH4.js +0 -297
  217. package/src/ui/dist/assets/select-D4dAtrA8.js +0 -1690
  218. package/src/ui/dist/assets/sigma-CKbE5jJT.js +0 -22
  219. package/src/ui/dist/assets/square-check-big-CZNGMgiB.js +0 -17
  220. package/src/ui/dist/assets/trash-DaB37xAz.js +0 -32
  221. package/src/ui/dist/assets/useCliAccess-C2OmAcWe.js +0 -957
  222. package/src/ui/dist/assets/useFileDiffOverlay-Dowd1Ij4.js +0 -53
  223. package/src/ui/dist/assets/wrap-text-BGjAhAUq.js +0 -35
  224. package/src/ui/dist/assets/yjs-DncrqiZ8.js +0 -11243
  225. package/src/ui/dist/assets/zoom-out-dMZQMXzc.js +0 -34
@@ -3,9 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  import os
5
5
  import re
6
- import shlex
7
6
  import shutil
8
- import signal
9
7
  import subprocess
10
8
  import sys
11
9
  import tempfile
@@ -17,7 +15,9 @@ from pathlib import Path
17
15
  from typing import Any
18
16
 
19
17
  from ..mcp.context import McpContext
18
+ from ..process_control import is_process_alive, process_session_popen_kwargs, terminate_process_ids
20
19
  from ..shared import append_jsonl, ensure_dir, generate_id, iter_jsonl, read_json, read_jsonl, read_jsonl_tail, utc_now
20
+ from .shells import build_exec_shell_launch, build_terminal_shell_launch
21
21
  from .runtime import TerminalRuntimeManager
22
22
 
23
23
  BASH_STATUS_MARKER_PREFIX = "__DS_BASH_STATUS__"
@@ -32,6 +32,8 @@ DEFAULT_POLL_INTERVAL_SECONDS = 0.35
32
32
  TERMINAL_STATUSES = {"completed", "failed", "terminated"}
33
33
  DEFAULT_TERMINAL_SESSION_ID = "terminal-main"
34
34
  BASH_WATCHDOG_AFTER_SECONDS = 1800
35
+ SUMMARY_RECENT_SESSION_LIMIT = 256
36
+ SUMMARY_RUNNING_SESSION_LIMIT = 64
35
37
  INPUT_ESCAPE_SEQUENCE_RE = re.compile(r"\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b[@-_]")
36
38
 
37
39
 
@@ -114,26 +116,6 @@ def _session_sort_key(session: dict[str, Any]) -> tuple[str, str]:
114
116
  )
115
117
 
116
118
 
117
- def _is_process_alive(pid: object) -> bool:
118
- if not isinstance(pid, int) or pid <= 0:
119
- return False
120
- proc_stat_path = Path("/proc") / str(pid) / "stat"
121
- if proc_stat_path.exists():
122
- try:
123
- parts = proc_stat_path.read_text(encoding="utf-8").split()
124
- except OSError:
125
- parts = []
126
- if len(parts) >= 3 and parts[2] == "Z":
127
- return False
128
- try:
129
- os.kill(pid, 0)
130
- except ProcessLookupError:
131
- return False
132
- except PermissionError:
133
- return True
134
- return True
135
-
136
-
137
119
  def _parse_progress_marker(line: str) -> dict[str, Any] | None:
138
120
  if not line.startswith(BASH_PROGRESS_PREFIX):
139
121
  return None
@@ -393,6 +375,65 @@ class BashExecService:
393
375
  str(session.get("bash_id") or ""),
394
376
  )
395
377
 
378
+ @classmethod
379
+ def _normalize_summary_session_list(
380
+ cls,
381
+ sessions: Any,
382
+ *,
383
+ limit: int,
384
+ ) -> list[dict[str, Any]]:
385
+ max_items = max(0, int(limit or 0))
386
+ if max_items <= 0:
387
+ return []
388
+ normalized: dict[str, dict[str, Any]] = {}
389
+ for raw in sessions or []:
390
+ if not isinstance(raw, dict):
391
+ continue
392
+ compact = cls._summary_session_payload(raw)
393
+ bash_id = _normalize_string(compact.get("bash_id"))
394
+ if not bash_id:
395
+ continue
396
+ normalized[bash_id] = compact
397
+ ordered = sorted(normalized.values(), key=cls._summary_sort_key, reverse=True)
398
+ return ordered[:max_items]
399
+
400
+ @classmethod
401
+ def _merge_summary_session_list(
402
+ cls,
403
+ sessions: Any,
404
+ compact: dict[str, Any],
405
+ *,
406
+ limit: int,
407
+ ) -> list[dict[str, Any]]:
408
+ bash_id = _normalize_string(compact.get("bash_id"))
409
+ merged = cls._normalize_summary_session_list(sessions, limit=max(1, int(limit or 0)) + 1)
410
+ merged = [
411
+ item
412
+ for item in merged
413
+ if _normalize_string(item.get("bash_id")) != bash_id
414
+ ]
415
+ if bash_id:
416
+ merged.append(cls._summary_session_payload(compact))
417
+ merged.sort(key=cls._summary_sort_key, reverse=True)
418
+ return merged[: max(1, int(limit or 0))]
419
+
420
+ @classmethod
421
+ def _remove_summary_session(
422
+ cls,
423
+ sessions: Any,
424
+ bash_id: str,
425
+ *,
426
+ limit: int,
427
+ ) -> list[dict[str, Any]]:
428
+ normalized_bash_id = _normalize_string(bash_id)
429
+ if not normalized_bash_id:
430
+ return cls._normalize_summary_session_list(sessions, limit=limit)
431
+ return [
432
+ item
433
+ for item in cls._normalize_summary_session_list(sessions, limit=limit)
434
+ if _normalize_string(item.get("bash_id")) != normalized_bash_id
435
+ ][: max(1, int(limit or 0))]
436
+
396
437
  @staticmethod
397
438
  def _is_active_status(value: object) -> bool:
398
439
  return _coerce_session_status(value) in {"running", "terminating"}
@@ -402,9 +443,31 @@ class BashExecService:
402
443
  "session_count": 0,
403
444
  "running_count": 0,
404
445
  "latest_session": None,
446
+ "recent_sessions": [],
447
+ "running_sessions": [],
405
448
  "updated_at": utc_now(),
406
449
  }
407
450
 
451
+ def _normalize_summary_payload(self, summary: Any) -> dict[str, Any]:
452
+ merged = {**self._default_summary(), **(summary if isinstance(summary, dict) else {})}
453
+ latest_session = merged.get("latest_session")
454
+ if isinstance(latest_session, dict):
455
+ compact_latest = self._summary_session_payload(latest_session)
456
+ merged["latest_session"] = compact_latest if _normalize_string(compact_latest.get("bash_id")) else None
457
+ else:
458
+ merged["latest_session"] = None
459
+ merged["session_count"] = max(0, int(merged.get("session_count") or 0))
460
+ merged["running_count"] = max(0, int(merged.get("running_count") or 0))
461
+ merged["recent_sessions"] = self._normalize_summary_session_list(
462
+ merged.get("recent_sessions"),
463
+ limit=SUMMARY_RECENT_SESSION_LIMIT,
464
+ )
465
+ merged["running_sessions"] = self._normalize_summary_session_list(
466
+ merged.get("running_sessions"),
467
+ limit=SUMMARY_RUNNING_SESSION_LIMIT,
468
+ )
469
+ return merged
470
+
408
471
  def _refresh_summary_cache(self, quest_root: Path, summary: dict[str, Any]) -> dict[str, Any]:
409
472
  path = self.summary_path(quest_root)
410
473
  cache_key = str(path.resolve())
@@ -417,7 +480,7 @@ class BashExecService:
417
480
  )
418
481
  else:
419
482
  state = None
420
- payload = dict(summary)
483
+ payload = self._normalize_summary_payload(summary)
421
484
  with self._summary_cache_lock:
422
485
  self._summary_cache[cache_key] = {
423
486
  "state": state,
@@ -443,38 +506,109 @@ class BashExecService:
443
506
  summary = read_json(path, None)
444
507
  if not isinstance(summary, dict):
445
508
  return None
446
- merged = {**self._default_summary(), **summary}
509
+ merged = self._normalize_summary_payload(summary)
447
510
  return self._refresh_summary_cache(quest_root, merged)
448
511
 
449
512
  def _write_summary(self, quest_root: Path, summary: dict[str, Any]) -> dict[str, Any]:
450
- normalized = {**self._default_summary(), **summary, "updated_at": utc_now()}
513
+ normalized = self._normalize_summary_payload(summary)
514
+ normalized["updated_at"] = utc_now()
451
515
  _atomic_write_json(self.summary_path(quest_root), normalized)
452
516
  return self._refresh_summary_cache(quest_root, normalized)
453
517
 
518
+ def _hydrate_summary_from_index(
519
+ self,
520
+ quest_root: Path,
521
+ summary: dict[str, Any],
522
+ ) -> dict[str, Any]:
523
+ needs_recent_sessions = not bool(summary.get("recent_sessions"))
524
+ needs_running_sessions = int(summary.get("running_count") or 0) > 0 and not bool(summary.get("running_sessions"))
525
+ if not needs_recent_sessions and not needs_running_sessions:
526
+ return summary
527
+
528
+ index_path = self.index_path(quest_root)
529
+ if not index_path.exists():
530
+ return summary
531
+
532
+ candidate_ids: list[str] = []
533
+ seen_ids: set[str] = set()
534
+ max_candidates = max(SUMMARY_RECENT_SESSION_LIMIT, SUMMARY_RUNNING_SESSION_LIMIT * 4)
535
+ for entry in reversed(read_jsonl(index_path)):
536
+ bash_id = _normalize_string((entry or {}).get("bash_id") if isinstance(entry, dict) else "")
537
+ if not bash_id or bash_id in seen_ids:
538
+ continue
539
+ seen_ids.add(bash_id)
540
+ candidate_ids.append(bash_id)
541
+ if len(candidate_ids) >= max_candidates:
542
+ break
543
+
544
+ if not candidate_ids:
545
+ return summary
546
+
547
+ recent_sessions: list[dict[str, Any]] = []
548
+ running_sessions: list[dict[str, Any]] = []
549
+ for bash_id in candidate_ids:
550
+ meta = read_json(self.meta_path(quest_root, bash_id), {})
551
+ if not isinstance(meta, dict) or not meta:
552
+ continue
553
+ compact = self._summary_session_payload(meta)
554
+ recent_sessions.append(compact)
555
+ if self._is_active_status(meta.get("status")):
556
+ running_sessions.append(compact)
557
+
558
+ if not recent_sessions and not running_sessions:
559
+ return summary
560
+
561
+ updated_summary = dict(summary)
562
+ if needs_recent_sessions and recent_sessions:
563
+ updated_summary["recent_sessions"] = self._normalize_summary_session_list(
564
+ recent_sessions,
565
+ limit=SUMMARY_RECENT_SESSION_LIMIT,
566
+ )
567
+ if updated_summary["recent_sessions"] and not isinstance(updated_summary.get("latest_session"), dict):
568
+ updated_summary["latest_session"] = updated_summary["recent_sessions"][0]
569
+ if needs_running_sessions:
570
+ updated_summary["running_sessions"] = self._normalize_summary_session_list(
571
+ running_sessions,
572
+ limit=SUMMARY_RUNNING_SESSION_LIMIT,
573
+ )
574
+ return self._write_summary(quest_root, updated_summary)
575
+
454
576
  def _rebuild_summary(self, quest_root: Path) -> dict[str, Any]:
455
577
  summary = self._default_summary()
456
578
  latest_session: dict[str, Any] | None = None
457
579
  session_count = 0
458
580
  running_count = 0
581
+ recent_sessions: list[dict[str, Any]] = []
582
+ running_sessions: list[dict[str, Any]] = []
459
583
  for meta_path in self.sessions_root(quest_root).glob("*/meta.json"):
460
584
  meta = read_json(meta_path, {})
461
585
  if not isinstance(meta, dict) or not meta:
462
586
  continue
463
587
  session_count += 1
588
+ compact = self._summary_session_payload(meta)
589
+ recent_sessions.append(compact)
464
590
  if self._is_active_status(meta.get("status")):
465
591
  running_count += 1
466
- compact = self._summary_session_payload(meta)
592
+ running_sessions.append(compact)
467
593
  if latest_session is None or self._summary_sort_key(compact) >= self._summary_sort_key(latest_session):
468
594
  latest_session = compact
469
595
  summary["session_count"] = session_count
470
596
  summary["running_count"] = running_count
471
597
  summary["latest_session"] = latest_session
598
+ summary["recent_sessions"] = self._normalize_summary_session_list(
599
+ recent_sessions,
600
+ limit=SUMMARY_RECENT_SESSION_LIMIT,
601
+ )
602
+ summary["running_sessions"] = self._normalize_summary_session_list(
603
+ running_sessions,
604
+ limit=SUMMARY_RUNNING_SESSION_LIMIT,
605
+ )
472
606
  return self._write_summary(quest_root, summary)
473
607
 
474
608
  def summary(self, quest_root: Path) -> dict[str, Any]:
475
609
  loaded = self._load_summary_from_disk(quest_root)
476
610
  if loaded is not None:
477
- return loaded
611
+ return self._hydrate_summary_from_index(quest_root, loaded)
478
612
  return self._rebuild_summary(quest_root)
479
613
 
480
614
  def _write_meta(self, quest_root: Path, bash_id: str, meta: dict[str, Any]) -> dict[str, Any]:
@@ -501,6 +635,23 @@ class BashExecService:
501
635
  or self._summary_sort_key(compact) >= self._summary_sort_key(latest_session)
502
636
  ):
503
637
  summary["latest_session"] = compact
638
+ summary["recent_sessions"] = self._merge_summary_session_list(
639
+ summary.get("recent_sessions"),
640
+ compact,
641
+ limit=SUMMARY_RECENT_SESSION_LIMIT,
642
+ )
643
+ if new_running:
644
+ summary["running_sessions"] = self._merge_summary_session_list(
645
+ summary.get("running_sessions"),
646
+ compact,
647
+ limit=SUMMARY_RUNNING_SESSION_LIMIT,
648
+ )
649
+ else:
650
+ summary["running_sessions"] = self._remove_summary_session(
651
+ summary.get("running_sessions"),
652
+ str(compact.get("bash_id") or ""),
653
+ limit=SUMMARY_RUNNING_SESSION_LIMIT,
654
+ )
504
655
  return self._write_summary(quest_root, summary)
505
656
 
506
657
  def reconcile_session(self, quest_root: Path, bash_id: str) -> dict[str, Any]:
@@ -518,20 +669,14 @@ class BashExecService:
518
669
  return self._session_payload(quest_root, read_json(meta_path, meta) or meta)
519
670
  monitor_pid = meta.get("monitor_pid")
520
671
  process_pid = meta.get("process_pid")
521
- if kind == "terminal" and _is_process_alive(process_pid):
522
- process_group_id = meta.get("process_group_id")
523
- if isinstance(process_group_id, int) and process_group_id > 0:
524
- try:
525
- os.killpg(process_group_id, signal.SIGTERM)
526
- except ProcessLookupError:
527
- pass
528
- elif isinstance(process_pid, int) and process_pid > 0:
529
- try:
530
- os.kill(process_pid, signal.SIGTERM)
531
- except ProcessLookupError:
532
- pass
672
+ if kind == "terminal" and is_process_alive(process_pid):
673
+ terminate_process_ids(
674
+ process_pid=process_pid if isinstance(process_pid, int) else None,
675
+ process_group_id=meta.get("process_group_id") if isinstance(meta.get("process_group_id"), int) else None,
676
+ force=False,
677
+ )
533
678
  time.sleep(0.05)
534
- if kind != "terminal" and (_is_process_alive(process_pid) or _is_process_alive(monitor_pid)):
679
+ if kind != "terminal" and (is_process_alive(process_pid) or is_process_alive(monitor_pid)):
535
680
  return self._session_payload(quest_root, meta)
536
681
  stop_reason = _normalize_string(meta.get("stop_reason"))
537
682
  meta["status"] = "terminated" if stop_reason else "failed"
@@ -566,6 +711,59 @@ class BashExecService:
566
711
  normalized_agent_instance_ids = {item for item in (agent_instance_ids or []) if item}
567
712
  normalized_agent_ids = {item for item in (agent_ids or []) if item}
568
713
  normalized_chat_session = _normalize_string(chat_session_id)
714
+ summary = self.summary(quest_root)
715
+ if normalized_status in {"running", "terminating"} and int(summary.get("running_count") or 0) <= 0:
716
+ return []
717
+ can_use_summary_fast_path = (
718
+ not normalized_agent_instance_ids
719
+ and not normalized_agent_ids
720
+ and not normalized_chat_session
721
+ )
722
+ if can_use_summary_fast_path:
723
+ candidate_compacts: list[dict[str, Any]] | None = None
724
+ if normalized_status in {"running", "terminating"}:
725
+ running_sessions = self._normalize_summary_session_list(
726
+ summary.get("running_sessions"),
727
+ limit=SUMMARY_RUNNING_SESSION_LIMIT,
728
+ )
729
+ filtered_running = [
730
+ item
731
+ for item in running_sessions
732
+ if (not normalized_kind or _normalize_string(item.get("kind")).lower() == normalized_kind)
733
+ and (not normalized_status or _normalize_string(item.get("status")).lower() == normalized_status)
734
+ ]
735
+ running_count = int(summary.get("running_count") or 0)
736
+ if len(filtered_running) >= max(1, limit) or running_count <= len(running_sessions):
737
+ candidate_compacts = filtered_running
738
+ elif not normalized_status:
739
+ recent_sessions = self._normalize_summary_session_list(
740
+ summary.get("recent_sessions"),
741
+ limit=SUMMARY_RECENT_SESSION_LIMIT,
742
+ )
743
+ if not normalized_kind:
744
+ if len(recent_sessions) >= max(1, limit) or int(summary.get("session_count") or 0) <= len(recent_sessions):
745
+ candidate_compacts = recent_sessions
746
+ else:
747
+ filtered_recent = [
748
+ item
749
+ for item in recent_sessions
750
+ if _normalize_string(item.get("kind")).lower() == normalized_kind
751
+ ]
752
+ if len(filtered_recent) >= max(1, limit) or int(summary.get("session_count") or 0) <= len(recent_sessions):
753
+ candidate_compacts = filtered_recent
754
+ if candidate_compacts is not None:
755
+ resolved_sessions: list[dict[str, Any]] = []
756
+ for compact in candidate_compacts:
757
+ bash_id = _normalize_string(compact.get("bash_id"))
758
+ if not bash_id:
759
+ continue
760
+ try:
761
+ resolved_sessions.append(self.reconcile_session(quest_root, bash_id))
762
+ except FileNotFoundError:
763
+ continue
764
+ if len(resolved_sessions) >= max(1, limit):
765
+ break
766
+ return resolved_sessions[: max(1, limit)]
569
767
  sessions: list[dict[str, Any]] = []
570
768
  for bash_id in self._list_session_ids(quest_root):
571
769
  try:
@@ -593,6 +791,11 @@ class BashExecService:
593
791
  if self.meta_path(quest_root, normalized).exists():
594
792
  return normalized
595
793
  raise FileNotFoundError(f"Unknown bash session `{normalized}`.")
794
+ summary = self.summary(quest_root)
795
+ latest_session = summary.get("latest_session")
796
+ latest_bash_id = _normalize_string((latest_session or {}).get("bash_id") if isinstance(latest_session, dict) else "")
797
+ if latest_bash_id and self.meta_path(quest_root, latest_bash_id).exists():
798
+ return latest_bash_id
596
799
  sessions = self.list_sessions(quest_root, limit=1)
597
800
  if not sessions:
598
801
  raise FileNotFoundError("No bash session found.")
@@ -717,18 +920,11 @@ class BashExecService:
717
920
  if runtime is not None:
718
921
  runtime.stop(reason=request_payload["reason"], force=bool(force))
719
922
  else:
720
- process_group_id = meta.get("process_group_id")
721
- process_pid = meta.get("process_pid")
722
- if isinstance(process_group_id, int) and process_group_id > 0:
723
- try:
724
- os.killpg(process_group_id, signal.SIGKILL if force else signal.SIGTERM)
725
- except ProcessLookupError:
726
- pass
727
- elif isinstance(process_pid, int) and process_pid > 0:
728
- try:
729
- os.kill(process_pid, signal.SIGKILL if force else signal.SIGTERM)
730
- except ProcessLookupError:
731
- pass
923
+ terminate_process_ids(
924
+ process_pid=meta.get("process_pid") if isinstance(meta.get("process_pid"), int) else None,
925
+ process_group_id=meta.get("process_group_id") if isinstance(meta.get("process_group_id"), int) else None,
926
+ force=bool(force),
927
+ )
732
928
  return self._session_payload(quest_root, meta)
733
929
 
734
930
  def _build_initial_meta(
@@ -737,6 +933,7 @@ class BashExecService:
737
933
  context: McpContext,
738
934
  bash_id: str,
739
935
  command: str,
936
+ launch_argv: list[str] | None,
740
937
  mode: str,
741
938
  cwd: Path,
742
939
  workdir_display: str,
@@ -744,6 +941,8 @@ class BashExecService:
744
941
  env_keys: list[str],
745
942
  comment: str | dict[str, Any] | None = None,
746
943
  kind: str = "exec",
944
+ shell_family: str | None = None,
945
+ shell_name: str | None = None,
747
946
  ) -> dict[str, Any]:
748
947
  quest_root = context.require_quest_root().resolve()
749
948
  session_id = _normalize_string(context.conversation_id) or f"quest:{context.quest_id or quest_root.name}"
@@ -766,6 +965,9 @@ class BashExecService:
766
965
  "stopped_by_user_id": None,
767
966
  "comment": comment,
768
967
  "command": command,
968
+ "launch_argv": list(launch_argv or []),
969
+ "shell_family": shell_family,
970
+ "shell_name": shell_name,
769
971
  "workdir": workdir_display,
770
972
  "cwd": str(cwd),
771
973
  "kind": kind,
@@ -808,7 +1010,7 @@ class BashExecService:
808
1010
  stderr=monitor_log_handle,
809
1011
  cwd=str(quest_root),
810
1012
  env=monitor_env,
811
- start_new_session=True,
1013
+ **process_session_popen_kwargs(hide_window=True),
812
1014
  )
813
1015
  finally:
814
1016
  monitor_log_handle.close()
@@ -833,10 +1035,12 @@ class BashExecService:
833
1035
  session_dir = self.session_dir(quest_root, bash_id)
834
1036
  ensure_dir(session_dir)
835
1037
  env_payload = {str(key): str(value) for key, value in (env or {}).items() if value is not None}
1038
+ launch = build_exec_shell_launch(command)
836
1039
  meta = self._build_initial_meta(
837
1040
  context=context,
838
1041
  bash_id=bash_id,
839
1042
  command=command,
1043
+ launch_argv=launch.argv,
840
1044
  mode=mode,
841
1045
  cwd=cwd,
842
1046
  workdir_display=workdir_display,
@@ -844,6 +1048,8 @@ class BashExecService:
844
1048
  env_keys=sorted(env_payload),
845
1049
  comment=comment,
846
1050
  kind="exec",
1051
+ shell_family=launch.family,
1052
+ shell_name=launch.shell_name,
847
1053
  )
848
1054
  self.terminal_log_path(quest_root, bash_id).touch()
849
1055
  self.log_path(quest_root, bash_id).touch()
@@ -880,10 +1086,14 @@ class BashExecService:
880
1086
  cwd: Path,
881
1087
  workdir_display: str,
882
1088
  command: str,
1089
+ launch_argv: list[str] | None,
883
1090
  source: str,
884
1091
  conversation_id: str | None,
885
1092
  user_id: str | None,
886
1093
  env_keys: list[str],
1094
+ shell_family: str | None = None,
1095
+ shell_name: str | None = None,
1096
+ transport_preference: str | None = None,
887
1097
  ) -> dict[str, Any]:
888
1098
  timestamp = utc_now()
889
1099
  session_id = _normalize_string(conversation_id) or f"quest:{quest_id}:terminal"
@@ -903,6 +1113,10 @@ class BashExecService:
903
1113
  "stopped_by_user_id": None,
904
1114
  "label": label,
905
1115
  "command": command,
1116
+ "launch_argv": list(launch_argv or []),
1117
+ "shell_family": shell_family,
1118
+ "shell_name": shell_name,
1119
+ "transport_preference": transport_preference,
906
1120
  "workdir": workdir_display,
907
1121
  "cwd": str(cwd),
908
1122
  "kind": "terminal",
@@ -984,29 +1198,49 @@ class BashExecService:
984
1198
  self.line_buffer_path(resolved_quest_root, bash_id),
985
1199
  {"buffer": "", "updated_at": utc_now()},
986
1200
  )
987
- terminal_rc_path = self.terminal_rc_path(resolved_quest_root, bash_id)
988
- terminal_rc_path.write_text(
989
- "\n".join(
990
- [
991
- "PS1='\\w$ '",
992
- "PS2='> '",
993
- 'PROMPT_COMMAND=\'printf "__DS_TERMINAL_PROMPT__ cwd=%q ts=%s\\n" "$PWD" "$(date -u +%FT%TZ)" >> "${DS_TERMINAL_PROMPT_PATH}"\'',
994
- "bind 'set enable-bracketed-paste off' >/dev/null 2>&1 || true",
995
- "",
996
- ]
997
- ),
998
- encoding="utf-8",
999
- )
1201
+ terminal_script_path = session_dir / ("terminal.ps1" if os.name == "nt" else "terminal.rc")
1000
1202
  stop_request = self.stop_request_path(resolved_quest_root, bash_id)
1001
1203
  if stop_request.exists():
1002
1204
  stop_request.unlink()
1003
1205
 
1004
1206
  env_payload = {
1005
- "TERM": "xterm-256color",
1006
- "COLORTERM": "truecolor",
1007
1207
  "DS_TERMINAL_PROMPT_PATH": str(self.prompt_events_path(resolved_quest_root, bash_id)),
1008
1208
  }
1009
- command = f"exec bash --noprofile --rcfile {shlex.quote(str(terminal_rc_path))} -i"
1209
+ if os.name != "nt":
1210
+ terminal_script_path.write_text(
1211
+ "\n".join(
1212
+ [
1213
+ "PS1='\\w$ '",
1214
+ "PS2='> '",
1215
+ 'PROMPT_COMMAND=\'printf "__DS_TERMINAL_PROMPT__ cwd_b64=%s ts=%s\\n" "$(printf "%s" "$PWD" | base64 | tr -d "\\n")" "$(date -u +%FT%TZ)" >> "${DS_TERMINAL_PROMPT_PATH}"\'',
1216
+ "bind 'set enable-bracketed-paste off' >/dev/null 2>&1 || true",
1217
+ "",
1218
+ ]
1219
+ ),
1220
+ encoding="utf-8",
1221
+ )
1222
+ env_payload["TERM"] = "xterm-256color"
1223
+ env_payload["COLORTERM"] = "truecolor"
1224
+ else:
1225
+ terminal_script_path.write_text(
1226
+ "\n".join(
1227
+ [
1228
+ "$global:__dsPromptPath = $env:DS_TERMINAL_PROMPT_PATH",
1229
+ "function global:prompt {",
1230
+ " $cwdBytes = [System.Text.Encoding]::UTF8.GetBytes((Get-Location).Path)",
1231
+ " $cwdB64 = [Convert]::ToBase64String($cwdBytes)",
1232
+ ' $ts = [DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")',
1233
+ ' Add-Content -LiteralPath $global:__dsPromptPath -Value "__DS_TERMINAL_PROMPT__ cwd_b64=$cwdB64 ts=$ts"',
1234
+ ' return "PS $((Get-Location).Path)> "',
1235
+ "}",
1236
+ "try { Set-PSReadLineOption -BellStyle None | Out-Null } catch {}",
1237
+ "",
1238
+ ]
1239
+ ),
1240
+ encoding="utf-8",
1241
+ )
1242
+ launch = build_terminal_shell_launch(terminal_script_path)
1243
+ command = " ".join(launch.argv)
1010
1244
  resolved_label = _normalize_string(label) or previous_label
1011
1245
  meta = self._build_terminal_meta(
1012
1246
  quest_root=resolved_quest_root,
@@ -1016,10 +1250,14 @@ class BashExecService:
1016
1250
  cwd=target_cwd,
1017
1251
  workdir_display=workdir_display,
1018
1252
  command=command,
1253
+ launch_argv=launch.argv,
1019
1254
  source=source,
1020
1255
  conversation_id=conversation_id,
1021
1256
  user_id=user_id,
1022
1257
  env_keys=sorted(env_payload),
1258
+ shell_family=launch.family,
1259
+ shell_name=launch.shell_name,
1260
+ transport_preference="pipe" if os.name == "nt" else "pty",
1023
1261
  )
1024
1262
  self._write_meta(resolved_quest_root, bash_id, meta)
1025
1263
  append_jsonl(
@@ -1042,7 +1280,9 @@ class BashExecService:
1042
1280
  prompt_events_path=self.prompt_events_path(resolved_quest_root, bash_id),
1043
1281
  env_payload=env_payload,
1044
1282
  command=command,
1283
+ launch_argv=launch.argv,
1045
1284
  cwd=target_cwd,
1285
+ transport_preference="pipe" if os.name == "nt" else "pty",
1046
1286
  )
1047
1287
  meta["updated_at"] = utc_now()
1048
1288
  self._write_meta(resolved_quest_root, bash_id, meta)
@@ -0,0 +1,87 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import os
5
+ from pathlib import Path
6
+ import shutil
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class ShellLaunchSpec:
11
+ argv: list[str]
12
+ family: str
13
+ shell_name: str
14
+
15
+
16
+ def _resolve_windows_shell(*, interactive: bool) -> tuple[str, str]:
17
+ candidates: list[tuple[str, str]] = []
18
+ if not interactive:
19
+ candidates.extend(
20
+ [
21
+ ("bash.exe", "bash"),
22
+ ("bash", "bash"),
23
+ ]
24
+ )
25
+ candidates.extend(
26
+ [
27
+ ("pwsh", "powershell"),
28
+ ("powershell.exe", "powershell"),
29
+ ]
30
+ )
31
+ if not interactive:
32
+ candidates.append(("cmd.exe", "cmd"))
33
+ for binary, family in candidates:
34
+ resolved = shutil.which(binary)
35
+ if resolved:
36
+ return resolved, family
37
+ fallback = "cmd.exe" if not interactive else "powershell.exe"
38
+ return fallback, "cmd" if fallback == "cmd.exe" else "powershell"
39
+
40
+
41
+ def build_exec_shell_launch(command: str) -> ShellLaunchSpec:
42
+ normalized = str(command or "").strip()
43
+ if os.name != "nt":
44
+ return ShellLaunchSpec(
45
+ argv=["bash", "-lc", normalized],
46
+ family="bash",
47
+ shell_name="bash",
48
+ )
49
+ binary, family = _resolve_windows_shell(interactive=False)
50
+ if family == "bash":
51
+ return ShellLaunchSpec(
52
+ argv=[binary, "-lc", normalized],
53
+ family=family,
54
+ shell_name=Path(binary).name,
55
+ )
56
+ if family == "cmd":
57
+ return ShellLaunchSpec(
58
+ argv=[binary, "/d", "/s", "/c", normalized],
59
+ family=family,
60
+ shell_name=Path(binary).name,
61
+ )
62
+ return ShellLaunchSpec(
63
+ argv=[binary, "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", normalized],
64
+ family=family,
65
+ shell_name=Path(binary).name,
66
+ )
67
+
68
+
69
+ def build_terminal_shell_launch(script_path: Path) -> ShellLaunchSpec:
70
+ if os.name != "nt":
71
+ return ShellLaunchSpec(
72
+ argv=["bash", "--noprofile", "--rcfile", str(script_path), "-i"],
73
+ family="bash",
74
+ shell_name="bash",
75
+ )
76
+ binary, family = _resolve_windows_shell(interactive=True)
77
+ if family == "powershell":
78
+ return ShellLaunchSpec(
79
+ argv=[binary, "-NoLogo", "-NoProfile", "-NoExit", "-ExecutionPolicy", "Bypass", "-File", str(script_path)],
80
+ family=family,
81
+ shell_name=Path(binary).name,
82
+ )
83
+ return ShellLaunchSpec(
84
+ argv=[binary, "/q", "/k", str(script_path)],
85
+ family=family,
86
+ shell_name=Path(binary).name,
87
+ )