coding-bridge 2026.7.2.1__tar.gz → 2026.7.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/PKG-INFO +1 -1
  2. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/history.py +5 -199
  3. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/pyproject.toml +1 -1
  4. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/test_history.py +0 -106
  5. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/.env.example +0 -0
  6. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/.github/workflows/ci.yaml +0 -0
  7. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/.github/workflows/publish.yml +0 -0
  8. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/.gitignore +0 -0
  9. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/LICENSE +0 -0
  10. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/NOTICE +0 -0
  11. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/README.md +0 -0
  12. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/__init__.py +0 -0
  13. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/__main__.py +0 -0
  14. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/attachments.py +0 -0
  15. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/capabilities.py +0 -0
  16. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/cli.py +0 -0
  17. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/config.py +0 -0
  18. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/connection.py +0 -0
  19. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/fs.py +0 -0
  20. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/images.py +0 -0
  21. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/locking.py +0 -0
  22. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/logs.py +0 -0
  23. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/pairing.py +0 -0
  24. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/permissions.py +0 -0
  25. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/protocol.py +0 -0
  26. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/providers/__init__.py +0 -0
  27. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/providers/base.py +0 -0
  28. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/providers/claude.py +0 -0
  29. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/providers/codex.py +0 -0
  30. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/providers/copilot.py +0 -0
  31. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/session.py +0 -0
  32. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/session_meta.py +0 -0
  33. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/coding_bridge/store.py +0 -0
  34. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/docs/design/reliable-event-delivery.md +0 -0
  35. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/conftest.py +0 -0
  36. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/test_ask_user_question.py +0 -0
  37. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/test_attachments.py +0 -0
  38. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/test_capabilities.py +0 -0
  39. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/test_codex.py +0 -0
  40. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/test_config.py +0 -0
  41. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/test_connection.py +0 -0
  42. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/test_copilot.py +0 -0
  43. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/test_edit_fork.py +0 -0
  44. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/test_fs.py +0 -0
  45. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/test_images.py +0 -0
  46. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/test_locking.py +0 -0
  47. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/test_outbox.py +0 -0
  48. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/test_pairing.py +0 -0
  49. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/test_permissions.py +0 -0
  50. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/test_protocol.py +0 -0
  51. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/test_resume_guard.py +0 -0
  52. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/test_session.py +0 -0
  53. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/test_session_meta.py +0 -0
  54. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/test_slash_commands.py +0 -0
  55. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/test_store.py +0 -0
  56. {coding_bridge-2026.7.2.1 → coding_bridge-2026.7.2.3}/tests/test_streaming.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-bridge
3
- Version: 2026.7.2.1
3
+ Version: 2026.7.2.3
4
4
  Summary: Node daemon for AceDataCloud Coding Bridge — run Claude Code on your own machine and drive it from the web.
5
5
  Project-URL: Homepage, https://github.com/AceDataCloud/CodingBridge
6
6
  Project-URL: Issues, https://github.com/AceDataCloud/CodingBridge/issues
@@ -20,7 +20,6 @@ from concurrent.futures import ThreadPoolExecutor
20
20
  from datetime import datetime
21
21
  from pathlib import Path
22
22
  from typing import Any
23
- from urllib.parse import unquote, urlparse
24
23
 
25
24
  # Module-level roots so tests can point them at fixtures via monkeypatch.
26
25
  CLAUDE_ROOT = Path.home() / ".claude" / "projects"
@@ -30,33 +29,6 @@ COPILOT_ROOT = (
30
29
  Path(os.environ.get("COPILOT_HOME") or str(Path.home() / ".copilot")) / "session-state"
31
30
  )
32
31
 
