npcsh 1.1.18__py3-none-any.whl → 1.1.19__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.
- npcsh/_state.py +8 -0
- npcsh/benchmark/npcsh_agent.py +47 -16
- npcsh/config.py +1 -0
- npcsh/diff_viewer.py +452 -0
- npcsh/npc_team/jinxs/bin/config_tui.jinx +299 -0
- npcsh/npc_team/jinxs/bin/memories.jinx +316 -0
- npcsh/npc_team/jinxs/bin/setup.jinx +240 -0
- npcsh/npc_team/jinxs/bin/sync.jinx +143 -150
- npcsh/npc_team/jinxs/bin/team_tui.jinx +327 -0
- npcsh/npc_team/jinxs/incognide/add_tab.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/close_pane.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/close_tab.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/confirm.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/focus_pane.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/list_panes.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/navigate.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/notify.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/open_pane.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/read_pane.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/run_terminal.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/send_message.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/split_pane.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/switch_npc.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/switch_tab.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/write_file.jinx +1 -1
- npcsh/npc_team/jinxs/incognide/zen_mode.jinx +1 -1
- npcsh/npc_team/jinxs/modes/guac.jinx +0 -2
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/add_tab.jinx +1 -1
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_pane.jinx +1 -1
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_tab.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/config_tui.jinx +299 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/confirm.jinx +1 -1
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/focus_pane.jinx +1 -1
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/guac.jinx +0 -2
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/list_panes.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/memories.jinx +316 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/navigate.jinx +1 -1
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/notify.jinx +1 -1
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/open_pane.jinx +1 -1
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/read_pane.jinx +1 -1
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/run_terminal.jinx +1 -1
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/send_message.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/setup.jinx +240 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/split_pane.jinx +1 -1
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch_npc.jinx +1 -1
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch_tab.jinx +1 -1
- npcsh-1.1.19.data/data/npcsh/npc_team/sync.jinx +223 -0
- npcsh-1.1.19.data/data/npcsh/npc_team/team_tui.jinx +327 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/write_file.jinx +1 -1
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/zen_mode.jinx +1 -1
- {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/METADATA +21 -14
- {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/RECORD +138 -129
- {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/entry_points.txt +4 -0
- npcsh-1.1.18.data/data/npcsh/npc_team/sync.jinx +0 -230
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/alicanto.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/alicanto.npc +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/alicanto.png +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/arxiv.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/benchmark.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/build.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/chat.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/click.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/cmd.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/compile.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/compress.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/convene.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca.npc +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca.png +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/corca_example.png +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/db_search.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/delegate.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/file_search.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/frederic.npc +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/frederic4.png +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/guac.npc +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/guac.png +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/help.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/incognide.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/init.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/jinxs.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/kadiefa.png +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/key_press.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/kg_search.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/load_file.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/mem_review.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/mem_search.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/nql.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/ots.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/paper_search.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/paste.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonk.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonk.npc +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonk.png +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/plonkjr.png +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/pti.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/python.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/reattach.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/roll.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sample.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/search.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/semantic_scholar.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/serve.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/set.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sh.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/shh.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sibiji.npc +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sibiji.png +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sleep.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/spool.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/spool.png +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/sql.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switch.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/switches.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/trigger.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/type_text.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/usage.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/verbose.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/vixynt.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/wait.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/wander.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/web_search.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/yap.jinx +0 -0
- {npcsh-1.1.18.data → npcsh-1.1.19.data}/data/npcsh/npc_team/yap.png +0 -0
- {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/WHEEL +0 -0
- {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/licenses/LICENSE +0 -0
- {npcsh-1.1.18.dist-info → npcsh-1.1.19.dist-info}/top_level.txt +0 -0
npcsh/_state.py
CHANGED
|
@@ -125,6 +125,7 @@ from .config import (
|
|
|
125
125
|
NPCSH_API_URL,
|
|
126
126
|
NPCSH_SEARCH_PROVIDER,
|
|
127
127
|
NPCSH_BUILD_KG,
|
|
128
|
+
NPCSH_EDIT_APPROVAL,
|
|
128
129
|
setup_npcsh_config,
|
|
129
130
|
is_npcsh_initialized,
|
|
130
131
|
set_npcsh_initialized,
|
|
@@ -185,6 +186,10 @@ class ShellState:
|
|
|
185
186
|
session_start_time: float = field(default_factory=lambda: __import__('time').time())
|
|
186
187
|
# Logging level: "silent", "normal", "verbose"
|
|
187
188
|
log_level: str = "normal"
|
|
189
|
+
# Edit approval mode: "off", "interactive", "auto"
|
|
190
|
+
edit_approval: str = NPCSH_EDIT_APPROVAL
|
|
191
|
+
# Pending file edits for approval
|
|
192
|
+
pending_edits: Dict[str, Dict[str, str]] = field(default_factory=dict)
|
|
188
193
|
|
|
189
194
|
def get_model_for_command(self, model_type: str = "chat"):
|
|
190
195
|
if model_type == "chat":
|
|
@@ -271,6 +276,8 @@ CONFIG_KEY_MAP = {
|
|
|
271
276
|
"stream": "NPCSH_STREAM_OUTPUT",
|
|
272
277
|
"apiurl": "NPCSH_API_URL",
|
|
273
278
|
"buildkg": "NPCSH_BUILD_KG",
|
|
279
|
+
"editapproval": "NPCSH_EDIT_APPROVAL",
|
|
280
|
+
"approval": "NPCSH_EDIT_APPROVAL",
|
|
274
281
|
}
|
|
275
282
|
|
|
276
283
|
|
|
@@ -315,6 +322,7 @@ def set_npcsh_config_value(key: str, value: str):
|
|
|
315
322
|
"NPCSH_BUILD_KG": "build_kg",
|
|
316
323
|
"NPCSH_API_URL": "api_url",
|
|
317
324
|
"NPCSH_STREAM_OUTPUT": "stream_output",
|
|
325
|
+
"NPCSH_EDIT_APPROVAL": "edit_approval",
|
|
318
326
|
}
|
|
319
327
|
if env_key in field_map:
|
|
320
328
|
setattr(ShellState, field_map[env_key], parsed_val)
|
npcsh/benchmark/npcsh_agent.py
CHANGED
|
@@ -53,14 +53,26 @@ class NpcshAgent(BaseInstalledAgent):
|
|
|
53
53
|
Returns:
|
|
54
54
|
List of ExecInput commands to execute
|
|
55
55
|
"""
|
|
56
|
-
# Wrap the instruction with explicit jinx usage directions
|
|
56
|
+
# Wrap the instruction with explicit jinx usage directions and retry logic
|
|
57
57
|
tool_instruction = f"""You have access to jinxs including edit_file (for writing/creating files), sh (for running shell commands), and python (for running Python code).
|
|
58
58
|
|
|
59
|
-
IMPORTANT:
|
|
59
|
+
IMPORTANT RULES:
|
|
60
|
+
1. You MUST use these jinxs to complete the task. Do NOT just output code as text - use the edit_file jinx to actually write files to disk.
|
|
61
|
+
2. After implementing a solution, you MUST verify it works by running any provided test scripts.
|
|
62
|
+
3. If a test fails or produces an error, you MUST try a DIFFERENT approach. Do not give up.
|
|
63
|
+
4. Keep trying different approaches until you succeed or have tried at least 10 different solutions.
|
|
64
|
+
5. NEVER assume success - always check the actual output of test commands.
|
|
60
65
|
|
|
61
66
|
Task: {instruction}
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
WORKFLOW:
|
|
69
|
+
1. Implement your solution using edit_file and sh
|
|
70
|
+
2. Run any test scripts mentioned in the task
|
|
71
|
+
3. Check the output carefully - look for "PASS", "SUCCESS", "OK" or similar
|
|
72
|
+
4. If the test failed, analyze why and try a completely different approach
|
|
73
|
+
5. Repeat until the test passes
|
|
74
|
+
|
|
75
|
+
Remember: Use edit_file to write code files. Use sh to run commands. VERIFY your solution works before concluding."""
|
|
64
76
|
|
|
65
77
|
escaped_instruction = shlex.quote(tool_instruction)
|
|
66
78
|
model_name = self.model_name
|
|
@@ -90,18 +102,25 @@ Remember: Use edit_file to write any code files. Use sh to run shell commands li
|
|
|
90
102
|
# Build environment variables for API keys
|
|
91
103
|
env_vars = []
|
|
92
104
|
api_key_map = {
|
|
93
|
-
"anthropic": "ANTHROPIC_API_KEY",
|
|
94
|
-
"openai": "OPENAI_API_KEY",
|
|
95
|
-
"gemini": "GOOGLE_API_KEY",
|
|
96
|
-
"google": "GOOGLE_API_KEY",
|
|
97
|
-
"deepseek": "DEEPSEEK_API_KEY",
|
|
98
|
-
"groq": "GROQ_API_KEY",
|
|
99
|
-
"openrouter": "OPENROUTER_API_KEY",
|
|
105
|
+
"anthropic": ["ANTHROPIC_API_KEY"],
|
|
106
|
+
"openai": ["OPENAI_API_KEY"],
|
|
107
|
+
"gemini": ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
|
|
108
|
+
"google": ["GOOGLE_API_KEY", "GEMINI_API_KEY"],
|
|
109
|
+
"deepseek": ["DEEPSEEK_API_KEY"],
|
|
110
|
+
"groq": ["GROQ_API_KEY"],
|
|
111
|
+
"openrouter": ["OPENROUTER_API_KEY"],
|
|
100
112
|
}
|
|
101
113
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
114
|
+
added_keys = set()
|
|
115
|
+
for prov, env_keys in api_key_map.items():
|
|
116
|
+
for env_key in env_keys:
|
|
117
|
+
if env_key in os.environ:
|
|
118
|
+
# For Gemini, always pass as GOOGLE_API_KEY (what litellm expects)
|
|
119
|
+
target_key = "GOOGLE_API_KEY" if env_key == "GEMINI_API_KEY" else env_key
|
|
120
|
+
if target_key not in added_keys:
|
|
121
|
+
env_vars.append(f'{target_key}="{os.environ[env_key]}"')
|
|
122
|
+
added_keys.add(target_key)
|
|
123
|
+
break
|
|
105
124
|
|
|
106
125
|
env_prefix = " ".join(env_vars) + " " if env_vars else ""
|
|
107
126
|
|
|
@@ -215,14 +234,26 @@ class NpcshAgentWithNpc(NpcshAgent):
|
|
|
215
234
|
|
|
216
235
|
def create_run_agent_commands(self, instruction: str) -> list:
|
|
217
236
|
"""Create commands using a specific NPC."""
|
|
218
|
-
# Wrap the instruction with explicit jinx usage directions
|
|
237
|
+
# Wrap the instruction with explicit jinx usage directions and retry logic
|
|
219
238
|
tool_instruction = f"""You have access to jinxs including edit_file (for writing/creating files), sh (for running shell commands), and python (for running Python code).
|
|
220
239
|
|
|
221
|
-
IMPORTANT:
|
|
240
|
+
IMPORTANT RULES:
|
|
241
|
+
1. You MUST use these jinxs to complete the task. Do NOT just output code as text - use the edit_file jinx to actually write files to disk.
|
|
242
|
+
2. After implementing a solution, you MUST verify it works by running any provided test scripts.
|
|
243
|
+
3. If a test fails or produces an error, you MUST try a DIFFERENT approach. Do not give up.
|
|
244
|
+
4. Keep trying different approaches until you succeed or have tried at least 10 different solutions.
|
|
245
|
+
5. NEVER assume success - always check the actual output of test commands.
|
|
222
246
|
|
|
223
247
|
Task: {instruction}
|
|
224
248
|
|
|
225
|
-
|
|
249
|
+
WORKFLOW:
|
|
250
|
+
1. Implement your solution using edit_file and sh
|
|
251
|
+
2. Run any test scripts mentioned in the task
|
|
252
|
+
3. Check the output carefully - look for "PASS", "SUCCESS", "OK" or similar
|
|
253
|
+
4. If the test failed, analyze why and try a completely different approach
|
|
254
|
+
5. Repeat until the test passes
|
|
255
|
+
|
|
256
|
+
Remember: Use edit_file to write code files. Use sh to run commands. VERIFY your solution works before concluding."""
|
|
226
257
|
|
|
227
258
|
escaped_instruction = shlex.quote(tool_instruction)
|
|
228
259
|
model_name = self.model_name
|
npcsh/config.py
CHANGED
|
@@ -43,6 +43,7 @@ NPCSH_STREAM_OUTPUT = os.environ.get("NPCSH_STREAM_OUTPUT", "0") == "1"
|
|
|
43
43
|
NPCSH_API_URL = os.environ.get("NPCSH_API_URL", None)
|
|
44
44
|
NPCSH_SEARCH_PROVIDER = os.environ.get("NPCSH_SEARCH_PROVIDER", "duckduckgo")
|
|
45
45
|
NPCSH_BUILD_KG = os.environ.get("NPCSH_BUILD_KG", "1") != "0"
|
|
46
|
+
NPCSH_EDIT_APPROVAL = os.environ.get("NPCSH_EDIT_APPROVAL", "off") # off, interactive, auto
|
|
46
47
|
|
|
47
48
|
|
|
48
49
|
def get_shell_config_file() -> str:
|
npcsh/diff_viewer.py
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git-diff approval TUI for npcsh.
|
|
3
|
+
Provides interactive diff viewing with approve/reject functionality.
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import difflib
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import List, Dict, Optional, Tuple
|
|
10
|
+
from enum import Enum
|
|
11
|
+
|
|
12
|
+
# Platform-specific imports
|
|
13
|
+
try:
|
|
14
|
+
import tty
|
|
15
|
+
import termios
|
|
16
|
+
import select
|
|
17
|
+
HAS_TTY = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
HAS_TTY = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class HunkDecision(Enum):
|
|
23
|
+
PENDING = "pending"
|
|
24
|
+
APPROVED = "approved"
|
|
25
|
+
REJECTED = "rejected"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class DiffHunk:
|
|
30
|
+
"""Represents a single diff hunk"""
|
|
31
|
+
start_original: int
|
|
32
|
+
count_original: int
|
|
33
|
+
start_modified: int
|
|
34
|
+
count_modified: int
|
|
35
|
+
lines: List[str]
|
|
36
|
+
header: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class DiffViewerState:
|
|
41
|
+
"""State for the diff viewer TUI"""
|
|
42
|
+
file_path: str
|
|
43
|
+
original: str
|
|
44
|
+
modified: str
|
|
45
|
+
hunks: List[DiffHunk] = field(default_factory=list)
|
|
46
|
+
decisions: Dict[int, HunkDecision] = field(default_factory=dict)
|
|
47
|
+
selected_hunk: int = 0
|
|
48
|
+
scroll_offset: int = 0
|
|
49
|
+
mode: str = "normal" # normal, help
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def compute_diff_hunks(original: str, modified: str) -> List[DiffHunk]:
|
|
53
|
+
"""Compute diff hunks between original and modified content."""
|
|
54
|
+
original_lines = original.splitlines(keepends=True)
|
|
55
|
+
modified_lines = modified.splitlines(keepends=True)
|
|
56
|
+
|
|
57
|
+
diff = list(difflib.unified_diff(
|
|
58
|
+
original_lines,
|
|
59
|
+
modified_lines,
|
|
60
|
+
lineterm=''
|
|
61
|
+
))
|
|
62
|
+
|
|
63
|
+
hunks = []
|
|
64
|
+
current_hunk_lines = []
|
|
65
|
+
current_header = ""
|
|
66
|
+
start_orig = 0
|
|
67
|
+
count_orig = 0
|
|
68
|
+
start_mod = 0
|
|
69
|
+
count_mod = 0
|
|
70
|
+
|
|
71
|
+
for line in diff[2:]: # Skip the --- and +++ headers
|
|
72
|
+
if line.startswith('@@'):
|
|
73
|
+
# Save previous hunk if exists
|
|
74
|
+
if current_hunk_lines:
|
|
75
|
+
hunks.append(DiffHunk(
|
|
76
|
+
start_original=start_orig,
|
|
77
|
+
count_original=count_orig,
|
|
78
|
+
start_modified=start_mod,
|
|
79
|
+
count_modified=count_mod,
|
|
80
|
+
lines=current_hunk_lines,
|
|
81
|
+
header=current_header
|
|
82
|
+
))
|
|
83
|
+
|
|
84
|
+
# Parse new hunk header
|
|
85
|
+
current_header = line.strip()
|
|
86
|
+
current_hunk_lines = []
|
|
87
|
+
|
|
88
|
+
# Parse @@ -start,count +start,count @@
|
|
89
|
+
try:
|
|
90
|
+
parts = line.split('@@')[1].strip().split()
|
|
91
|
+
orig_part = parts[0] # -start,count
|
|
92
|
+
mod_part = parts[1] # +start,count
|
|
93
|
+
|
|
94
|
+
if ',' in orig_part:
|
|
95
|
+
start_orig, count_orig = map(int, orig_part[1:].split(','))
|
|
96
|
+
else:
|
|
97
|
+
start_orig = int(orig_part[1:])
|
|
98
|
+
count_orig = 1
|
|
99
|
+
|
|
100
|
+
if ',' in mod_part:
|
|
101
|
+
start_mod, count_mod = map(int, mod_part[1:].split(','))
|
|
102
|
+
else:
|
|
103
|
+
start_mod = int(mod_part[1:])
|
|
104
|
+
count_mod = 1
|
|
105
|
+
except (IndexError, ValueError):
|
|
106
|
+
start_orig = count_orig = start_mod = count_mod = 0
|
|
107
|
+
else:
|
|
108
|
+
current_hunk_lines.append(line)
|
|
109
|
+
|
|
110
|
+
# Save last hunk
|
|
111
|
+
if current_hunk_lines:
|
|
112
|
+
hunks.append(DiffHunk(
|
|
113
|
+
start_original=start_orig,
|
|
114
|
+
count_original=count_orig,
|
|
115
|
+
start_modified=start_mod,
|
|
116
|
+
count_modified=count_mod,
|
|
117
|
+
lines=current_hunk_lines,
|
|
118
|
+
header=current_header
|
|
119
|
+
))
|
|
120
|
+
|
|
121
|
+
return hunks
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_terminal_size() -> Tuple[int, int]:
|
|
125
|
+
"""Get terminal size (width, height)."""
|
|
126
|
+
try:
|
|
127
|
+
size = os.get_terminal_size()
|
|
128
|
+
return size.columns, size.lines
|
|
129
|
+
except:
|
|
130
|
+
return 80, 24
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class DiffViewer:
|
|
134
|
+
"""Interactive diff viewer with approve/reject functionality."""
|
|
135
|
+
|
|
136
|
+
def __init__(self, file_path: str, original: str, modified: str):
|
|
137
|
+
self.state = DiffViewerState(
|
|
138
|
+
file_path=file_path,
|
|
139
|
+
original=original,
|
|
140
|
+
modified=modified
|
|
141
|
+
)
|
|
142
|
+
self.state.hunks = compute_diff_hunks(original, modified)
|
|
143
|
+
|
|
144
|
+
# Initialize all hunks as pending
|
|
145
|
+
for i in range(len(self.state.hunks)):
|
|
146
|
+
self.state.decisions[i] = HunkDecision.PENDING
|
|
147
|
+
|
|
148
|
+
def render_screen(self):
|
|
149
|
+
"""Render the diff viewer screen."""
|
|
150
|
+
width, height = get_terminal_size()
|
|
151
|
+
out = []
|
|
152
|
+
|
|
153
|
+
# Clear screen and move to top
|
|
154
|
+
out.append("\033[2J\033[H")
|
|
155
|
+
|
|
156
|
+
# Header
|
|
157
|
+
header = f" File Edit: {self.state.file_path} "
|
|
158
|
+
if len(header) > width - 4:
|
|
159
|
+
header = f" ...{self.state.file_path[-width+15:]} "
|
|
160
|
+
out.append(f"\033[1;1H\033[44;37;1m{'=' * width}\033[0m")
|
|
161
|
+
out.append(f"\033[1;2H\033[44;37;1m{header}\033[0m")
|
|
162
|
+
|
|
163
|
+
# Stats line
|
|
164
|
+
approved = sum(1 for d in self.state.decisions.values() if d == HunkDecision.APPROVED)
|
|
165
|
+
rejected = sum(1 for d in self.state.decisions.values() if d == HunkDecision.REJECTED)
|
|
166
|
+
pending = sum(1 for d in self.state.decisions.values() if d == HunkDecision.PENDING)
|
|
167
|
+
stats = f"Hunks: {len(self.state.hunks)} | Approved: {approved} | Rejected: {rejected} | Pending: {pending}"
|
|
168
|
+
out.append(f"\033[2;1H\033[90m{stats.center(width)}\033[0m")
|
|
169
|
+
|
|
170
|
+
# Diff content area
|
|
171
|
+
content_start = 4
|
|
172
|
+
content_height = height - 6
|
|
173
|
+
|
|
174
|
+
if not self.state.hunks:
|
|
175
|
+
out.append(f"\033[{content_start};2H\033[33mNo differences found.\033[0m")
|
|
176
|
+
else:
|
|
177
|
+
# Render current hunk
|
|
178
|
+
hunk = self.state.hunks[self.state.selected_hunk]
|
|
179
|
+
decision = self.state.decisions[self.state.selected_hunk]
|
|
180
|
+
|
|
181
|
+
# Hunk header with decision indicator
|
|
182
|
+
decision_indicator = {
|
|
183
|
+
HunkDecision.PENDING: "\033[33m[?]\033[0m",
|
|
184
|
+
HunkDecision.APPROVED: "\033[32m[+]\033[0m",
|
|
185
|
+
HunkDecision.REJECTED: "\033[31m[-]\033[0m"
|
|
186
|
+
}[decision]
|
|
187
|
+
|
|
188
|
+
hunk_header = f"{decision_indicator} Hunk {self.state.selected_hunk + 1}/{len(self.state.hunks)}: {hunk.header}"
|
|
189
|
+
out.append(f"\033[3;1H\033[90m{'-' * width}\033[0m")
|
|
190
|
+
out.append(f"\033[3;2H{hunk_header[:width-4]}")
|
|
191
|
+
|
|
192
|
+
# Render diff lines
|
|
193
|
+
visible_lines = hunk.lines[self.state.scroll_offset:self.state.scroll_offset + content_height]
|
|
194
|
+
|
|
195
|
+
for i, line in enumerate(visible_lines):
|
|
196
|
+
row = content_start + i
|
|
197
|
+
if row >= height - 2:
|
|
198
|
+
break
|
|
199
|
+
|
|
200
|
+
# Color based on line type
|
|
201
|
+
if line.startswith('+'):
|
|
202
|
+
color = "\033[32m" # Green for additions
|
|
203
|
+
elif line.startswith('-'):
|
|
204
|
+
color = "\033[31m" # Red for deletions
|
|
205
|
+
else:
|
|
206
|
+
color = "\033[0m" # Default for context
|
|
207
|
+
|
|
208
|
+
# Truncate long lines
|
|
209
|
+
display_line = line.rstrip()[:width-2]
|
|
210
|
+
out.append(f"\033[{row};1H{color}{display_line}\033[0m")
|
|
211
|
+
|
|
212
|
+
# Scroll indicator
|
|
213
|
+
if len(hunk.lines) > content_height:
|
|
214
|
+
scroll_pct = (self.state.scroll_offset / (len(hunk.lines) - content_height)) * 100
|
|
215
|
+
scroll_info = f"[{int(scroll_pct)}%]"
|
|
216
|
+
out.append(f"\033[{content_start};{width-len(scroll_info)-1}H\033[90m{scroll_info}\033[0m")
|
|
217
|
+
|
|
218
|
+
# Footer with keybindings
|
|
219
|
+
footer_y = height - 1
|
|
220
|
+
out.append(f"\033[{footer_y};1H\033[90m{'-' * width}\033[0m")
|
|
221
|
+
|
|
222
|
+
keys = "[a] Approve [r] Reject [A] Approve All [R] Reject All [j/k] Hunks [q] Done [?] Help"
|
|
223
|
+
out.append(f"\033[{height};1H\033[90m{keys[:width]}\033[0m")
|
|
224
|
+
|
|
225
|
+
sys.stdout.write(''.join(out))
|
|
226
|
+
sys.stdout.flush()
|
|
227
|
+
|
|
228
|
+
def handle_input(self, c: str) -> bool:
|
|
229
|
+
"""Handle input character. Returns False to exit."""
|
|
230
|
+
if c == 'q':
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
elif c == 'a': # Approve current hunk
|
|
234
|
+
self.state.decisions[self.state.selected_hunk] = HunkDecision.APPROVED
|
|
235
|
+
if self.state.selected_hunk < len(self.state.hunks) - 1:
|
|
236
|
+
self.state.selected_hunk += 1
|
|
237
|
+
self.state.scroll_offset = 0
|
|
238
|
+
|
|
239
|
+
elif c == 'r': # Reject current hunk
|
|
240
|
+
self.state.decisions[self.state.selected_hunk] = HunkDecision.REJECTED
|
|
241
|
+
if self.state.selected_hunk < len(self.state.hunks) - 1:
|
|
242
|
+
self.state.selected_hunk += 1
|
|
243
|
+
self.state.scroll_offset = 0
|
|
244
|
+
|
|
245
|
+
elif c == 'A': # Approve all
|
|
246
|
+
for i in range(len(self.state.hunks)):
|
|
247
|
+
self.state.decisions[i] = HunkDecision.APPROVED
|
|
248
|
+
|
|
249
|
+
elif c == 'R': # Reject all
|
|
250
|
+
for i in range(len(self.state.hunks)):
|
|
251
|
+
self.state.decisions[i] = HunkDecision.REJECTED
|
|
252
|
+
|
|
253
|
+
elif c == 'j' or c == '\x1b': # Down/next hunk (or escape sequence)
|
|
254
|
+
if c == '\x1b':
|
|
255
|
+
# Handle escape sequences
|
|
256
|
+
if HAS_TTY and select.select([sys.stdin], [], [], 0.05)[0]:
|
|
257
|
+
c2 = sys.stdin.read(1)
|
|
258
|
+
if c2 == '[':
|
|
259
|
+
c3 = sys.stdin.read(1)
|
|
260
|
+
if c3 == 'B': # Down arrow
|
|
261
|
+
if self.state.selected_hunk < len(self.state.hunks) - 1:
|
|
262
|
+
self.state.selected_hunk += 1
|
|
263
|
+
self.state.scroll_offset = 0
|
|
264
|
+
elif c3 == 'A': # Up arrow
|
|
265
|
+
if self.state.selected_hunk > 0:
|
|
266
|
+
self.state.selected_hunk -= 1
|
|
267
|
+
self.state.scroll_offset = 0
|
|
268
|
+
else:
|
|
269
|
+
if self.state.selected_hunk < len(self.state.hunks) - 1:
|
|
270
|
+
self.state.selected_hunk += 1
|
|
271
|
+
self.state.scroll_offset = 0
|
|
272
|
+
|
|
273
|
+
elif c == 'k': # Up/previous hunk
|
|
274
|
+
if self.state.selected_hunk > 0:
|
|
275
|
+
self.state.selected_hunk -= 1
|
|
276
|
+
self.state.scroll_offset = 0
|
|
277
|
+
|
|
278
|
+
elif c == 'n': # Next hunk (same as j)
|
|
279
|
+
if self.state.selected_hunk < len(self.state.hunks) - 1:
|
|
280
|
+
self.state.selected_hunk += 1
|
|
281
|
+
self.state.scroll_offset = 0
|
|
282
|
+
|
|
283
|
+
elif c == 'p': # Previous hunk (same as k)
|
|
284
|
+
if self.state.selected_hunk > 0:
|
|
285
|
+
self.state.selected_hunk -= 1
|
|
286
|
+
self.state.scroll_offset = 0
|
|
287
|
+
|
|
288
|
+
elif c == ' ': # Scroll down within hunk
|
|
289
|
+
if self.state.hunks:
|
|
290
|
+
hunk = self.state.hunks[self.state.selected_hunk]
|
|
291
|
+
_, height = get_terminal_size()
|
|
292
|
+
content_height = height - 6
|
|
293
|
+
max_scroll = max(0, len(hunk.lines) - content_height)
|
|
294
|
+
self.state.scroll_offset = min(self.state.scroll_offset + 5, max_scroll)
|
|
295
|
+
|
|
296
|
+
elif c == 'b': # Scroll up within hunk
|
|
297
|
+
self.state.scroll_offset = max(0, self.state.scroll_offset - 5)
|
|
298
|
+
|
|
299
|
+
return True
|
|
300
|
+
|
|
301
|
+
def apply_decisions(self) -> str:
|
|
302
|
+
"""Apply decisions and return the resulting content."""
|
|
303
|
+
if not self.state.hunks:
|
|
304
|
+
return self.state.modified
|
|
305
|
+
|
|
306
|
+
# If all hunks approved, return modified
|
|
307
|
+
if all(d == HunkDecision.APPROVED for d in self.state.decisions.values()):
|
|
308
|
+
return self.state.modified
|
|
309
|
+
|
|
310
|
+
# If all hunks rejected, return original
|
|
311
|
+
if all(d == HunkDecision.REJECTED for d in self.state.decisions.values()):
|
|
312
|
+
return self.state.original
|
|
313
|
+
|
|
314
|
+
# Partial application - reconstruct from decisions
|
|
315
|
+
# This is complex - for now, we'll use a simple approach:
|
|
316
|
+
# If any hunk is rejected, we need to carefully reconstruct
|
|
317
|
+
|
|
318
|
+
result_lines = self.state.original.splitlines(keepends=True)
|
|
319
|
+
offset = 0 # Track line number offset from applied changes
|
|
320
|
+
|
|
321
|
+
for i, hunk in enumerate(self.state.hunks):
|
|
322
|
+
if self.state.decisions[i] == HunkDecision.APPROVED:
|
|
323
|
+
# Apply this hunk
|
|
324
|
+
start = hunk.start_original - 1 + offset
|
|
325
|
+
|
|
326
|
+
# Count removals and additions in this hunk
|
|
327
|
+
removals = [l[1:] for l in hunk.lines if l.startswith('-')]
|
|
328
|
+
additions = [l[1:] for l in hunk.lines if l.startswith('+')]
|
|
329
|
+
|
|
330
|
+
# Remove old lines
|
|
331
|
+
del result_lines[start:start + len(removals)]
|
|
332
|
+
|
|
333
|
+
# Insert new lines
|
|
334
|
+
for j, line in enumerate(additions):
|
|
335
|
+
if not line.endswith('\n'):
|
|
336
|
+
line += '\n'
|
|
337
|
+
result_lines.insert(start + j, line)
|
|
338
|
+
|
|
339
|
+
# Update offset
|
|
340
|
+
offset += len(additions) - len(removals)
|
|
341
|
+
|
|
342
|
+
return ''.join(result_lines)
|
|
343
|
+
|
|
344
|
+
def run(self) -> Dict[str, any]:
|
|
345
|
+
"""Run the interactive diff viewer. Returns approval decisions."""
|
|
346
|
+
if not HAS_TTY:
|
|
347
|
+
print("TTY not available - cannot run interactive diff viewer")
|
|
348
|
+
return {
|
|
349
|
+
"approved": False,
|
|
350
|
+
"decisions": {},
|
|
351
|
+
"content": self.state.original
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if not self.state.hunks:
|
|
355
|
+
return {
|
|
356
|
+
"approved": True,
|
|
357
|
+
"decisions": {},
|
|
358
|
+
"content": self.state.modified
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
fd = sys.stdin.fileno()
|
|
362
|
+
old_settings = termios.tcgetattr(fd)
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
tty.setcbreak(fd)
|
|
366
|
+
sys.stdout.write('\033[?25l') # Hide cursor
|
|
367
|
+
|
|
368
|
+
self.render_screen()
|
|
369
|
+
|
|
370
|
+
while True:
|
|
371
|
+
c = sys.stdin.read(1)
|
|
372
|
+
if not self.handle_input(c):
|
|
373
|
+
break
|
|
374
|
+
self.render_screen()
|
|
375
|
+
|
|
376
|
+
finally:
|
|
377
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
378
|
+
sys.stdout.write('\033[?25h') # Show cursor
|
|
379
|
+
sys.stdout.write('\033[2J\033[H') # Clear screen
|
|
380
|
+
sys.stdout.flush()
|
|
381
|
+
|
|
382
|
+
# Determine if approved
|
|
383
|
+
all_approved = all(d == HunkDecision.APPROVED for d in self.state.decisions.values())
|
|
384
|
+
any_approved = any(d == HunkDecision.APPROVED for d in self.state.decisions.values())
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
"approved": any_approved,
|
|
388
|
+
"all_approved": all_approved,
|
|
389
|
+
"decisions": {i: d.value for i, d in self.state.decisions.items()},
|
|
390
|
+
"content": self.apply_decisions()
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def show_diff_approval(file_path: str, original: str, modified: str) -> Dict[str, any]:
|
|
395
|
+
"""
|
|
396
|
+
Show an interactive diff approval dialog.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
file_path: Path to the file being edited
|
|
400
|
+
original: Original file content
|
|
401
|
+
modified: Modified file content
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
Dict with:
|
|
405
|
+
- approved: bool - whether any changes were approved
|
|
406
|
+
- all_approved: bool - whether all changes were approved
|
|
407
|
+
- content: str - the resulting content after applying decisions
|
|
408
|
+
- decisions: dict - per-hunk decisions
|
|
409
|
+
"""
|
|
410
|
+
viewer = DiffViewer(file_path, original, modified)
|
|
411
|
+
return viewer.run()
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def quick_diff_preview(original: str, modified: str, max_lines: int = 20) -> str:
|
|
415
|
+
"""
|
|
416
|
+
Generate a quick text-based diff preview (non-interactive).
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
original: Original content
|
|
420
|
+
modified: Modified content
|
|
421
|
+
max_lines: Maximum lines to show
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
Colored diff string
|
|
425
|
+
"""
|
|
426
|
+
original_lines = original.splitlines(keepends=True)
|
|
427
|
+
modified_lines = modified.splitlines(keepends=True)
|
|
428
|
+
|
|
429
|
+
diff = list(difflib.unified_diff(
|
|
430
|
+
original_lines,
|
|
431
|
+
modified_lines,
|
|
432
|
+
lineterm=''
|
|
433
|
+
))
|
|
434
|
+
|
|
435
|
+
if not diff:
|
|
436
|
+
return "No changes"
|
|
437
|
+
|
|
438
|
+
result = []
|
|
439
|
+
for line in diff[:max_lines]:
|
|
440
|
+
if line.startswith('+') and not line.startswith('+++'):
|
|
441
|
+
result.append(f"\033[32m{line.rstrip()}\033[0m")
|
|
442
|
+
elif line.startswith('-') and not line.startswith('---'):
|
|
443
|
+
result.append(f"\033[31m{line.rstrip()}\033[0m")
|
|
444
|
+
elif line.startswith('@@'):
|
|
445
|
+
result.append(f"\033[36m{line.rstrip()}\033[0m")
|
|
446
|
+
else:
|
|
447
|
+
result.append(line.rstrip())
|
|
448
|
+
|
|
449
|
+
if len(diff) > max_lines:
|
|
450
|
+
result.append(f"\033[90m... ({len(diff) - max_lines} more lines)\033[0m")
|
|
451
|
+
|
|
452
|
+
return '\n'.join(result)
|