pcode 0.1.0__py3-none-any.whl

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 (163) hide show
  1. pcode/__init__.py +7 -0
  2. pcode/_job_supervisor.py +65 -0
  3. pcode/agent.py +592 -0
  4. pcode/anthropic_oauth.py +469 -0
  5. pcode/app.py +3165 -0
  6. pcode/appearance.py +181 -0
  7. pcode/aside.py +541 -0
  8. pcode/aside_guard.py +39 -0
  9. pcode/aside_ui.py +683 -0
  10. pcode/auth.py +113 -0
  11. pcode/block.py +39 -0
  12. pcode/browser.py +393 -0
  13. pcode/cache_diagnostics.py +336 -0
  14. pcode/cache_settings.py +58 -0
  15. pcode/cache_warnings.py +139 -0
  16. pcode/claude_sdk/__init__.py +96 -0
  17. pcode/claude_sdk/cli.py +52 -0
  18. pcode/claude_sdk/errors.py +61 -0
  19. pcode/claude_sdk/messages.py +154 -0
  20. pcode/claude_sdk/model.py +179 -0
  21. pcode/claude_sdk/resume.py +114 -0
  22. pcode/claude_sdk/session.py +547 -0
  23. pcode/claude_sdk/session_pool.py +251 -0
  24. pcode/claude_sdk/workspace.py +34 -0
  25. pcode/cli.py +73 -0
  26. pcode/clipboard.py +76 -0
  27. pcode/code_mode.py +43 -0
  28. pcode/codex_login.py +189 -0
  29. pcode/command_transcript.py +71 -0
  30. pcode/commands.py +145 -0
  31. pcode/compaction.py +457 -0
  32. pcode/completion.py +263 -0
  33. pcode/config.py +223 -0
  34. pcode/context_breakdown.py +178 -0
  35. pcode/context_usage.py +52 -0
  36. pcode/controller.py +3496 -0
  37. pcode/conversation_ids.py +27 -0
  38. pcode/conversation_tree.py +160 -0
  39. pcode/copy_ui.py +172 -0
  40. pcode/delegation.py +236 -0
  41. pcode/delta.py +295 -0
  42. pcode/diagnostics.py +226 -0
  43. pcode/edit_preview.py +158 -0
  44. pcode/edit_transcript.py +287 -0
  45. pcode/edit_ui.py +850 -0
  46. pcode/edits.py +162 -0
  47. pcode/email_remote/__init__.py +14 -0
  48. pcode/email_remote/cli.py +133 -0
  49. pcode/email_remote/listener.py +695 -0
  50. pcode/email_remote/mailbox.py +336 -0
  51. pcode/email_remote/outbound.py +75 -0
  52. pcode/email_remote/parsing.py +226 -0
  53. pcode/email_remote/render.py +175 -0
  54. pcode/email_remote/state.py +79 -0
  55. pcode/error_report.py +38 -0
  56. pcode/ext.py +602 -0
  57. pcode/extension_guide.md +311 -0
  58. pcode/extensions/browser.py +284 -0
  59. pcode/extensions/ponytail.py +217 -0
  60. pcode/extensions/sandbox.py +138 -0
  61. pcode/extensions/session_history.py +170 -0
  62. pcode/extensions/web_research.py +89 -0
  63. pcode/file_refs.py +265 -0
  64. pcode/filesystem.py +138 -0
  65. pcode/frame.py +149 -0
  66. pcode/gateway.py +465 -0
  67. pcode/git_diff.py +497 -0
  68. pcode/history.py +589 -0
  69. pcode/history_cursor.py +147 -0
  70. pcode/history_embeddings.py +102 -0
  71. pcode/host.py +941 -0
  72. pcode/host_protocol.py +291 -0
  73. pcode/host_ui.py +225 -0
  74. pcode/input_keys.py +35 -0
  75. pcode/inspection.py +289 -0
  76. pcode/inspector_ui.py +448 -0
  77. pcode/isolated_delegation.py +425 -0
  78. pcode/job_notices.py +54 -0
  79. pcode/jobs.py +584 -0
  80. pcode/jobs_ui.py +265 -0
  81. pcode/keymap.py +215 -0
  82. pcode/layout_speed.py +103 -0
  83. pcode/links.py +90 -0
  84. pcode/links_ui.py +200 -0
  85. pcode/live.py +1301 -0
  86. pcode/llm_proxy.py +44 -0
  87. pcode/logins.py +233 -0
  88. pcode/mcp.py +683 -0
  89. pcode/mcp_notice.py +91 -0
  90. pcode/mcp_oauth.py +381 -0
  91. pcode/meridian.py +233 -0
  92. pcode/meridian_process.py +337 -0
  93. pcode/meridian_reminders.py +107 -0
  94. pcode/meridian_setup.py +356 -0
  95. pcode/model_metadata.py +451 -0
  96. pcode/model_ui.py +168 -0
  97. pcode/models.py +209 -0
  98. pcode/native_results.py +119 -0
  99. pcode/oauth_pages.py +66 -0
  100. pcode/output_limits.py +41 -0
  101. pcode/paste.py +45 -0
  102. pcode/path_completion.py +66 -0
  103. pcode/plan_preview.py +131 -0
  104. pcode/plan_sizes.py +35 -0
  105. pcode/planning.py +161 -0
  106. pcode/popup_ui.py +873 -0
  107. pcode/preferences.py +1176 -0
  108. pcode/prefix_keys.py +490 -0
  109. pcode/profile_benchmark.py +332 -0
  110. pcode/profiling.py +566 -0
  111. pcode/project_trust.py +94 -0
  112. pcode/prompt_keys.py +286 -0
  113. pcode/remote.py +717 -0
  114. pcode/remote_print.py +125 -0
  115. pcode/remote_profile.py +237 -0
  116. pcode/replay_benchmark.py +319 -0
  117. pcode/repo_context.py +158 -0
  118. pcode/retries.py +28 -0
  119. pcode/rpc.py +280 -0
  120. pcode/runtime.py +244 -0
  121. pcode/sandbox.py +454 -0
  122. pcode/session_naming.py +82 -0
  123. pcode/session_ui.py +550 -0
  124. pcode/sessions.py +892 -0
  125. pcode/shell.py +113 -0
  126. pcode/shell_mode.py +172 -0
  127. pcode/shell_tools.py +459 -0
  128. pcode/skills.py +161 -0
  129. pcode/stall_log.py +187 -0
  130. pcode/startup_input.py +96 -0
  131. pcode/steering.py +27 -0
  132. pcode/stream_display.py +188 -0
  133. pcode/stream_events.py +437 -0
  134. pcode/strict_tools.py +130 -0
  135. pcode/syntax.py +43 -0
  136. pcode/syntax_colors.py +206 -0
  137. pcode/task_prompt.py +28 -0
  138. pcode/task_worktrees.py +482 -0
  139. pcode/terminal_notify.py +383 -0
  140. pcode/theme.py +135 -0
  141. pcode/theme_gallery.py +88 -0
  142. pcode/thinking_markdown.py +41 -0
  143. pcode/token_accounting.py +67 -0
  144. pcode/tool_display.py +784 -0
  145. pcode/tool_output_limits.py +108 -0
  146. pcode/tool_panel.py +377 -0
  147. pcode/transcript_log.py +127 -0
  148. pcode/transcript_notice.py +103 -0
  149. pcode/tree_ui.py +288 -0
  150. pcode/turn.py +44 -0
  151. pcode/ui.py +3745 -0
  152. pcode/usage.py +293 -0
  153. pcode/word_wrap.py +107 -0
  154. pcode/worker_ui.py +235 -0
  155. pcode/workers.py +136 -0
  156. pcode/workspace.py +57 -0
  157. pcode/workspace_filesystem.py +72 -0
  158. pcode/worktree.py +650 -0
  159. pcode-0.1.0.dist-info/METADATA +145 -0
  160. pcode-0.1.0.dist-info/RECORD +163 -0
  161. pcode-0.1.0.dist-info/WHEEL +4 -0
  162. pcode-0.1.0.dist-info/entry_points.txt +3 -0
  163. pcode-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,147 @@