33
-
34
- def _vscode_chat_roots() -> list[Path]:
35
- """Candidate ``workspaceStorage`` dirs for VS Code Copilot Chat transcripts.
36
-
37
- Chat sessions live at ``<storage>/<workspace-hash>/chatSessions/<id>.jsonl``.
38
- ``VSCODE_CHAT_HOME`` overrides for tests / non-standard installs.
39
- """
40
- override = os.environ.get("VSCODE_CHAT_HOME")
41
- if override:
42
- return [Path(override)]
43
- home = Path.home()
44
- bases: list[Path | None] = [
45
- home / "Library" / "Application Support", # macOS
46
- Path(os.environ["APPDATA"]) if os.environ.get("APPDATA") else None, # Windows
47
- home / ".config", # Linux
48
- ]
49
- apps = ("Code", "Code - Insiders", "VSCodium", "Cursor")
50
- roots: list[Path] = []
51
- for base in bases:
52
- if base is None:
53
- continue
54
- roots.extend(base / app / "User" / "workspaceStorage" for app in apps)
55
- return roots
56
-
57
-
58
- VSCODE_CHAT_ROOTS = _vscode_chat_roots()
59
-
60
32
  _TITLE_MAX = 80
61
33
  _DETAIL_MAX_EVENTS = 4000
62
34
 
