@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.
- package/README.md +336 -90
- package/assets/branding/logo-raster.png +0 -0
- package/bin/ds.js +816 -131
- package/docs/en/00_QUICK_START.md +36 -15
- package/docs/en/01_SETTINGS_REFERENCE.md +53 -4
- package/docs/en/02_START_RESEARCH_GUIDE.md +7 -0
- package/docs/en/03_QQ_CONNECTOR_GUIDE.md +19 -0
- package/docs/en/05_TUI_GUIDE.md +6 -0
- package/docs/en/06_RUNTIME_AND_CANVAS.md +4 -3
- package/docs/en/09_DOCTOR.md +11 -5
- package/docs/en/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
- package/docs/en/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +65 -13
- package/docs/en/15_CODEX_PROVIDER_SETUP.md +25 -8
- package/docs/en/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
- package/docs/en/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
- package/docs/en/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
- package/docs/en/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
- package/docs/en/19_LOCAL_BROWSER_AUTH.md +70 -0
- package/docs/en/20_WORKSPACE_MODES_GUIDE.md +250 -0
- package/docs/en/README.md +24 -0
- package/docs/zh/00_QUICK_START.md +36 -15
- package/docs/zh/01_SETTINGS_REFERENCE.md +53 -4
- package/docs/zh/02_START_RESEARCH_GUIDE.md +7 -0
- package/docs/zh/03_QQ_CONNECTOR_GUIDE.md +19 -0
- package/docs/zh/05_TUI_GUIDE.md +6 -0
- package/docs/zh/09_DOCTOR.md +11 -5
- package/docs/zh/10_WEIXIN_CONNECTOR_GUIDE.md +20 -0
- package/docs/zh/14_PROMPT_SKILLS_AND_MCP_GUIDE.md +65 -13
- package/docs/zh/15_CODEX_PROVIDER_SETUP.md +25 -8
- package/docs/zh/16_TELEGRAM_CONNECTOR_GUIDE.md +134 -0
- package/docs/zh/17_WHATSAPP_CONNECTOR_GUIDE.md +126 -0
- package/docs/zh/18_FEISHU_CONNECTOR_GUIDE.md +136 -0
- package/docs/zh/19_EXTERNAL_CONTROLLER_GUIDE.md +226 -0
- package/docs/zh/19_LOCAL_BROWSER_AUTH.md +68 -0
- package/docs/zh/20_WORKSPACE_MODES_GUIDE.md +251 -0
- package/docs/zh/README.md +24 -0
- package/install.sh +2 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/deepscientist/__init__.py +1 -1
- package/src/deepscientist/acp/envelope.py +6 -0
- package/src/deepscientist/artifact/charts.py +567 -0
- package/src/deepscientist/artifact/guidance.py +50 -10
- package/src/deepscientist/artifact/metrics.py +228 -5
- package/src/deepscientist/artifact/schemas.py +3 -0
- package/src/deepscientist/artifact/service.py +4276 -308
- package/src/deepscientist/bash_exec/models.py +23 -0
- package/src/deepscientist/bash_exec/monitor.py +147 -67
- package/src/deepscientist/bash_exec/runtime.py +218 -156
- package/src/deepscientist/bash_exec/service.py +309 -69
- package/src/deepscientist/bash_exec/shells.py +87 -0
- package/src/deepscientist/bridges/connectors.py +51 -2
- package/src/deepscientist/cli.py +115 -19
- package/src/deepscientist/codex_cli_compat.py +232 -0
- package/src/deepscientist/config/models.py +8 -4
- package/src/deepscientist/config/service.py +38 -11
- package/src/deepscientist/connector/weixin_support.py +122 -1
- package/src/deepscientist/daemon/api/handlers.py +199 -9
- package/src/deepscientist/daemon/api/router.py +5 -0
- package/src/deepscientist/daemon/app.py +1458 -289
- package/src/deepscientist/doctor.py +51 -0
- package/src/deepscientist/file_lock.py +48 -0
- package/src/deepscientist/gitops/__init__.py +10 -1
- package/src/deepscientist/gitops/diff.py +296 -1
- package/src/deepscientist/gitops/service.py +4 -1
- package/src/deepscientist/mcp/server.py +212 -5
- package/src/deepscientist/process_control.py +161 -0
- package/src/deepscientist/prompts/builder.py +501 -453
- package/src/deepscientist/quest/layout.py +15 -2
- package/src/deepscientist/quest/service.py +2539 -195
- package/src/deepscientist/quest/stage_views.py +177 -1
- package/src/deepscientist/runners/base.py +2 -0
- package/src/deepscientist/runners/codex.py +169 -31
- package/src/deepscientist/runners/runtime_overrides.py +17 -1
- package/src/deepscientist/skills/__init__.py +2 -2
- package/src/deepscientist/skills/installer.py +196 -5
- package/src/deepscientist/skills/registry.py +66 -0
- package/src/prompts/connectors/qq.md +18 -8
- package/src/prompts/connectors/weixin.md +16 -6
- package/src/prompts/contracts/shared_interaction.md +24 -4
- package/src/prompts/system.md +921 -72
- package/src/prompts/system_copilot.md +43 -0
- package/src/skills/analysis-campaign/SKILL.md +32 -2
- package/src/skills/analysis-campaign/references/artifact-orchestration.md +1 -1
- package/src/skills/analysis-campaign/references/writing-facing-slice-examples.md +65 -0
- package/src/skills/baseline/SKILL.md +10 -0
- package/src/skills/decision/SKILL.md +27 -2
- package/src/skills/experiment/SKILL.md +16 -2
- package/src/skills/figure-polish/SKILL.md +1 -0
- package/src/skills/finalize/SKILL.md +19 -0
- package/src/skills/idea/SKILL.md +79 -0
- package/src/skills/idea/references/idea-generation-playbook.md +100 -0
- package/src/skills/idea/references/outline-seeding-example.md +60 -0
- package/src/skills/intake-audit/SKILL.md +9 -1
- package/src/skills/mentor/SKILL.md +217 -0
- package/src/skills/mentor/references/correction-rules.md +210 -0
- package/src/skills/mentor/references/knowledge-profile.md +91 -0
- package/src/skills/mentor/references/persona-profile.md +138 -0
- package/src/skills/mentor/references/taste-profile.md +128 -0
- package/src/skills/mentor/references/thought-style-profile.md +138 -0
- package/src/skills/mentor/references/work-profile.md +289 -0
- package/src/skills/mentor/references/workflow-profile.md +240 -0
- package/src/skills/optimize/SKILL.md +1645 -0
- package/src/skills/rebuttal/SKILL.md +3 -1
- package/src/skills/review/SKILL.md +3 -1
- package/src/skills/scout/SKILL.md +8 -0
- package/src/skills/write/SKILL.md +81 -12
- package/src/skills/write/references/outline-evidence-contract-example.md +107 -0
- package/src/tui/dist/app/AppContainer.js +22 -11
- package/src/tui/dist/index.js +4 -1
- package/src/tui/dist/lib/api.js +33 -3
- package/src/tui/package.json +1 -1
- package/src/ui/dist/assets/AiManusChatView-COFACy7V.js +204 -0
- package/src/ui/dist/assets/AnalysisPlugin-DnSm0GZn.js +1 -0
- package/src/ui/dist/assets/CliPlugin-CvwCmDQ5.js +109 -0
- package/src/ui/dist/assets/CodeEditorPlugin-cOqSa0xq.js +2 -0
- package/src/ui/dist/assets/CodeViewerPlugin-itb0tltR.js +270 -0
- package/src/ui/dist/assets/DocViewerPlugin-DqKkiCI6.js +7 -0
- package/src/ui/dist/assets/GitCommitViewerPlugin-DVgNHBCS.js +1 -0
- package/src/ui/dist/assets/GitDiffViewerPlugin-DxL2ezFG.js +6 -0
- package/src/ui/dist/assets/GitSnapshotViewer-B_RQm1YZ.js +30 -0
- package/src/ui/dist/assets/ImageViewerPlugin-tHqlXY3n.js +26 -0
- package/src/ui/dist/assets/LabCopilotPanel-ClMbq5Yu.js +14 -0
- package/src/ui/dist/assets/LabPlugin-L_SuE8ow.js +22 -0
- package/src/ui/dist/assets/LatexPlugin-B495DTXC.js +25 -0
- package/src/ui/dist/assets/MarkdownViewerPlugin-DG28-61B.js +128 -0
- package/src/ui/dist/assets/MarketplacePlugin-BiOGT-Kj.js +13 -0
- package/src/ui/dist/assets/{NotebookEditor-CccQYZjX.css → NotebookEditor-BHH8rdGj.css} +1 -1
- package/src/ui/dist/assets/NotebookEditor-BOr3x3Ej.css +1 -0
- package/src/ui/dist/assets/NotebookEditor-C-4Kt1p9.js +81 -0
- package/src/ui/dist/assets/NotebookEditor-CVsj8h_T.js +361 -0
- package/src/ui/dist/assets/PdfLoader-CASDQmxJ.js +16 -0
- package/src/ui/dist/assets/PdfLoader-Cy5jtWrr.css +1 -0
- package/src/ui/dist/assets/PdfMarkdownPlugin-BFhwoKsY.js +1 -0
- package/src/ui/dist/assets/PdfViewerPlugin-DcOzU9vd.js +17 -0
- package/src/ui/dist/assets/PdfViewerPlugin-nwwE-fjJ.css +1 -0
- package/src/ui/dist/assets/SearchPlugin-CHj7M58O.js +16 -0
- package/src/ui/dist/assets/SearchPlugin-DA4en4hK.css +1 -0
- package/src/ui/dist/assets/TextViewerPlugin-CB4DYfWO.js +54 -0
- package/src/ui/dist/assets/VNCViewer-CjlbyCB3.js +11 -0
- package/src/ui/dist/assets/bot-CFkZY-JP.js +6 -0
- package/src/ui/dist/assets/browser-CTB2jwNe.js +8 -0
- package/src/ui/dist/assets/chevron-up-Dq5ofbht.js +6 -0
- package/src/ui/dist/assets/code-DLC6G24T.js +6 -0
- package/src/ui/dist/assets/file-content-Dv4LoZec.js +1 -0
- package/src/ui/dist/assets/file-diff-panel-Denq-lC3.js +1 -0
- package/src/ui/dist/assets/file-jump-queue-DA-SdG__.js +1 -0
- package/src/ui/dist/assets/file-socket-Cu4Qln7Y.js +1 -0
- package/src/ui/dist/assets/git-commit-horizontal-BUh6G52n.js +6 -0
- package/src/ui/dist/assets/image-B9HUUddG.js +6 -0
- package/src/ui/dist/assets/index-B2B1sg-M.js +1 -0
- package/src/ui/dist/assets/index-Cgla8biy.css +33 -0
- package/src/ui/dist/assets/index-DRyx7vAc.js +1 -0
- package/src/ui/dist/assets/index-Gbl53BNp.js +2496 -0
- package/src/ui/dist/assets/index-wQ7RIIRd.js +11 -0
- package/src/ui/dist/assets/monaco-CiHMMNH_.js +1 -0
- package/src/ui/dist/assets/pdf-effect-queue-ZtnHFCAi.js +6 -0
- package/src/ui/dist/assets/plugin-monaco-C8UgLomw.js +19 -0
- package/src/ui/dist/assets/plugin-notebook-HbW2K-1c.js +169 -0
- package/src/ui/dist/assets/plugin-pdf-CR8hgQBV.js +357 -0
- package/src/ui/dist/assets/plugin-terminal-MXFIPun8.js +227 -0
- package/src/ui/dist/assets/popover-DL6h35vr.js +1 -0
- package/src/ui/dist/assets/project-sync-CsX08Qno.js +1 -0
- package/src/ui/dist/assets/select-DvmXt1yY.js +11 -0
- package/src/ui/dist/assets/sigma-7jpXazui.js +6 -0
- package/src/ui/dist/assets/trash-xA7kFt8i.js +11 -0
- package/src/ui/dist/assets/useCliAccess-DsMwDjOp.js +1 -0
- package/src/ui/dist/assets/useFileDiffOverlay-FuhcnKiw.js +1 -0
- package/src/ui/dist/assets/wrap-text-CwMn-iqb.js +11 -0
- package/src/ui/dist/assets/zoom-out-R-GWEhzS.js +11 -0
- package/src/ui/dist/index.html +5 -2
- package/src/ui/dist/assets/AiManusChatView-DaF9Nge_.js +0 -26597
- package/src/ui/dist/assets/AnalysisPlugin-BSVx6dXE.js +0 -123
- package/src/ui/dist/assets/CliPlugin-C9gzJX41.js +0 -5905
- package/src/ui/dist/assets/CodeEditorPlugin-DU9G0Tox.js +0 -427
- package/src/ui/dist/assets/CodeViewerPlugin-DoX_fI9l.js +0 -905
- package/src/ui/dist/assets/DocViewerPlugin-C4FWIXuU.js +0 -278
- package/src/ui/dist/assets/GitDiffViewerPlugin-BgfFMgtf.js +0 -2661
- package/src/ui/dist/assets/ImageViewerPlugin-tcPkfY_x.js +0 -500
- package/src/ui/dist/assets/LabCopilotPanel-_dKV60Bf.js +0 -4104
- package/src/ui/dist/assets/LabPlugin-Bje0ayoC.js +0 -2677
- package/src/ui/dist/assets/LatexPlugin-CVsBzAln.js +0 -1792
- package/src/ui/dist/assets/MarkdownViewerPlugin-xjmrqv_8.js +0 -308
- package/src/ui/dist/assets/MarketplacePlugin-mMM2A8wP.js +0 -413
- package/src/ui/dist/assets/NotebookEditor-3kVDSOBo.js +0 -4214
- package/src/ui/dist/assets/NotebookEditor-C3VQ7ylN.css +0 -1405
- package/src/ui/dist/assets/NotebookEditor-SoJ8X-MO.js +0 -84873
- package/src/ui/dist/assets/PdfLoader-C-Y707R3.css +0 -49
- package/src/ui/dist/assets/PdfLoader-DElVuHl9.js +0 -25468
- package/src/ui/dist/assets/PdfMarkdownPlugin-Bq88XT4G.js +0 -409
- package/src/ui/dist/assets/PdfViewerPlugin-CsCXMo9S.js +0 -3095
- package/src/ui/dist/assets/PdfViewerPlugin-DQ11QcSf.css +0 -3627
- package/src/ui/dist/assets/SearchPlugin-DDMrGDkh.css +0 -379
- package/src/ui/dist/assets/SearchPlugin-oUPvy19k.js +0 -741
- package/src/ui/dist/assets/TextViewerPlugin-CRkT9yNy.js +0 -472
- package/src/ui/dist/assets/VNCViewer-BgbuvWhR.js +0 -18821
- package/src/ui/dist/assets/awareness-C0NPR2Dj.js +0 -292
- package/src/ui/dist/assets/bot-v_RASACv.js +0 -21
- package/src/ui/dist/assets/browser-BAcuE0Xj.js +0 -2895
- package/src/ui/dist/assets/code-5hC9d0VH.js +0 -17
- package/src/ui/dist/assets/file-content-D1PxfOrp.js +0 -377
- package/src/ui/dist/assets/file-diff-panel-DG1oT_Hj.js +0 -92
- package/src/ui/dist/assets/file-jump-queue-r5XKgJEV.js +0 -16
- package/src/ui/dist/assets/file-socket-BmdFYQlk.js +0 -58
- package/src/ui/dist/assets/function-B5QZkkHC.js +0 -1895
- package/src/ui/dist/assets/image-Dqe2X2tW.js +0 -18
- package/src/ui/dist/assets/index-BQG-1s2o.css +0 -12553
- package/src/ui/dist/assets/index-DVsMKK_y.js +0 -25
- package/src/ui/dist/assets/index-Duvz8Ip0.js +0 -159
- package/src/ui/dist/assets/index-Nt9hS4ck.js +0 -244829
- package/src/ui/dist/assets/index-RDlNXXx1.js +0 -120
- package/src/ui/dist/assets/monaco-DIXge1CP.js +0 -623
- package/src/ui/dist/assets/pdf-effect-queue-BBTTQaO-.js +0 -47
- package/src/ui/dist/assets/pdf_viewer-e0g1is2C.js +0 -8206
- package/src/ui/dist/assets/popover-BWlolyxo.js +0 -476
- package/src/ui/dist/assets/project-sync-BM5PkFH4.js +0 -297
- package/src/ui/dist/assets/select-D4dAtrA8.js +0 -1690
- package/src/ui/dist/assets/sigma-CKbE5jJT.js +0 -22
- package/src/ui/dist/assets/square-check-big-CZNGMgiB.js +0 -17
- package/src/ui/dist/assets/trash-DaB37xAz.js +0 -32
- package/src/ui/dist/assets/useCliAccess-C2OmAcWe.js +0 -957
- package/src/ui/dist/assets/useFileDiffOverlay-Dowd1Ij4.js +0 -53
- package/src/ui/dist/assets/wrap-text-BGjAhAUq.js +0 -35
- package/src/ui/dist/assets/yjs-DncrqiZ8.js +0 -11243
- 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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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 (
|
|
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
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
)
|