1
+ """Process-local continuations and bounded, snapshot journal reads for recall.
2
+
3
+ Continuations retain parser state, not a reusable search index. They expire and
4
+ never write transcript text to disk. Journal readers reopen at their byte offset
5
+ on each page, so no file descriptor is held while a caller considers a result.
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import threading
11
+ import time
12
+ from collections import OrderedDict
13
+ from collections.abc import Callable, Generator
14
+ from dataclasses import dataclass, field
15
+ from pathlib import Path
16
+ from typing import Any
17
+ from uuid import uuid4
18
+
19
+ from pcode.sessions import SessionInfo, SessionReadBudget
20
+
21
+ CURSOR_TTL = 30 * 60
22
+ MAX_CURSORS = 16
23
+
24
+
25
+ @dataclass
26
+ class Continuation:
27
+ binding: tuple
28
+ pages: Generator[tuple[Any, bool], None, None]
29
+ expires: float
30
+
31
+
32
+ _continuations: OrderedDict[str, Continuation] = OrderedDict()
33
+ _lock = threading.Lock()
34
+
35
+
36
+ def advance(
37
+ binding: tuple,
38
+ after: str | None,
39
+ pages: Callable[[], Generator[tuple[Any, bool], None, None]],
40
+ ):
41
+ """Advance a single-use cursor, returning the page and its next opaque token."""
42
+ now = time.monotonic()
43
+ with _lock:
44
+ for token, saved in list(_continuations.items()):
45
+ if saved.expires <= now:
46
+ del _continuations[token]
47
+ saved.pages.close()
48
+ if after:
49
+ saved = _continuations.get(after)
50
+ if saved is None:
51
+ raise ValueError("Unknown cursor or expired continuation; restart without after.")
52
+ if saved.binding != binding:
53
+ raise ValueError("Cursor does not match this scope or read request.")
54
+ del _continuations[after]
55
+ iterator = saved.pages
56
+ else:
57
+ iterator = pages()
58
+ try:
59
+ result, more = next(iterator)
60
+ except BaseException:
61
+ iterator.close()
62
+ raise
63
+ token = None
64
+ if more:
65
+ token = uuid4().hex
66
+ with _lock:
67
+ while len(_continuations) >= MAX_CURSORS:
68
+ _, saved = _continuations.popitem(last=False)
69
+ saved.pages.close()
70
+ _continuations[token] = Continuation(binding, iterator, time.monotonic() + CURSOR_TTL)
71
+ else:
72
+ iterator.close()
73
+ return result, token
74
+
75
+
76
+ @dataclass
77
+ class JournalReader:
78
+ """Read a finite journal snapshot, even when a JSON line spans several pages."""
79
+
80
+ info: SessionInfo
81
+ root: Path
82
+ kinds: tuple[str, ...]
83
+ offset: int = 0
84
+ pending: bytearray = field(default_factory=bytearray)
85
+ incomplete_tail: bool = False
86
+
87
+ def __post_init__(self):
88
+ self.path = self.root / self.info.id / "transcript.jsonl"
89
+ self._check_path()
90
+ stat = self.path.stat()
91
+ self.identity = (stat.st_dev, stat.st_ino)
92
+ self.size = stat.st_size
93
+ self.mtime = stat.st_mtime_ns
94
+ self.markers = tuple(f'"{kind}"'.encode() for kind in self.kinds)
95
+
96
+ @property
97
+ def done(self) -> bool:
98
+ return self.offset == self.size
99
+
100
+ def _check_path(self):
101
+ if self.path.parent.is_symlink() or self.path.is_symlink():
102
+ raise OSError("symlinked session")
103
+ if not (self.path.parent / "session.json").is_file():
104
+ raise OSError("session removed")
105
+
106
+ def validate(self, stat=None):
107
+ # Catches a replaced, shrunk, or same-size-edited journal. Journals are
108
+ # assumed append-only otherwise: a rewrite that grows the file looks
109
+ # exactly like an append and is not detected.
110
+ self._check_path()
111
+ stat = stat or self.path.stat()
112
+ if (
113
+ (stat.st_dev, stat.st_ino) != self.identity
114
+ or stat.st_size < self.size
115
+ or (stat.st_size == self.size and stat.st_mtime_ns != self.mtime)
116
+ ):
117
+ raise ValueError("Session journal changed; restart without after.")
118
+
119
+ def records(self, budget: SessionReadBudget):
120
+ self.validate()
121
+ with self.path.open("rb") as stream:
122
+ self.validate(os.fstat(stream.fileno()))
123
+ stream.seek(self.offset)
124
+ while not self.done and budget.remaining > 0:
125
+ part = stream.readline(min(budget.remaining, self.size - self.offset))
126
+ if not part:
127
+ raise ValueError("Session journal was truncated; restart without after.")
128
+ budget.remaining -= len(part)
129
+ self.offset += len(part)
130
+ self.pending.extend(part)
131
+ terminated = part.endswith(b"\n")
132
+ if not terminated and not self.done:
133
+ continue
134
+ line, self.pending = self.pending, bytearray()
135
+ if terminated and not any(marker in line for marker in self.markers):
136
+ continue
137
+ try:
138
+ record = json.loads(line.decode("utf-8", errors="replace"))
139
+ except ValueError:
140
+ # A complete final JSON object needs no newline, but a torn
141
+ # live write must not be mistaken for complete coverage.
142
+ if not terminated:
143
+ self.incomplete_tail = True
144
+ continue
145
+ if isinstance(record, dict) and record.get("kind") in self.kinds:
146
+ yield record
147
+ budget.exhausted = not self.done
@@ -0,0 +1,102 @@
1
+ """Opt-in semantic ranking; cache vectors locally, never transcript text."""
2
+
3
+ import asyncio
4
+ import hashlib
5
+ import json
6
+ import math
7
+ import sqlite3
8
+ from contextlib import closing
9
+ from pathlib import Path
10
+
11
+ from pcode.diagnostics import redact
12
+ from pcode.history import Chunk
13
+ from pcode.sessions import private_file
14
+
15
+ MAX_NEW_CHUNKS = 128
16
+ BATCH_SIZE = 32
17
+ CACHE_NAME = ".history-embeddings.sqlite3"
18
+
19
+
20
+ def _cache(path: Path, keys: list[str], updates: dict[str, list[float]] | None = None):
21
+ path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
22
+ private_file(path)
23
+ with closing(sqlite3.connect(path)) as db, db:
24
+ db.execute(
25
+ "CREATE TABLE IF NOT EXISTS vectors (key TEXT PRIMARY KEY, vector TEXT NOT NULL)"
26
+ )
27
+ if updates:
28
+ db.executemany(
29
+ "INSERT OR REPLACE INTO vectors VALUES (?, ?)",
30
+ [(key, json.dumps(vector)) for key, vector in updates.items()],
31
+ )
32
+ result = {}
33
+ for key in keys:
34
+ row = db.execute("SELECT vector FROM vectors WHERE key = ?", (key,)).fetchone()
35
+ if row:
36
+ result[key] = json.loads(row[0])
37
+ return result
38
+
39
+
40
+ def _unit(vector) -> list[float]:
41
+ values = [float(value) for value in vector]
42
+ if not values or not all(math.isfinite(value) for value in values):
43
+ raise ValueError("Invalid embedding vector")
44
+ length = math.hypot(*values)
45
+ if not length:
46
+ raise ValueError("Empty embedding vector")
47
+ return [value / length for value in values]
48
+
49
+
50
+ async def semantic_ranking(
51
+ chunks: list[Chunk], query: str, model: str, root: Path, *, embedder=None
52
+ ) -> tuple[list[int], list[str]]:
53
+ """Incrementally index only this search's scope, bounded to 128 new chunks per call."""
54
+ if not chunks:
55
+ return [], []
56
+ if embedder is None:
57
+ from pydantic_ai import Embedder
58
+
59
+ embedder = Embedder(model)
60
+ # Include format/model in the key: changing either cannot reuse incompatible vectors.
61
+ keys = [hashlib.sha256(f"v1\0{model}\0{c.text}".encode()).hexdigest() for c in chunks]
62
+ path = root / CACHE_NAME
63
+ cached = await asyncio.to_thread(_cache, path, keys)
64
+ missing = list(dict.fromkeys(key for key in keys if key not in cached))
65
+ texts = {key: chunk.text for key, chunk in zip(keys, chunks)}
66
+ for start in range(0, min(len(missing), MAX_NEW_CHUNKS), BATCH_SIZE):
67
+ batch = missing[start : min(start + BATCH_SIZE, MAX_NEW_CHUNKS)]
68
+ result = await embedder.embed_documents([texts[key] for key in batch])
69
+ if len(result.embeddings) != len(batch):
70
+ raise ValueError("Embedding count mismatch")
71
+ updates = {key: _unit(vector) for key, vector in zip(batch, result.embeddings)}
72
+ await asyncio.to_thread(_cache, path, [], updates)
73
+ cached.update(updates)
74
+ result = await embedder.embed_query(redact(query))
75
+ query_vector = _unit(result.embeddings[0])
76
+ scored = []
77
+ for index, key in enumerate(keys):
78
+ if key not in cached:
79
+ continue
80
+ vector = _unit(cached[key])
81
+ if len(vector) != len(query_vector):
82
+ raise ValueError("Embedding dimensions changed; clear the history embedding cache")
83
+ similarity = sum(a * b for a, b in zip(vector, query_vector))
84
+ if similarity > 0:
85
+ scored.append((similarity, index))
86
+ warnings = []
87
+ if len(missing) > MAX_NEW_CHUNKS:
88
+ warnings.append(
89
+ f"Semantic index is partial: {len(missing) - MAX_NEW_CHUNKS} chunks remain. "
90
+ "Later semantic searches index more; keyword search covers the scanned corpus."
91
+ )
92
+ ranking, seen = [], set()
93
+ for _, index in sorted(scored, key=lambda item: (-item[0], item[1])):
94
+ chunk = chunks[index]
95
+ identity = (chunk.session.id, chunk.turn.id)
96
+ if identity in seen:
97
+ continue
98
+ seen.add(identity)
99
+ ranking.append(index)
100
+ if len(ranking) == 50:
101
+ break
102
+ return ranking, warnings