@@ -70,14 +42,9 @@ _UUID_RE = re.compile(
70
42
 
71
43
  # --- public API ------------------------------------------------------------
72
44
  def list_sessions(limit: int = 200) -> list[dict[str, Any]]:
73
- """Return session summaries from every provider, newest first."""
45
+ """Return session summaries from both providers, newest first."""
74
46
  limit = max(1, min(int(limit or 200), 1000))
75
- sessions = (
76
- _list_claude(limit)
77
- + _list_codex(limit)
78
- + _list_copilot(limit)
79
- + _list_vscode_chat(limit)
80
- )
47
+ sessions = _list_claude(limit) + _list_codex(limit) + _list_copilot(limit)
81
48
  sessions.sort(key=lambda s: s.get("updated_at") or 0, reverse=True)
82
49
  return sessions[:limit]
83
50
 
@@ -144,17 +111,10 @@ def read_session(provider: str, session_id: str) -> dict[str, Any]:
144
111
  raise FileNotFoundError(f"codex session not found: {session_id}")
145
112
  header, events = _codex_read(path, _codex_index())
146
113
  elif provider == "copilot":
147
- # A "copilot" session may live in the CLI store or the VS Code Copilot
148
- # Chat store — both are Copilot to the user. Prefer the CLI store, fall
149
- # back to the VS Code panel transcript.
150
114
  path = _copilot_path(session_id)
151
- if path is not None:
152
- header, events = _copilot_read(path)
153
- else:
154
- vpath = _vscode_chat_path(session_id)
155
- if vpath is None:
156
- raise FileNotFoundError(f"copilot session not found: {session_id}")
157
- header, events = _vscode_chat_read(vpath)
115
+ if path is None:
116
+ raise FileNotFoundError(f"copilot session not found: {session_id}")
117
+ header, events = _copilot_read(path)
158
118
  else:
159
119
  raise ValueError(f"unknown provider: {provider}")
160
120
  # Keep the MOST RECENT events when a transcript is huge — the tail is the
@@ -665,160 +625,6 @@ def _copilot_tool_result_text(data: dict[str, Any]) -> Any:
665
625
  return _stringify(error)
666
626
 
667
627
 
668
- # --- VS Code Copilot Chat --------------------------------------------------
669
- # The panel stores each session as an append-only delta log of `{kind, v}` lines
670
- # (format v3). We don't replay the deltas; we recursively find the request
671
- # objects (dict with `message.text` + `response`), which is resilient to delta
672
- # opcode changes across VS Code releases. These are surfaced as ordinary
673
- # `copilot` sessions (same backend); read-only, so continuing makes a copy.
674
- def _list_vscode_chat(limit: int) -> list[dict[str, Any]]:
675
- files: list[Path] = []
676
- for root in VSCODE_CHAT_ROOTS:
677
- if root.exists():
678
- files.extend(root.glob("*/chatSessions/*.jsonl"))
679
- files = sorted(files, key=_safe_mtime, reverse=True)[:limit]
680
- return _summaries(files, _vscode_chat_summary)
681
-
682
-
683
- def _vscode_collect_requests(obj: Any, out: list[dict[str, Any]]) -> None:
684
- """Collect chat-request objects, pruning descent into the huge `response`."""
685
- if isinstance(obj, dict):
686
- msg = obj.get("message")
687
- if isinstance(msg, dict) and "text" in msg and "response" in obj:
688
- out.append(obj)
689
- return # don't recurse into this request's response array
690
- for value in obj.values():
691
- _vscode_collect_requests(value, out)
692
- elif isinstance(obj, list):
693
- for value in obj:
694
- _vscode_collect_requests(value, out)
695
-
696
-
697
- def _vscode_requests(path: Path) -> list[dict[str, Any]]:
698
- seen: dict[str, dict[str, Any]] = {}
699
- ordered: list[dict[str, Any]] = []
700
- for rec in _iter_jsonl(path):
701
- found: list[dict[str, Any]] = []
702
- _vscode_collect_requests(rec.get("v"), found)
703
- for req in found:
704
- rid = req.get("requestId")
705
- key = rid if isinstance(rid, str) else str(id(req))
706
- if key not in seen:
707
- seen[key] = req
708
- ordered.append(req)
709
- ordered.sort(key=lambda r: r.get("timestamp") or 0)
710
- return ordered
711
-
712
-
713
- def _vscode_user_text(req: dict[str, Any]) -> str:
714
- msg = req.get("message")
715
- text = msg.get("text") if isinstance(msg, dict) else None
716
- return text.strip() if isinstance(text, str) else ""
717
-
718
-
719
- def _vscode_chat_summary(path: Path) -> dict[str, Any]:
720
- reqs = _vscode_requests(path)
721
- title = ""
722
- updated_at = _mtime_ms(path)
723
- for req in reqs:
724
- ts = req.get("timestamp")
725
- if isinstance(ts, int):
726
- updated_at = max(updated_at, ts)
727
- if not title:
728
- text = _vscode_user_text(req)
729
- if text and not _is_context_noise(text):
730
- title = _clean_title(text)
731
- return {
732
- "provider": "copilot",
733
- "session_id": path.stem,
734
- "title": title or "(no prompt)",
735
- "cwd": _vscode_cwd(path),
736
- "git_branch": None,
737
- "updated_at": updated_at,
738
- "message_count": len(reqs),
739
- }
740
-
741
-
742
- def _vscode_chat_read(path: Path) -> tuple[dict[str, Any], list[dict[str, Any]]]:
743
- header: dict[str, Any] = {
744
- "cwd": _vscode_cwd(path),
745
- "git_branch": None,
746
- "model": None,
747
- "title": "",
748
- }
749
- events: list[dict[str, Any]] = []
750
- for req in _vscode_requests(path):
751
- ts = req.get("timestamp") if isinstance(req.get("timestamp"), int) else None
752
- model = req.get("modelId")
753
- if header["model"] is None and isinstance(model, str) and model:
754
- header["model"] = model
755
- text = _vscode_user_text(req)
756
- if text and not _is_context_noise(text):
757
- events.append({"kind": "prompt", "text": text, "ts": ts})
758
- if not header["title"]:
759
- header["title"] = _clean_title(text)
760
- _vscode_response_events(req.get("response"), ts, events)
761
- return header, events
762
-
763
-
764
- def _vscode_response_events(response: Any, ts: int | None, events: list[dict[str, Any]]) -> None:
765
- if not isinstance(response, list):
766
- return
767
- for part in response:
768
- if not isinstance(part, dict):
769
- continue
770
- kind = part.get("kind")
771
- if kind is None: # markdown text block: {"value": "...", ...}
772
- value = part.get("value")
773
- text = value.get("value") if isinstance(value, dict) else value
774
- if isinstance(text, str) and text.strip():
775
- events.append({"kind": "text", "text": text, "ts": ts})
776
- elif kind == "thinking":
777
- value = part.get("value")
778
- if isinstance(value, str) and value.strip():
779
- events.append({"kind": "thinking", "text": value, "ts": ts})
780
- elif kind == "toolInvocationSerialized":
781
- msg = part.get("pastTenseMessage") or part.get("invocationMessage")
782
- label = msg.get("value") if isinstance(msg, dict) else msg
783
- events.append(
784
- {
785
- "kind": "tool_use",
786
- "tool": part.get("toolId"),
787
- "tool_use_id": part.get("toolCallId"),
788
- "input": label if isinstance(label, str) else None,
789
- "ts": ts,
790
- }
791
- )
792
-
793
-
794
- def _vscode_chat_path(session_id: str) -> Path | None:
795
- if not _safe_id(session_id) or session_id in (".", ".."):
796
- return None
797
- matches: list[Path] = []
798
- for root in VSCODE_CHAT_ROOTS:
799
- if root.exists():
800
- matches.extend(root.glob(f"*/chatSessions/{session_id}.jsonl"))
801
- matches = [p for p in matches if p.is_file()]
802
- return max(matches, key=_safe_mtime) if matches else None
803
-
804
-
805
- def _vscode_cwd(path: Path) -> str | None:
806
- # chatSessions/<id>.jsonl -> workspace.json sits two levels up (workspace hash dir)
807
- meta = path.parent.parent / "workspace.json"
808
- try:
809
- data = json.loads(meta.read_text(encoding="utf-8"))
810
- except (OSError, json.JSONDecodeError, ValueError):
811
- return None
812
- folder = data.get("folder") if isinstance(data, dict) else None
813
- if isinstance(folder, str) and folder.startswith("file://"):
814
- path = unquote(urlparse(folder).path)
815
- # Windows file URLs parse to "/C:/..." — drop the spurious leading slash.
816
- if re.match(r"^/[A-Za-z]:/", path):
817
- path = path[1:]
818
- return path or None
819
- return folder if isinstance(folder, str) and folder else None
820
-
821
-
822
628
  # --- shared helpers --------------------------------------------------------
823
629
  def _iter_jsonl(path: Path) -> Iterator[dict[str, Any]]:
824
630
  try:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "coding-bridge"
7
- version = "2026.7.2.1"
7
+ version = "2026.7.2.3"
8
8
  description = "Node daemon for AceDataCloud Coding Bridge — run Claude Code on your own machine and drive it from the web."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -139,7 +139,6 @@ def _patch_roots(monkeypatch, tmp_path):
139
139
  monkeypatch.setattr(history, "CODEX_ROOT", codex_root)
140
140
  monkeypatch.setattr(history, "CODEX_INDEX", codex_index)
141
141
  monkeypatch.setattr(history, "COPILOT_ROOT", copilot_root)
142
- monkeypatch.setattr(history, "VSCODE_CHAT_ROOTS", [tmp_path / "vscode"])
143
142
 
144
143
 
145
144
  def _seed_copilot(root):
@@ -375,7 +374,6 @@ def test_copilot_falls_back_to_transformed_content(monkeypatch, tmp_path):
375
374
  monkeypatch.setattr(history, "CODEX_ROOT", tmp_path / "none-codex")
376
375
  monkeypatch.setattr(history, "CODEX_INDEX", tmp_path / "none.jsonl")
377
376
  monkeypatch.setattr(history, "COPILOT_ROOT", copilot_root)
378
- monkeypatch.setattr(history, "VSCODE_CHAT_ROOTS", [tmp_path / "none-vscode"])
379
377
  copilot = next(s for s in history.list_sessions() if s["provider"] == "copilot")
380
378
  assert copilot["title"] == "show me recent sessions"
381
379
  detail = history.read_session("copilot", COPILOT_SID)
@@ -601,107 +599,3 @@ async def test_dispatch_history_get_requires_params():
601
599
  await conn._dispatch({"action": Action.HISTORY_GET})
602
600
  errors = _payloads(conn, Event.SESSION_ERROR)
603
601
  assert errors and "required" in errors[0]["message"]
604
-
605
-
606
- VSCODE_SID = "55555555-5555-5555-5555-555555555555"
607
-
608
-
609
- def _seed_vscode_chat(storage_root):
610
- ws = storage_root / "abc123hash"
611
- (ws / "chatSessions").mkdir(parents=True, exist_ok=True)
612
- (ws / "workspace.json").write_text(
613
- json.dumps({"folder": "file:///Users/me/My%20Proj"}), encoding="utf-8"
614
- )
615
- # v3 delta log: header line then a kind:2 line carrying the request objects.
616
- records = [
617
- {"kind": 0, "v": {"version": 3, "sessionId": VSCODE_SID, "requests": []}},
618
- {
619
- "kind": 2,
620
- "v": [
621
- {
622
- "requestId": "req_1",
623
- "timestamp": 1750000000000,
624
- "modelId": "copilot/claude-opus-4.8",
625
- "message": {"text": "fix the parser"},
626
- "response": [
627
- {"kind": "thinking", "value": "let me look", "id": "t0"},
628
- {"value": "Here is the fix.", "supportThemeIcons": False},
629
- {
630
- "kind": "toolInvocationSerialized",
631
- "toolId": "copilot_readFile",
632
- "toolCallId": "call_1",
633
- "invocationMessage": {"value": "Reading file"},
634
- "isComplete": True,
635
- },
636
- ],
637
- }
638
- ],
639
- },
640
- ]
641
- _write_jsonl(ws / "chatSessions" / f"{VSCODE_SID}.jsonl", records)
642
-
643
-
644
- def test_vscode_chat_list_and_read(monkeypatch, tmp_path):
645
- storage = tmp_path / "workspaceStorage"
646
- _seed_vscode_chat(storage)
647
- monkeypatch.setattr(history, "VSCODE_CHAT_ROOTS", [storage])
648
- monkeypatch.setattr(history, "CLAUDE_ROOT", tmp_path / "none1")
649
- monkeypatch.setattr(history, "CODEX_ROOT", tmp_path / "none2")
650
- monkeypatch.setattr(history, "COPILOT_ROOT", tmp_path / "none3")
651
-
652
- sessions = history.list_sessions()
653
- vs = [s for s in sessions if s["session_id"] == VSCODE_SID]
654
- assert len(vs) == 1
655
- s = vs[0]
656
- assert s["provider"] == "copilot" # surfaced as ordinary Copilot, no "vscode"
657
- assert "origin" not in s
658
- assert s["title"] == "fix the parser"
659
- assert s["cwd"] == "/Users/me/My Proj" # file:// unquoted
660
-
661
- detail = history.read_session("copilot", VSCODE_SID)
662
- assert "origin" not in detail
663
- assert detail["model"] == "copilot/claude-opus-4.8"
664
- kinds = [e["kind"] for e in detail["events"]]
665
- assert kinds == ["prompt", "thinking", "text", "tool_use"]
666
- assert detail["events"][0]["text"] == "fix the parser"
667
- assert detail["events"][2]["text"] == "Here is the fix."
668
- assert detail["events"][3]["tool"] == "copilot_readFile"
669
-
670
-
671
- def test_vscode_chat_path_rejects_traversal(monkeypatch, tmp_path):
672
- storage = tmp_path / "workspaceStorage"
673
- storage.mkdir(parents=True, exist_ok=True)
674
- monkeypatch.setattr(history, "VSCODE_CHAT_ROOTS", [storage])
675
- monkeypatch.setattr(history, "COPILOT_ROOT", tmp_path / "none")
676
- assert history._vscode_chat_path("..") is None
677
- assert history._vscode_chat_path(".") is None
678
- with pytest.raises(FileNotFoundError):
679
- history.read_session("copilot", "..")
680
-
681
-
682
- def test_vscode_chat_tolerates_malformed(monkeypatch, tmp_path):
683
- storage = tmp_path / "workspaceStorage"
684
- ws = storage / "h" / "chatSessions"
685
- ws.mkdir(parents=True, exist_ok=True)
686
- (ws / f"{VSCODE_SID}.jsonl").write_text(
687
- "\n".join(
688
- [
689
- "not json",
690
- json.dumps({"kind": 1, "v": "just a string"}),
691
- json.dumps({"kind": 2, "v": [{"message": ["bad"], "response": []}]}),
692
- json.dumps(
693
- {
694
- "kind": 2,
695
- "v": [{"requestId": "r", "message": {"text": "real"}, "response": []}],
696
- }
697
- ),
698
- ]
699
- )
700
- + "\n",
701
- encoding="utf-8",
702
- )
703
- monkeypatch.setattr(history, "VSCODE_CHAT_ROOTS", [storage])
704
- monkeypatch.setattr(history, "COPILOT_ROOT", tmp_path / "none")
705
- detail = history.read_session("copilot", VSCODE_SID)
706
- prompts = [e["text"] for e in detail["events"] if e["kind"] == "prompt"]
707
- assert prompts == ["real"]