k-cli-for-devs 1.0.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.
- k_cli/__init__.py +77 -0
- k_cli/agents/__init__.py +0 -0
- k_cli/agents/adversarial_swarm.py +338 -0
- k_cli/agents/agent_core.py +255 -0
- k_cli/agents/background_daemon.py +141 -0
- k_cli/agents/orchestrator.py +376 -0
- k_cli/agents/persona.py +649 -0
- k_cli/agents/scaffold_engine.py +121 -0
- k_cli/agents/strands_agent.py +832 -0
- k_cli/agents/subagents.py +1496 -0
- k_cli/cli.py +3297 -0
- k_cli/core/__init__.py +0 -0
- k_cli/core/airgap.py +95 -0
- k_cli/core/credentials.py +548 -0
- k_cli/core/intent_sensor.py +177 -0
- k_cli/core/llm_driver.py +1028 -0
- k_cli/core/model_manager.py +1109 -0
- k_cli/core/models_hub.py +913 -0
- k_cli/core/prompting.py +41 -0
- k_cli/core/sdk.py +322 -0
- k_cli/core/session.py +826 -0
- k_cli/core/smart_router.py +230 -0
- k_cli/core/storage_manager.py +176 -0
- k_cli/core/viewport_engine.py +117 -0
- k_cli/demo/demo_runner.py +579 -0
- k_cli/git/__init__.py +0 -0
- k_cli/git/ai_bisect.py +208 -0
- k_cli/git/conflict_resolver.py +1039 -0
- k_cli/git/git_guard.py +417 -0
- k_cli/git/patcher.py +1175 -0
- k_cli/git/repo_map.py +1780 -0
- k_cli/git/smart_git.py +928 -0
- k_cli/git/verifier.py +969 -0
- k_cli/github/__init__.py +0 -0
- k_cli/github/dedup_engine.py +787 -0
- k_cli/github/github_client.py +1702 -0
- k_cli/github/github_engine.py +641 -0
- k_cli/github/local_hub.py +209 -0
- k_cli/github/pr_watcher.py +129 -0
- k_cli/github/trending.py +205 -0
- k_cli/tools/__init__.py +0 -0
- k_cli/tools/audit.py +79 -0
- k_cli/tools/chaos_immunity.py +377 -0
- k_cli/tools/codebase_qa.py +106 -0
- k_cli/tools/command_runner.py +256 -0
- k_cli/tools/diagram_generator.py +547 -0
- k_cli/tools/doc_retriever.py +1332 -0
- k_cli/tools/feature.py +105 -0
- k_cli/tools/ghost_daemon.py +122 -0
- k_cli/tools/incident_triage.py +1365 -0
- k_cli/tools/mcp_client.py +1846 -0
- k_cli/tools/repo_gardener.py +142 -0
- k_cli/tools/rules.py +109 -0
- k_cli/tools/security.py +52 -0
- k_cli/tools/security_healer.py +999 -0
- k_cli/tools/synapse_graph.py +155 -0
- k_cli/tui/__init__.py +0 -0
- k_cli/tui/diff_viewer.py +223 -0
- k_cli/tui/tui.py +1145 -0
- k_cli/tui/tui_animations.py +648 -0
- k_cli/tui/tui_app.py +2788 -0
- k_cli/ui/__init__.py +10 -0
- k_cli/ui/simple_repl.py +315 -0
- k_cli/web/__init__.py +7 -0
- k_cli/web/server.py +624 -0
- k_cli/web/static/app.js +830 -0
- k_cli/web/static/index.html +495 -0
- k_cli/web/static/monitor.html +189 -0
- k_cli/web/static/style.css +838 -0
- k_cli_for_devs-1.0.0.dist-info/METADATA +461 -0
- k_cli_for_devs-1.0.0.dist-info/RECORD +75 -0
- k_cli_for_devs-1.0.0.dist-info/WHEEL +5 -0
- k_cli_for_devs-1.0.0.dist-info/entry_points.txt +2 -0
- k_cli_for_devs-1.0.0.dist-info/licenses/LICENSE +21 -0
- k_cli_for_devs-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
synapse_graph.py - AST-Indexed Neural Code Graph & Context Compressor for K-CLI
|
|
3
|
+
Project Bankai v1.0.0
|
|
4
|
+
|
|
5
|
+
Builds an in-memory & SQLite dependency graph of functions, classes, imports,
|
|
6
|
+
and call edges across the codebase, extracting minimal surgical AST subgraphs
|
|
7
|
+
for LLM prompts to achieve 95%+ token compression and sub-second latency.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import ast
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import sqlite3
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger("k_cli.tools.synapse_graph")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class CodeNode:
|
|
25
|
+
"""A node in the code graph representing a function, class, or module."""
|
|
26
|
+
id: str # "filepath::symbol"
|
|
27
|
+
name: str
|
|
28
|
+
kind: str # "function", "class", "module"
|
|
29
|
+
file_path: str
|
|
30
|
+
line_start: int
|
|
31
|
+
line_end: int
|
|
32
|
+
docstring: str = ""
|
|
33
|
+
dependencies: List[str] = field(default_factory=list)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class SynapseSlice:
|
|
38
|
+
"""Targeted AST subgraph slice extracted for an agent prompt."""
|
|
39
|
+
query: str
|
|
40
|
+
nodes: List[CodeNode] = field(default_factory=list)
|
|
41
|
+
raw_tokens_estimate: int = 0
|
|
42
|
+
compressed_tokens_estimate: int = 0
|
|
43
|
+
compression_ratio: float = 0.0
|
|
44
|
+
|
|
45
|
+
def render_context(self) -> str:
|
|
46
|
+
"""Renders minimal context slice."""
|
|
47
|
+
lines = [f"# 🧠 Synapse Code Subgraph ({len(self.nodes)} symbols, {self.compression_ratio:.1%} reduction):"]
|
|
48
|
+
for n in self.nodes:
|
|
49
|
+
lines.append(f"- [{n.kind.upper()}] `{n.id}` (lines {n.line_start}-{n.line_end})")
|
|
50
|
+
if n.docstring:
|
|
51
|
+
lines.append(f" Doc: {n.docstring[:100]}...")
|
|
52
|
+
return "\n".join(lines)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class SynapseCodeGraph:
|
|
56
|
+
"""
|
|
57
|
+
Codebase Graph Indexer & Minimal Context Extractor.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, repo_path: str = ".", db_path: Optional[str] = None):
|
|
61
|
+
self.repo_path = Path(repo_path).resolve()
|
|
62
|
+
self.db_path = Path(db_path) if db_path else self.repo_path / ".kcli" / "synapse.db"
|
|
63
|
+
self.nodes: Dict[str, CodeNode] = {}
|
|
64
|
+
self.call_edges: List[Tuple[str, str]] = [] # (caller_id, callee_id)
|
|
65
|
+
|
|
66
|
+
def index_codebase(self) -> int:
|
|
67
|
+
"""
|
|
68
|
+
Indexes all Python files into the graph. Returns total nodes indexed.
|
|
69
|
+
"""
|
|
70
|
+
self.nodes.clear()
|
|
71
|
+
self.call_edges.clear()
|
|
72
|
+
|
|
73
|
+
ignored_dirs = {".venv", "k_cli_env", ".git", ".pytest_cache", "__pycache__", "build", "dist", "data"}
|
|
74
|
+
py_files = [
|
|
75
|
+
p for p in self.repo_path.rglob("*.py")
|
|
76
|
+
if not any(ig in p.parts for ig in ignored_dirs) and not p.name.startswith("test_")
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
for p in py_files[:150]:
|
|
80
|
+
rel = str(p.relative_to(self.repo_path))
|
|
81
|
+
try:
|
|
82
|
+
content = p.read_text(encoding="utf-8", errors="ignore")
|
|
83
|
+
tree = ast.parse(content)
|
|
84
|
+
for node in ast.walk(tree):
|
|
85
|
+
if isinstance(node, ast.FunctionDef):
|
|
86
|
+
nid = f"{rel}::{node.name}"
|
|
87
|
+
doc = ast.get_docstring(node) or ""
|
|
88
|
+
self.nodes[nid] = CodeNode(
|
|
89
|
+
id=nid,
|
|
90
|
+
name=node.name,
|
|
91
|
+
kind="function",
|
|
92
|
+
file_path=rel,
|
|
93
|
+
line_start=node.lineno,
|
|
94
|
+
line_end=getattr(node, "end_lineno", node.lineno + 10),
|
|
95
|
+
docstring=doc,
|
|
96
|
+
)
|
|
97
|
+
elif isinstance(node, ast.ClassDef):
|
|
98
|
+
nid = f"{rel}::{node.name}"
|
|
99
|
+
doc = ast.get_docstring(node) or ""
|
|
100
|
+
self.nodes[nid] = CodeNode(
|
|
101
|
+
id=nid,
|
|
102
|
+
name=node.name,
|
|
103
|
+
kind="class",
|
|
104
|
+
file_path=rel,
|
|
105
|
+
line_start=node.lineno,
|
|
106
|
+
line_end=getattr(node, "end_lineno", node.lineno + 20),
|
|
107
|
+
docstring=doc,
|
|
108
|
+
)
|
|
109
|
+
except Exception:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
return len(self.nodes)
|
|
113
|
+
|
|
114
|
+
def extract_subgraph_slice(self, query: str, max_nodes: int = 15) -> SynapseSlice:
|
|
115
|
+
"""
|
|
116
|
+
Extracts only relevant AST nodes matching query keywords.
|
|
117
|
+
"""
|
|
118
|
+
if not self.nodes:
|
|
119
|
+
self.index_codebase()
|
|
120
|
+
|
|
121
|
+
query_tokens = set(query.lower().split())
|
|
122
|
+
matched: List[Tuple[int, CodeNode]] = []
|
|
123
|
+
|
|
124
|
+
for nid, node in self.nodes.items():
|
|
125
|
+
score = 0
|
|
126
|
+
name_lower = node.name.lower()
|
|
127
|
+
path_lower = node.file_path.lower()
|
|
128
|
+
doc_lower = node.docstring.lower()
|
|
129
|
+
|
|
130
|
+
for tok in query_tokens:
|
|
131
|
+
if tok in name_lower:
|
|
132
|
+
score += 10
|
|
133
|
+
if tok in path_lower:
|
|
134
|
+
score += 5
|
|
135
|
+
if tok in doc_lower:
|
|
136
|
+
score += 2
|
|
137
|
+
|
|
138
|
+
if score > 0:
|
|
139
|
+
matched.append((score, node))
|
|
140
|
+
|
|
141
|
+
matched.sort(key=lambda x: x[0], reverse=True)
|
|
142
|
+
selected = [node for _, node in matched[:max_nodes]]
|
|
143
|
+
|
|
144
|
+
# Estimates
|
|
145
|
+
raw_tokens = len(list(self.repo_path.rglob("*.py"))) * 600
|
|
146
|
+
comp_tokens = max(100, len(selected) * 45)
|
|
147
|
+
ratio = max(0.0, 1.0 - (comp_tokens / max(1, raw_tokens)))
|
|
148
|
+
|
|
149
|
+
return SynapseSlice(
|
|
150
|
+
query=query,
|
|
151
|
+
nodes=selected,
|
|
152
|
+
raw_tokens_estimate=raw_tokens,
|
|
153
|
+
compressed_tokens_estimate=comp_tokens,
|
|
154
|
+
compression_ratio=ratio,
|
|
155
|
+
)
|
k_cli/tui/__init__.py
ADDED
|
File without changes
|
k_cli/tui/diff_viewer.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""
|
|
2
|
+
diff_viewer.py - Surgical and Unified Diff Visualizer for K-CLI
|
|
3
|
+
|
|
4
|
+
Provides high-speed, modern terminal diff visualizers using Rich:
|
|
5
|
+
- Side-by-Side (2-column) diff visualizer with synchronized line numbers and change highlights.
|
|
6
|
+
- Inline unified diff visualizer with colored additions/deletions and hunk header markers.
|
|
7
|
+
- Surgical SEARCH/REPLACE patch visualizer.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import difflib
|
|
13
|
+
from typing import List, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
from rich.console import Console, RenderableType
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
from rich.syntax import Syntax
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
from rich.text import Text
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DiffVisualizer:
|
|
23
|
+
"""Renders beautiful terminal visual diffs in inline or side-by-side formats."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, console: Optional[Console] = None):
|
|
26
|
+
self.console = console or Console()
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def render_inline_diff(
|
|
30
|
+
diff_text: str,
|
|
31
|
+
title: str = "Unified Diff",
|
|
32
|
+
border_style: str = "yellow",
|
|
33
|
+
) -> Panel:
|
|
34
|
+
"""
|
|
35
|
+
Renders a unified diff string with stylized line numbers and color-coded changes.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
diff_text: Standard unified diff text.
|
|
39
|
+
title: Title for the enclosing Rich Panel.
|
|
40
|
+
border_style: Color for panel border.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Rich Panel containing formatted diff.
|
|
44
|
+
"""
|
|
45
|
+
if not diff_text or not diff_text.strip():
|
|
46
|
+
return Panel(
|
|
47
|
+
Text("No changes (working tree clean)", style="dim italic"),
|
|
48
|
+
title=title,
|
|
49
|
+
border_style="dim",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
lines = diff_text.splitlines()
|
|
53
|
+
formatted_text = Text()
|
|
54
|
+
|
|
55
|
+
old_lineno = 0
|
|
56
|
+
new_lineno = 0
|
|
57
|
+
in_hunk = False
|
|
58
|
+
|
|
59
|
+
for line in lines:
|
|
60
|
+
if line.startswith("--- ") or line.startswith("+++ "):
|
|
61
|
+
formatted_text.append(f"{line}\n", style="bold cyan")
|
|
62
|
+
elif line.startswith("diff --git") or line.startswith("index "):
|
|
63
|
+
formatted_text.append(f"{line}\n", style="bold dim")
|
|
64
|
+
elif line.startswith("@@"):
|
|
65
|
+
in_hunk = True
|
|
66
|
+
# Parse hunk header e.g. @@ -1,5 +1,6 @@
|
|
67
|
+
formatted_text.append(f"\n{line}\n", style="bold magenta")
|
|
68
|
+
try:
|
|
69
|
+
parts = line.split("@@")[1].strip().split()
|
|
70
|
+
old_spec = parts[0][1:]
|
|
71
|
+
new_spec = parts[1][1:]
|
|
72
|
+
old_lineno = int(old_spec.split(",")[0])
|
|
73
|
+
new_lineno = int(new_spec.split(",")[0])
|
|
74
|
+
except Exception:
|
|
75
|
+
old_lineno = 1
|
|
76
|
+
new_lineno = 1
|
|
77
|
+
elif line.startswith("-") and not line.startswith("---"):
|
|
78
|
+
num_str = f"{old_lineno:4d} │ " if in_hunk else " - │ "
|
|
79
|
+
formatted_text.append(num_str, style="dim red")
|
|
80
|
+
formatted_text.append(f"{line}\n", style="bold red")
|
|
81
|
+
if in_hunk:
|
|
82
|
+
old_lineno += 1
|
|
83
|
+
elif line.startswith("+") and not line.startswith("+++"):
|
|
84
|
+
num_str = f" {new_lineno:4d} │ " if in_hunk else " + │ "
|
|
85
|
+
formatted_text.append(num_str, style="dim green")
|
|
86
|
+
formatted_text.append(f"{line}\n", style="bold green")
|
|
87
|
+
if in_hunk:
|
|
88
|
+
new_lineno += 1
|
|
89
|
+
else:
|
|
90
|
+
prefix = line[1:] if line.startswith(" ") else line
|
|
91
|
+
num_str = f"{old_lineno:4d} {new_lineno:4d} │ " if in_hunk else " │ "
|
|
92
|
+
formatted_text.append(num_str, style="dim gray")
|
|
93
|
+
formatted_text.append(f" {prefix}\n", style="bright_white")
|
|
94
|
+
if in_hunk:
|
|
95
|
+
old_lineno += 1
|
|
96
|
+
new_lineno += 1
|
|
97
|
+
|
|
98
|
+
return Panel(formatted_text, title=f"[bold yellow]{title}[/bold yellow]", border_style=border_style)
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def render_side_by_side(
|
|
102
|
+
cls,
|
|
103
|
+
old_code: str,
|
|
104
|
+
new_code: str,
|
|
105
|
+
old_title: str = "Original / Candidate",
|
|
106
|
+
new_title: str = "Modified / Repaired",
|
|
107
|
+
language: str = "python",
|
|
108
|
+
title: str = "Side-by-Side Diff Visualizer",
|
|
109
|
+
) -> Panel:
|
|
110
|
+
"""
|
|
111
|
+
Renders two code strings side-by-side in a 2-column table with aligned lines.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
old_code: Original code string.
|
|
115
|
+
new_code: Modified code string.
|
|
116
|
+
old_title: Header title for left column.
|
|
117
|
+
new_title: Header title for right column.
|
|
118
|
+
language: Programming language for syntax formatting.
|
|
119
|
+
title: Panel title.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Rich Panel containing 2-column table.
|
|
123
|
+
"""
|
|
124
|
+
old_lines = old_code.splitlines()
|
|
125
|
+
new_lines = new_code.splitlines()
|
|
126
|
+
|
|
127
|
+
matcher = difflib.SequenceMatcher(None, old_lines, new_lines)
|
|
128
|
+
|
|
129
|
+
table = Table(
|
|
130
|
+
show_header=True,
|
|
131
|
+
header_style="bold cyan",
|
|
132
|
+
expand=True,
|
|
133
|
+
box=None,
|
|
134
|
+
padding=(0, 1),
|
|
135
|
+
)
|
|
136
|
+
table.add_column(f"L#", justify="right", style="dim", width=4)
|
|
137
|
+
table.add_column(f"{old_title} (Before)", style="white", ratio=1)
|
|
138
|
+
table.add_column(f"│", justify="center", style="dim", width=1)
|
|
139
|
+
table.add_column(f"R#", justify="right", style="dim", width=4)
|
|
140
|
+
table.add_column(f"{new_title} (After)", style="white", ratio=1)
|
|
141
|
+
|
|
142
|
+
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
|
|
143
|
+
if tag == "equal":
|
|
144
|
+
for idx in range(i2 - i1):
|
|
145
|
+
l_no = str(i1 + idx + 1)
|
|
146
|
+
r_no = str(j1 + idx + 1)
|
|
147
|
+
l_text = Text(old_lines[i1 + idx], style="dim white")
|
|
148
|
+
r_text = Text(new_lines[j1 + idx], style="dim white")
|
|
149
|
+
table.add_row(l_no, l_text, "│", r_no, r_text)
|
|
150
|
+
|
|
151
|
+
elif tag == "replace":
|
|
152
|
+
max_len = max(i2 - i1, j2 - j1)
|
|
153
|
+
for idx in range(max_len):
|
|
154
|
+
if idx < (i2 - i1):
|
|
155
|
+
l_no = str(i1 + idx + 1)
|
|
156
|
+
l_text = Text(old_lines[i1 + idx], style="bold red")
|
|
157
|
+
else:
|
|
158
|
+
l_no = " "
|
|
159
|
+
l_text = Text("·", style="dim")
|
|
160
|
+
|
|
161
|
+
if idx < (j2 - j1):
|
|
162
|
+
r_no = str(j1 + idx + 1)
|
|
163
|
+
r_text = Text(new_lines[j1 + idx], style="bold green")
|
|
164
|
+
else:
|
|
165
|
+
r_no = " "
|
|
166
|
+
r_text = Text("·", style="dim")
|
|
167
|
+
|
|
168
|
+
table.add_row(l_no, l_text, "│", r_no, r_text)
|
|
169
|
+
|
|
170
|
+
elif tag == "delete":
|
|
171
|
+
for idx in range(i2 - i1):
|
|
172
|
+
l_no = str(i1 + idx + 1)
|
|
173
|
+
l_text = Text(old_lines[i1 + idx], style="bold red")
|
|
174
|
+
table.add_row(l_no, l_text, "│", " ", Text("·", style="dim"))
|
|
175
|
+
|
|
176
|
+
elif tag == "insert":
|
|
177
|
+
for idx in range(j2 - j1):
|
|
178
|
+
r_no = str(j1 + idx + 1)
|
|
179
|
+
r_text = Text(new_lines[j1 + idx], style="bold green")
|
|
180
|
+
table.add_row(" ", Text("·", style="dim"), "│", r_no, r_text)
|
|
181
|
+
|
|
182
|
+
return Panel(table, title=f"[bold yellow]{title}[/bold yellow]", border_style="yellow")
|
|
183
|
+
|
|
184
|
+
@classmethod
|
|
185
|
+
def render_diff_auto(
|
|
186
|
+
cls,
|
|
187
|
+
diff_text: str,
|
|
188
|
+
old_code: Optional[str] = None,
|
|
189
|
+
new_code: Optional[str] = None,
|
|
190
|
+
side_by_side: bool = False,
|
|
191
|
+
title: str = "Diff",
|
|
192
|
+
) -> Panel:
|
|
193
|
+
"""
|
|
194
|
+
Auto-renders diff in either side-by-side or inline view.
|
|
195
|
+
"""
|
|
196
|
+
if side_by_side and old_code is not None and new_code is not None:
|
|
197
|
+
return cls.render_side_by_side(old_code, new_code, title=title)
|
|
198
|
+
return cls.render_inline_diff(diff_text, title=title)
|
|
199
|
+
|
|
200
|
+
@classmethod
|
|
201
|
+
def render_surgical_patch_preview(
|
|
202
|
+
cls,
|
|
203
|
+
search_block: str,
|
|
204
|
+
replace_block: str,
|
|
205
|
+
file_path: str = "file.py",
|
|
206
|
+
) -> Panel:
|
|
207
|
+
"""
|
|
208
|
+
Renders a SEARCH/REPLACE surgical patch block preview.
|
|
209
|
+
"""
|
|
210
|
+
table = Table(show_header=True, header_style="bold magenta", expand=True, box=None)
|
|
211
|
+
table.add_column("[bold red]SEARCH (Target to Replace)[/bold red]", ratio=1)
|
|
212
|
+
table.add_column("│", justify="center", style="dim", width=1)
|
|
213
|
+
table.add_column("[bold green]REPLACE (Replacement Block)[/bold green]", ratio=1)
|
|
214
|
+
|
|
215
|
+
search_syn = Syntax(search_block.strip() or "(empty)", "python", theme="monokai", line_numbers=True)
|
|
216
|
+
replace_syn = Syntax(replace_block.strip() or "(empty)", "python", theme="monokai", line_numbers=True)
|
|
217
|
+
|
|
218
|
+
table.add_row(search_syn, "│", replace_syn)
|
|
219
|
+
return Panel(
|
|
220
|
+
table,
|
|
221
|
+
title=f"[bold cyan]Surgical Patch Block: {file_path}[/bold cyan]",
|
|
222
|
+
border_style="cyan",
|
|
223
|
+
)
|