mlx-code 0.0.17__tar.gz → 0.0.18__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.
- {mlx_code-0.0.17 → mlx_code-0.0.18}/PKG-INFO +2 -1
- {mlx_code-0.0.17 → mlx_code-0.0.18}/mlx_code/gits.py +42 -9
- {mlx_code-0.0.17 → mlx_code-0.0.18}/mlx_code/repl.py +171 -9
- {mlx_code-0.0.17 → mlx_code-0.0.18}/mlx_code.egg-info/PKG-INFO +2 -1
- {mlx_code-0.0.17 → mlx_code-0.0.18}/mlx_code.egg-info/requires.txt +1 -0
- {mlx_code-0.0.17 → mlx_code-0.0.18}/setup.py +2 -1
- {mlx_code-0.0.17 → mlx_code-0.0.18}/LICENSE +0 -0
- {mlx_code-0.0.17 → mlx_code-0.0.18}/README.md +0 -0
- {mlx_code-0.0.17 → mlx_code-0.0.18}/mlx_code/__init__.py +0 -0
- {mlx_code-0.0.17 → mlx_code-0.0.18}/mlx_code/apis.py +0 -0
- {mlx_code-0.0.17 → mlx_code-0.0.18}/mlx_code/lsp_tool.py +0 -0
- {mlx_code-0.0.17 → mlx_code-0.0.18}/mlx_code/main.py +0 -0
- {mlx_code-0.0.17 → mlx_code-0.0.18}/mlx_code/mcb.py +0 -0
- {mlx_code-0.0.17 → mlx_code-0.0.18}/mlx_code/mcb_tool.py +0 -0
- {mlx_code-0.0.17 → mlx_code-0.0.18}/mlx_code/stream_log.py +0 -0
- {mlx_code-0.0.17 → mlx_code-0.0.18}/mlx_code/tools.py +0 -0
- {mlx_code-0.0.17 → mlx_code-0.0.18}/mlx_code/util.py +0 -0
- {mlx_code-0.0.17 → mlx_code-0.0.18}/mlx_code/view_git.py +0 -0
- {mlx_code-0.0.17 → mlx_code-0.0.18}/mlx_code/view_log.py +0 -0
- {mlx_code-0.0.17 → mlx_code-0.0.18}/mlx_code.egg-info/SOURCES.txt +0 -0
- {mlx_code-0.0.17 → mlx_code-0.0.18}/mlx_code.egg-info/dependency_links.txt +0 -0
- {mlx_code-0.0.17 → mlx_code-0.0.18}/mlx_code.egg-info/entry_points.txt +0 -0
- {mlx_code-0.0.17 → mlx_code-0.0.18}/mlx_code.egg-info/top_level.txt +0 -0
- {mlx_code-0.0.17 → mlx_code-0.0.18}/setup.cfg +0 -0
- {mlx_code-0.0.17 → mlx_code-0.0.18}/tests/__init__.py +0 -0
- {mlx_code-0.0.17 → mlx_code-0.0.18}/tests/test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mlx-code
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.18
|
|
4
4
|
Summary: Coding Agent for Mac
|
|
5
5
|
Home-page: https://josefalbers.github.io/mlx-code/
|
|
6
6
|
Author: J Joe
|
|
@@ -20,6 +20,7 @@ Requires-Dist: rich>=15.0.0
|
|
|
20
20
|
Provides-Extra: all
|
|
21
21
|
Requires-Dist: python-lsp-server[all]; extra == "all"
|
|
22
22
|
Requires-Dist: GitPython; extra == "all"
|
|
23
|
+
Requires-Dist: pygments; extra == "all"
|
|
23
24
|
Dynamic: author
|
|
24
25
|
Dynamic: author-email
|
|
25
26
|
Dynamic: description
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
import json
|
|
3
3
|
import logging
|
|
4
4
|
import os
|
|
5
|
+
import re
|
|
5
6
|
import shutil
|
|
6
7
|
import subprocess
|
|
7
8
|
import uuid
|
|
@@ -25,9 +26,10 @@ def _git(cwd: str, *args: str, check: bool=True) -> str:
|
|
|
25
26
|
def _make_commit_message(messages) -> str:
|
|
26
27
|
if isinstance(messages, str):
|
|
27
28
|
return messages
|
|
28
|
-
|
|
29
|
+
filtered = [m for m in messages if m.get('role') != 'commit']
|
|
30
|
+
last_user = next((m['content'] for m in reversed(filtered) if m.get('role') == 'user'), None)
|
|
29
31
|
title = last_user.replace('\n', ' ').strip()[:30] if isinstance(last_user, str) else ''
|
|
30
|
-
return f'{title}\n\n{json.dumps(
|
|
32
|
+
return f'{title}\n\n{json.dumps(filtered, indent=2, ensure_ascii=False)}'
|
|
31
33
|
|
|
32
34
|
def _parse_messages_from_commit(raw: str) -> list[dict]:
|
|
33
35
|
if not raw or raw in ('snapshot', 'update'):
|
|
@@ -89,17 +91,18 @@ def create_worktree(repo_dir: str, *, worktree_dir: str | None=None, ref: str='H
|
|
|
89
91
|
logger.exception('create_worktree failed: %s', e)
|
|
90
92
|
return None
|
|
91
93
|
|
|
92
|
-
def commit_worktree(point: LedgerPoint | None, message: str='update') -> LedgerPoint | None:
|
|
94
|
+
def commit_worktree(point: LedgerPoint | None, message: str='update') -> tuple[LedgerPoint | None, str]:
|
|
93
95
|
if point is None:
|
|
94
|
-
return None
|
|
96
|
+
return (None, '')
|
|
95
97
|
try:
|
|
96
98
|
git_add_filtered(point.worktree)
|
|
97
99
|
if not _git(point.worktree, 'status', '--porcelain'):
|
|
98
100
|
sha = _git(point.worktree, 'rev-parse', 'HEAD', check=False) or point.commit
|
|
99
|
-
return LedgerPoint(branch=point.branch, commit=sha, worktree=point.worktree)
|
|
100
|
-
|
|
101
|
+
return (LedgerPoint(branch=point.branch, commit=sha, worktree=point.worktree), '')
|
|
102
|
+
diff_stat = ''
|
|
101
103
|
try:
|
|
102
104
|
stat = _git(point.worktree, 'diff', '--stat', 'HEAD')
|
|
105
|
+
diff_stat = stat
|
|
103
106
|
patch = _git(point.worktree, 'diff', 'HEAD')
|
|
104
107
|
diff_info = f'\n--- DIFF STAT ---\n{stat}\n--- PATCH ---\n{patch}'
|
|
105
108
|
except Exception as de:
|
|
@@ -108,10 +111,10 @@ def commit_worktree(point: LedgerPoint | None, message: str='update') -> LedgerP
|
|
|
108
111
|
new_sha = _git(point.worktree, 'rev-parse', 'HEAD')
|
|
109
112
|
new_point = LedgerPoint(branch=point.branch, commit=new_sha, worktree=point.worktree)
|
|
110
113
|
logger.info('%s%s', new_point, diff_info)
|
|
111
|
-
return new_point
|
|
114
|
+
return (new_point, diff_stat)
|
|
112
115
|
except Exception as e:
|
|
113
116
|
logger.exception('commit_worktree failed: %s', e)
|
|
114
|
-
return None
|
|
117
|
+
return (None, '')
|
|
115
118
|
|
|
116
119
|
def cleanup_worktree(point: LedgerPoint, *, remove_branch: bool=False) -> None:
|
|
117
120
|
root = _git(point.worktree, 'rev-parse', '--show-toplevel', check=False) or None
|
|
@@ -279,4 +282,34 @@ def find_rev_commit(worktree: str, n: int) -> str | None:
|
|
|
279
282
|
return chosen
|
|
280
283
|
except Exception as e:
|
|
281
284
|
logger.warning('find_rev_commit failed: %s', e)
|
|
282
|
-
return None
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
def get_diff_between_refs(worktree: str, ref1: str='HEAD~1', ref2: str='HEAD') -> str:
|
|
288
|
+
try:
|
|
289
|
+
return _git(worktree, 'diff', ref1, ref2)
|
|
290
|
+
except GitError:
|
|
291
|
+
try:
|
|
292
|
+
return _git(worktree, 'diff', '4b825dc642cb6eb9a060e54bf899d15d4a0d0e1d', ref2)
|
|
293
|
+
except GitError as e2:
|
|
294
|
+
raise GitError(f'Could not diff {ref1}..{ref2}: {e2}') from e2
|
|
295
|
+
|
|
296
|
+
def resolve_ref_short(worktree: str, ref: str) -> str:
|
|
297
|
+
try:
|
|
298
|
+
return _git(worktree, 'rev-parse', '--short', ref, check=False)
|
|
299
|
+
except GitError:
|
|
300
|
+
return ''
|
|
301
|
+
|
|
302
|
+
def get_branch_base_sha(worktree: str) -> str | None:
|
|
303
|
+
branch = _git(worktree, 'symbolic-ref', '--short', 'HEAD', check=False)
|
|
304
|
+
if branch:
|
|
305
|
+
match = re.search('--([0-9a-f]{12})', branch)
|
|
306
|
+
if match:
|
|
307
|
+
sha_part = match.group(1)
|
|
308
|
+
try:
|
|
309
|
+
full_sha = _git(worktree, 'rev-parse', sha_part, check=False)
|
|
310
|
+
if full_sha:
|
|
311
|
+
return full_sha
|
|
312
|
+
except GitError:
|
|
313
|
+
pass
|
|
314
|
+
root_sha = _git(worktree, 'rev-list', '--max-parents=0', 'HEAD', check=False)
|
|
315
|
+
return root_sha or None
|
|
@@ -13,7 +13,7 @@ import time
|
|
|
13
13
|
import logging
|
|
14
14
|
import urllib.parse
|
|
15
15
|
from typing import Any, Callable, Literal
|
|
16
|
-
from .gits import create_worktree, commit_worktree, resume_worktree, cleanup_worktree, git_new_branch, git_new_branch_at, git_switch_branch, GitError, get_commit_history_with_stats, find_rev_commit
|
|
16
|
+
from .gits import create_worktree, commit_worktree, resume_worktree, cleanup_worktree, git_new_branch, git_new_branch_at, git_switch_branch, GitError, get_commit_history_with_stats, find_rev_commit, get_diff_between_refs, get_branch_base_sha, resolve_ref_short
|
|
17
17
|
from .tools import Tool, validate_tool_call, DEFAULT_TOOLS
|
|
18
18
|
from .apis import resolve_api
|
|
19
19
|
logger = logging.getLogger(__name__)
|
|
@@ -26,6 +26,7 @@ from textual.widgets import ContentSwitcher, Static, TextArea
|
|
|
26
26
|
from rich.text import Text as RichText
|
|
27
27
|
from rich.table import Table
|
|
28
28
|
from rich.markup import escape
|
|
29
|
+
from rich.markdown import Markdown
|
|
29
30
|
|
|
30
31
|
class Agent:
|
|
31
32
|
|
|
@@ -101,7 +102,8 @@ class Agent:
|
|
|
101
102
|
final: dict = {'role': 'assistant', 'content': [], 'stop_reason': 'error', 'error_message': 'no turns ran', 'usage': {'input': 0, 'output': 0, 'cache_read': 0, 'cache_write': 0}}
|
|
102
103
|
while True:
|
|
103
104
|
await self._emit({'type': 'turn_start', 'payload': {}})
|
|
104
|
-
|
|
105
|
+
api_messages = [m for m in self.messages if m.get('role') != 'commit']
|
|
106
|
+
es = await self.api.stream(api_messages, self.system, self.tools)
|
|
105
107
|
async for event in es:
|
|
106
108
|
if event['type'] in ('text_delta', 'thinking_delta', 'error'):
|
|
107
109
|
await self._emit({'type': event['type'], 'payload': event['payload']})
|
|
@@ -133,7 +135,12 @@ class Agent:
|
|
|
133
135
|
r['content'].insert(0, {'type': 'text', 'text': warn})
|
|
134
136
|
self.messages.extend(results)
|
|
135
137
|
await self._emit({'type': 'tool_results_ready', 'payload': {}})
|
|
136
|
-
|
|
138
|
+
new_gwt, diff_stat = commit_worktree(self.ctx['gwt'], self.messages)
|
|
139
|
+
self.ctx['gwt'] = new_gwt
|
|
140
|
+
if diff_stat:
|
|
141
|
+
sha = new_gwt.commit[:8] if new_gwt else ''
|
|
142
|
+
self.messages.append({'role': 'commit', 'content': f'[{sha}]\n{diff_stat}', 'sha': sha})
|
|
143
|
+
await self._emit({'type': 'commit', 'payload': {'diff_stat': diff_stat, 'sha': sha}})
|
|
137
144
|
if self._signal and self._signal.is_set():
|
|
138
145
|
final['stop_reason'] = 'aborted'
|
|
139
146
|
break
|
|
@@ -168,6 +175,81 @@ def _branch_index_title(parent_path: tuple[int, ...], existing_tabs: list) -> tu
|
|
|
168
175
|
def _clean_block_text(text: str) -> str:
|
|
169
176
|
return text.lstrip('\n').rstrip()
|
|
170
177
|
|
|
178
|
+
def _render_commit_diff(text: str) -> RichText:
|
|
179
|
+
rt = RichText()
|
|
180
|
+
lines = text.split('\n')
|
|
181
|
+
for i, line in enumerate(lines):
|
|
182
|
+
if not line.strip():
|
|
183
|
+
continue
|
|
184
|
+
if line.lstrip().startswith('[') and ']' in line:
|
|
185
|
+
rt.append(line, style='bold cyan')
|
|
186
|
+
elif 'files changed' in line or 'file changed' in line:
|
|
187
|
+
rt.append(line, style='dim')
|
|
188
|
+
elif '|' in line:
|
|
189
|
+
pipe_idx = line.index('|')
|
|
190
|
+
rt.append(line[:pipe_idx + 1])
|
|
191
|
+
for ch in line[pipe_idx + 1:]:
|
|
192
|
+
if ch == '+':
|
|
193
|
+
rt.append(ch, style='green')
|
|
194
|
+
elif ch == '-':
|
|
195
|
+
rt.append(ch, style='red')
|
|
196
|
+
else:
|
|
197
|
+
rt.append(ch, style='dim')
|
|
198
|
+
else:
|
|
199
|
+
rt.append(line, style='dim')
|
|
200
|
+
if i < len(lines) - 1:
|
|
201
|
+
rt.append('\n')
|
|
202
|
+
return rt
|
|
203
|
+
|
|
204
|
+
def _render_split_diff(diff_text: str, ref1_label: str='HEAD~1', ref2_label: str='HEAD') -> Table:
|
|
205
|
+
tbl = Table(show_header=True, box=None, expand=True, padding=0, show_lines=False, pad_edge=False)
|
|
206
|
+
tbl.add_column(f' ◇ {ref1_label} ', style='red', ratio=1, width=1)
|
|
207
|
+
tbl.add_column(f' ◇ {ref2_label} ', style='green', ratio=1, width=1)
|
|
208
|
+
|
|
209
|
+
def _flush(left, right):
|
|
210
|
+
maxlen = max(len(left), len(right))
|
|
211
|
+
left += [''] * (maxlen - len(left))
|
|
212
|
+
right += [''] * (maxlen - len(right))
|
|
213
|
+
for l, r in zip(left, right):
|
|
214
|
+
l_style = 'bold red' if l.startswith('-') else 'dim' if l else ''
|
|
215
|
+
r_style = 'bold green' if r.startswith('+') else 'dim' if r else ''
|
|
216
|
+
tbl.add_row(RichText(escape(l), style=l_style), RichText(escape(r), style=r_style))
|
|
217
|
+
left.clear()
|
|
218
|
+
right.clear()
|
|
219
|
+
left_lines = []
|
|
220
|
+
right_lines = []
|
|
221
|
+
in_hunk = False
|
|
222
|
+
for line in diff_text.split('\n'):
|
|
223
|
+
if line.startswith('diff --git'):
|
|
224
|
+
_flush(left_lines, right_lines)
|
|
225
|
+
in_hunk = False
|
|
226
|
+
match = re.search('b/(.*)', line)
|
|
227
|
+
fname = match.group(1) if match else line
|
|
228
|
+
tbl.add_row(RichText(f'--- a/{fname}', style='bold cyan'), RichText(f'+++ b/{fname}', style='bold cyan'))
|
|
229
|
+
elif line.startswith('@@'):
|
|
230
|
+
_flush(left_lines, right_lines)
|
|
231
|
+
in_hunk = True
|
|
232
|
+
tbl.add_row(RichText(escape(line), style='bold yellow'), RichText(escape(line), style='bold yellow'))
|
|
233
|
+
elif not in_hunk:
|
|
234
|
+
if line.startswith('---') or line.startswith('+++'):
|
|
235
|
+
continue
|
|
236
|
+
_flush(left_lines, right_lines)
|
|
237
|
+
if line.strip():
|
|
238
|
+
tbl.add_row(RichText(escape(line), style='dim cyan'), RichText('', style='dim'))
|
|
239
|
+
elif line.startswith('-'):
|
|
240
|
+
left_lines.append(line)
|
|
241
|
+
elif line.startswith('+'):
|
|
242
|
+
right_lines.append(line)
|
|
243
|
+
elif line.startswith(' '):
|
|
244
|
+
_flush(left_lines, right_lines)
|
|
245
|
+
tbl.add_row(RichText(escape(line), style='dim'), RichText(escape(line), style='dim'))
|
|
246
|
+
else:
|
|
247
|
+
_flush(left_lines, right_lines)
|
|
248
|
+
if line.strip():
|
|
249
|
+
tbl.add_row(RichText(escape(line), style='dim'), RichText(escape(line), style='dim'))
|
|
250
|
+
_flush(left_lines, right_lines)
|
|
251
|
+
return tbl
|
|
252
|
+
|
|
171
253
|
def render_history(messages: list[dict]) -> Table:
|
|
172
254
|
tbl = Table(show_header=False, show_lines=False, box=None, pad_edge=False, expand=True, padding=0)
|
|
173
255
|
tbl.add_column(width=2)
|
|
@@ -176,7 +258,9 @@ def render_history(messages: list[dict]) -> Table:
|
|
|
176
258
|
role = msg.get('role')
|
|
177
259
|
content = msg.get('content')
|
|
178
260
|
is_error = msg.get('is_error', False)
|
|
179
|
-
if isinstance(content,
|
|
261
|
+
if isinstance(content, (Table, RichText)):
|
|
262
|
+
blocks = [{'type': 'renderable', 'renderable': content}]
|
|
263
|
+
elif isinstance(content, str):
|
|
180
264
|
blocks = [{'type': 'text', 'text': content}]
|
|
181
265
|
elif isinstance(content, list):
|
|
182
266
|
blocks = content
|
|
@@ -186,8 +270,11 @@ def render_history(messages: list[dict]) -> Table:
|
|
|
186
270
|
b_type = block.get('type', 'text')
|
|
187
271
|
text = block.get('text', '') or block.get('thinking', '')
|
|
188
272
|
prefix, style = ('', '')
|
|
273
|
+
render_as_md = False
|
|
189
274
|
if is_error:
|
|
190
275
|
prefix, style = ('✗', 'bold red')
|
|
276
|
+
elif role == 'commit':
|
|
277
|
+
prefix, style = ('◇', 'cyan')
|
|
191
278
|
elif role == 'user':
|
|
192
279
|
prefix, style = ('≫', 'bold green')
|
|
193
280
|
elif role == 'system':
|
|
@@ -198,6 +285,7 @@ def render_history(messages: list[dict]) -> Table:
|
|
|
198
285
|
prefix, style = ('◌', 'dim italic')
|
|
199
286
|
elif role == 'toolResult':
|
|
200
287
|
prefix, style = ('→', 'dim yellow')
|
|
288
|
+
render_as_md = True
|
|
201
289
|
elif b_type == 'toolCall':
|
|
202
290
|
prefix, style = ('⚙', 'yellow')
|
|
203
291
|
args = block.get('arguments', {})
|
|
@@ -207,12 +295,26 @@ def render_history(messages: list[dict]) -> Table:
|
|
|
207
295
|
elif role == 'assistant':
|
|
208
296
|
prefix, style = ('○', '')
|
|
209
297
|
text = re.sub('\\s*<tool_call>.*?</tool_call>\\s*', '', text, flags=re.DOTALL).strip()
|
|
298
|
+
render_as_md = True
|
|
210
299
|
text = _clean_block_text(text)
|
|
300
|
+
if b_type == 'renderable':
|
|
301
|
+
prefix, style = ('✓', 'blue')
|
|
302
|
+
body = block.get('renderable')
|
|
303
|
+
tbl.add_row(RichText(prefix, style=style), body)
|
|
304
|
+
continue
|
|
211
305
|
if not text:
|
|
212
306
|
continue
|
|
213
|
-
|
|
307
|
+
if role == 'commit':
|
|
308
|
+
body = _render_commit_diff(text)
|
|
309
|
+
tbl.add_row(RichText('◇', style='cyan'), body)
|
|
310
|
+
continue
|
|
311
|
+
if render_as_md:
|
|
312
|
+
body = Markdown(text)
|
|
313
|
+
else:
|
|
314
|
+
body = RichText(escape(text), style=style)
|
|
315
|
+
tbl.add_row(RichText(prefix, style=style), body)
|
|
214
316
|
return tbl
|
|
215
|
-
REPL_HELP = '
|
|
317
|
+
REPL_HELP = '\nCommands:\n/help show this message\n/clear clear conversation (transcript + agent memory)\n/history show full conversation transcript\n/history --raw show raw API message log (debug)\n/diff [--all] show side-by-side diff of changes\n/errors show timestamped error log for this tab\n/tools list active tools\n/branch [--rev N] [--as-worktree] [prompt]\n open a branch tab; optional prompt runs immediately\n/abort abort the running agent\n/export [path] export session to JSON\n/exit /quit close branch tab, or exit the app\n!command run shell command in worktree (output captured in TUI)\n!!command run interactive shell command (TUI suspends, terminal handed to process)\n e.g. !ls !git diff !cat file.py\n !!vim file.py !!yazi !!less log.txt\nKeys:\nEnter submit\nCtrl-J insert newline in editor\nAlt-1 … Alt-9 jump directly to tab N\nTab / Shift-Tab cycle through tabs\nCtrl-C abort running agent\nCtrl-D close branch tab, or exit app\nCtrl-R recall last prompt into editor\n'
|
|
216
318
|
|
|
217
319
|
class InputBox(TextArea):
|
|
218
320
|
BINDINGS = [Binding('ctrl+j', 'insert_newline', 'New line', show=False), Binding('enter', 'submit_text', 'Submit', show=False, priority=True), Binding('ctrl+r', 'recall_last', 'Recall', show=False), Binding('ctrl+d', 'request_close', 'Exit', show=False, priority=True)]
|
|
@@ -317,6 +419,8 @@ class Tab(Vertical):
|
|
|
317
419
|
self.refresh_cache()
|
|
318
420
|
self._stream_blocks = []
|
|
319
421
|
self.refresh_stream()
|
|
422
|
+
elif et == 'commit':
|
|
423
|
+
self.refresh_cache()
|
|
320
424
|
elif et == 'error':
|
|
321
425
|
err = str(payload.get('error', payload))
|
|
322
426
|
self._stream_blocks.append({'type': 'text', 'text': err, 'is_error': True})
|
|
@@ -424,7 +528,7 @@ class StatusBar(Static):
|
|
|
424
528
|
class HelpBar(Static):
|
|
425
529
|
|
|
426
530
|
def __init__(self, **kwargs):
|
|
427
|
-
self._idle_text = RichText('/help Ctrl-J newline Alt-1…9 tabs Ctrl-C abort Ctrl-D exit
|
|
531
|
+
self._idle_text = RichText('/help !cmd !!interactive Ctrl-J newline Alt-1…9 tabs Ctrl-C abort Ctrl-D exit', style='dim')
|
|
428
532
|
super().__init__(self._idle_text, **kwargs)
|
|
429
533
|
|
|
430
534
|
def show_idle(self) -> None:
|
|
@@ -492,7 +596,7 @@ class ReplApp(App[None]):
|
|
|
492
596
|
|
|
493
597
|
async def on_event(event: dict) -> None:
|
|
494
598
|
tab.apply_event(event)
|
|
495
|
-
if event['type'] in ('agent_start', 'agent_end', 'turn_end'):
|
|
599
|
+
if event['type'] in ('agent_start', 'agent_end', 'turn_end', 'commit'):
|
|
496
600
|
self._refresh_chrome()
|
|
497
601
|
self._unsubscribers[key] = tab.agent.subscribe(on_event)
|
|
498
602
|
|
|
@@ -517,6 +621,12 @@ class ReplApp(App[None]):
|
|
|
517
621
|
tab.errors.clear()
|
|
518
622
|
tab.last_error = ''
|
|
519
623
|
self.query_one('#helpbar', HelpBar).show_idle()
|
|
624
|
+
if text.startswith('!'):
|
|
625
|
+
command = text[1:].strip()
|
|
626
|
+
if not command:
|
|
627
|
+
return
|
|
628
|
+
await self._run_shell_command(tab, command)
|
|
629
|
+
return
|
|
520
630
|
if text.startswith('/'):
|
|
521
631
|
await self._handle_command(tab, text)
|
|
522
632
|
return
|
|
@@ -542,6 +652,25 @@ class ReplApp(App[None]):
|
|
|
542
652
|
tab.status = 'idle'
|
|
543
653
|
self._refresh_chrome()
|
|
544
654
|
|
|
655
|
+
async def _run_shell_command(self, tab: Tab, command: str) -> None:
|
|
656
|
+
if not command:
|
|
657
|
+
return
|
|
658
|
+
if command.startswith('cd ') or command == 'cd':
|
|
659
|
+
tab.show_command(f'!{command}', 'Not allowed — use /branch or set cwd in context')
|
|
660
|
+
return
|
|
661
|
+
gwt = tab.agent.ctx.get('gwt')
|
|
662
|
+
cwd = gwt.worktree if gwt and getattr(gwt, 'worktree', None) else tab.agent.ctx.get('cwd') or os.getcwd()
|
|
663
|
+
env = tab.agent.ctx.get('env')
|
|
664
|
+
try:
|
|
665
|
+
proc = await asyncio.create_subprocess_shell(command, cwd=cwd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, env=env if env else None)
|
|
666
|
+
stdout, _ = await proc.communicate()
|
|
667
|
+
output = stdout.decode(errors='replace').rstrip('\n')
|
|
668
|
+
if proc.returncode and proc.returncode != 0:
|
|
669
|
+
output += f'\n[exit {proc.returncode}]'
|
|
670
|
+
tab.show_command(f'!{command}', output or '(no output)')
|
|
671
|
+
except Exception as e:
|
|
672
|
+
tab.show_command(f'!{command}', f'Error: {e}')
|
|
673
|
+
|
|
545
674
|
async def _handle_command(self, tab: Tab, text: str) -> None:
|
|
546
675
|
cmd, _, arg = text.partition(' ')
|
|
547
676
|
cmd = cmd.lower().strip()
|
|
@@ -605,9 +734,42 @@ class ReplApp(App[None]):
|
|
|
605
734
|
line += f'\n {file_lines}'
|
|
606
735
|
lines.append(line)
|
|
607
736
|
tab.show_command(text, '\n'.join(lines))
|
|
737
|
+
elif cmd == '/diff':
|
|
738
|
+
gwt = tab.agent.ctx.get('gwt')
|
|
739
|
+
if not gwt or not getattr(gwt, 'worktree', None):
|
|
740
|
+
self.query_one('#helpbar', HelpBar).show_error('No git worktree available for this tab.')
|
|
741
|
+
return
|
|
742
|
+
ref1 = 'HEAD~1'
|
|
743
|
+
ref2 = 'HEAD'
|
|
744
|
+
is_all = '--all' in arg
|
|
745
|
+
if is_all:
|
|
746
|
+
base = get_branch_base_sha(gwt.worktree)
|
|
747
|
+
if base:
|
|
748
|
+
ref1 = base
|
|
749
|
+
else:
|
|
750
|
+
self.query_one('#helpbar', HelpBar).show_error('Could not determine base commit for --all.')
|
|
751
|
+
return
|
|
752
|
+
ref1_short = resolve_ref_short(gwt.worktree, ref1)
|
|
753
|
+
ref2_short = resolve_ref_short(gwt.worktree, ref2)
|
|
754
|
+
if is_all:
|
|
755
|
+
ref1_label = f'base ({ref1_short})' if ref1_short else 'base'
|
|
756
|
+
else:
|
|
757
|
+
ref1_label = f'HEAD~1 ({ref1_short})' if ref1_short else 'HEAD~1'
|
|
758
|
+
ref2_label = f'HEAD ({ref2_short})' if ref2_short else 'HEAD'
|
|
759
|
+
try:
|
|
760
|
+
diff_text = get_diff_between_refs(gwt.worktree, ref1, ref2)
|
|
761
|
+
except GitError as e:
|
|
762
|
+
self.query_one('#helpbar', HelpBar).show_error(f'Git diff failed: {e}')
|
|
763
|
+
return
|
|
764
|
+
if not diff_text.strip():
|
|
765
|
+
tab.show_command(text, f'No differences between {ref1_label} and {ref2_label}.')
|
|
766
|
+
return
|
|
767
|
+
renderable = _render_split_diff(diff_text, ref1_label, ref2_label)
|
|
768
|
+
tab._command_blocks.extend([{'role': 'user', 'content': text}, {'role': 'command', 'content': renderable}])
|
|
769
|
+
tab.refresh_stream()
|
|
608
770
|
elif cmd == '/errors':
|
|
609
771
|
if not tab.errors:
|
|
610
|
-
tab.show_command('No errors recorded.')
|
|
772
|
+
tab.show_command(text, 'No errors recorded.')
|
|
611
773
|
else:
|
|
612
774
|
lines = [f'{ts} {msg}' for ts, msg in tab.errors[-30:]]
|
|
613
775
|
tab.show_command(text, 'Error log\n' + '\n'.join(lines))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mlx-code
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.18
|
|
4
4
|
Summary: Coding Agent for Mac
|
|
5
5
|
Home-page: https://josefalbers.github.io/mlx-code/
|
|
6
6
|
Author: J Joe
|
|
@@ -20,6 +20,7 @@ Requires-Dist: rich>=15.0.0
|
|
|
20
20
|
Provides-Extra: all
|
|
21
21
|
Requires-Dist: python-lsp-server[all]; extra == "all"
|
|
22
22
|
Requires-Dist: GitPython; extra == "all"
|
|
23
|
+
Requires-Dist: pygments; extra == "all"
|
|
23
24
|
Dynamic: author
|
|
24
25
|
Dynamic: author-email
|
|
25
26
|
Dynamic: description
|
|
@@ -11,7 +11,7 @@ setup(
|
|
|
11
11
|
author_email="albersj66@gmail.com",
|
|
12
12
|
author="J Joe",
|
|
13
13
|
license="Apache-2.0",
|
|
14
|
-
version="0.0.
|
|
14
|
+
version="0.0.18",
|
|
15
15
|
readme="README.md",
|
|
16
16
|
description="Coding Agent for Mac",
|
|
17
17
|
long_description=open("README.md").read(),
|
|
@@ -28,6 +28,7 @@ setup(
|
|
|
28
28
|
extras_require={"all": [
|
|
29
29
|
"python-lsp-server[all]",
|
|
30
30
|
"GitPython",
|
|
31
|
+
"pygments",
|
|
31
32
|
]},
|
|
32
33
|
packages=find_packages(),
|
|
33
34
|
entry_points={
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|