mlx-code 0.0.29__tar.gz → 0.0.30__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.29 → mlx_code-0.0.30}/PKG-INFO +12 -1
- {mlx_code-0.0.29 → mlx_code-0.0.30}/README.md +11 -0
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code/apis.py +2 -2
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code/gits.py +18 -1
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code/main.py +3 -2
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code/repl.py +57 -8
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code/tools.py +13 -0
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code/tui.py +0 -1
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code/view_log.py +1 -1
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code/web.py +1 -1
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code.egg-info/PKG-INFO +12 -1
- {mlx_code-0.0.29 → mlx_code-0.0.30}/setup.py +1 -1
- {mlx_code-0.0.29 → mlx_code-0.0.30}/LICENSE +0 -0
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code/__init__.py +0 -0
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code/bare.py +0 -0
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code/bats.py +0 -0
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code/lsp_tool.py +0 -0
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code/mcb.py +0 -0
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code/mcb_tool.py +0 -0
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code/stream_log.py +0 -0
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code/util.py +0 -0
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code/view_git.py +0 -0
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code.egg-info/SOURCES.txt +0 -0
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code.egg-info/dependency_links.txt +0 -0
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code.egg-info/entry_points.txt +0 -0
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code.egg-info/requires.txt +0 -0
- {mlx_code-0.0.29 → mlx_code-0.0.30}/mlx_code.egg-info/top_level.txt +0 -0
- {mlx_code-0.0.29 → mlx_code-0.0.30}/setup.cfg +0 -0
- {mlx_code-0.0.29 → mlx_code-0.0.30}/tests/__init__.py +0 -0
- {mlx_code-0.0.29 → mlx_code-0.0.30}/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.30
|
|
4
4
|
Summary: Coding Agent for Mac
|
|
5
5
|
Home-page: https://josefalbers.github.io/mlx-code/
|
|
6
6
|
Author: J Joe
|
|
@@ -483,6 +483,7 @@ agent = Agent(extra_tool_classes=[LiveDBTool], tool_names=["QueryDB"])
|
|
|
483
483
|
| `/abort` | Abort the running agent |
|
|
484
484
|
| `/errors` | Show timestamped error log for the current tab |
|
|
485
485
|
| `/export [path]` | Export session to JSON |
|
|
486
|
+
| `/merge` | Merge current branch into parent tab |
|
|
486
487
|
| `/exit [--all]` | Close branch tab, or exit the app |
|
|
487
488
|
| `/help` | Show command reference |
|
|
488
489
|
| `!command` | Run a shell command; output captured in the TUI (eg, `ls`, `cat hello.c`) |
|
|
@@ -548,8 +549,18 @@ mlc --leash claude # claude code
|
|
|
548
549
|
- subdomain: jjoe
|
|
549
550
|
- domain: mlx-code.com
|
|
550
551
|
- service url: http://host.containers.internal:8080
|
|
552
|
+
|
|
553
|
+
[protect & connect]-[zero trust]-[access controls]-[applications]-[create new application]-[self-hosted and private]:
|
|
554
|
+
- subdomain: jjoe
|
|
555
|
+
- domain: mlx-code.com
|
|
556
|
+
- [create new policy]
|
|
557
|
+
- policy name: Kaputt
|
|
558
|
+
- action: Allow
|
|
559
|
+
- [policy rules]-[(or) include]-[selector is]
|
|
560
|
+
|
|
551
561
|
mlc --host 0.0.0.0 --engine batch --web &
|
|
552
562
|
podman run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token $JJ_CFD_TOKEN
|
|
563
|
+
|
|
553
564
|
phone http://jjoe.mlx-code.com
|
|
554
565
|
```
|
|
555
566
|
|
|
@@ -445,6 +445,7 @@ agent = Agent(extra_tool_classes=[LiveDBTool], tool_names=["QueryDB"])
|
|
|
445
445
|
| `/abort` | Abort the running agent |
|
|
446
446
|
| `/errors` | Show timestamped error log for the current tab |
|
|
447
447
|
| `/export [path]` | Export session to JSON |
|
|
448
|
+
| `/merge` | Merge current branch into parent tab |
|
|
448
449
|
| `/exit [--all]` | Close branch tab, or exit the app |
|
|
449
450
|
| `/help` | Show command reference |
|
|
450
451
|
| `!command` | Run a shell command; output captured in the TUI (eg, `ls`, `cat hello.c`) |
|
|
@@ -510,8 +511,18 @@ mlc --leash claude # claude code
|
|
|
510
511
|
- subdomain: jjoe
|
|
511
512
|
- domain: mlx-code.com
|
|
512
513
|
- service url: http://host.containers.internal:8080
|
|
514
|
+
|
|
515
|
+
[protect & connect]-[zero trust]-[access controls]-[applications]-[create new application]-[self-hosted and private]:
|
|
516
|
+
- subdomain: jjoe
|
|
517
|
+
- domain: mlx-code.com
|
|
518
|
+
- [create new policy]
|
|
519
|
+
- policy name: Kaputt
|
|
520
|
+
- action: Allow
|
|
521
|
+
- [policy rules]-[(or) include]-[selector is]
|
|
522
|
+
|
|
513
523
|
mlc --host 0.0.0.0 --engine batch --web &
|
|
514
524
|
podman run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token $JJ_CFD_TOKEN
|
|
525
|
+
|
|
515
526
|
phone http://jjoe.mlx-code.com
|
|
516
527
|
```
|
|
517
528
|
|
|
@@ -75,7 +75,7 @@ class ClaudeChat:
|
|
|
75
75
|
out.append(blk)
|
|
76
76
|
elif t == 'thinking':
|
|
77
77
|
if b['redacted']:
|
|
78
|
-
out.append({'type': 'redacted_thinking', 'data': b
|
|
78
|
+
out.append({'type': 'redacted_thinking', 'data': b.get('thinking', b.get('redacted_data', ''))})
|
|
79
79
|
else:
|
|
80
80
|
out.append({'type': 'thinking', 'thinking': b['thinking'], 'signature': b.get('signature') or ''})
|
|
81
81
|
elif t == 'toolCall':
|
|
@@ -156,7 +156,7 @@ class ClaudeChat:
|
|
|
156
156
|
msg['content'].append({'type': 'thinking', 'thinking': '', 'signature': None, 'redacted': False})
|
|
157
157
|
elif bt == 'redacted_thinking':
|
|
158
158
|
_idx[idx] = len(msg['content'])
|
|
159
|
-
msg['content'].append({'type': 'thinking', '
|
|
159
|
+
msg['content'].append({'type': 'thinking', 'redacted_data': blk.get('data', ''), 'signature': None, 'redacted': True})
|
|
160
160
|
elif bt == 'tool_use':
|
|
161
161
|
_idx[idx] = len(msg['content'])
|
|
162
162
|
msg['content'].append({'type': 'toolCall', 'id': blk['id'], 'name': blk['name'], 'arguments': {}})
|
|
@@ -349,4 +349,21 @@ def get_branch_base_sha(worktree: str) -> str | None:
|
|
|
349
349
|
except GitError:
|
|
350
350
|
pass
|
|
351
351
|
root_sha = _git(worktree, 'rev-list', '--max-parents=0', 'HEAD', check=False)
|
|
352
|
-
return root_sha or None
|
|
352
|
+
return root_sha or None
|
|
353
|
+
|
|
354
|
+
def merge_branch_into_worktree(parent_gwt, child_gwt) -> tuple[bool, str]:
|
|
355
|
+
if parent_gwt is None or child_gwt is None:
|
|
356
|
+
out = f'Not git: parent_gwt={parent_gwt!r} child_gwt={child_gwt!r}'
|
|
357
|
+
logger.debug(out)
|
|
358
|
+
return (False, out)
|
|
359
|
+
try:
|
|
360
|
+
out = _git(parent_gwt.worktree, 'merge', '--no-edit', '--no-ff', child_gwt.branch)
|
|
361
|
+
logger.info(out)
|
|
362
|
+
return (True, out)
|
|
363
|
+
except GitError as exc:
|
|
364
|
+
logger.error('Failed merge')
|
|
365
|
+
try:
|
|
366
|
+
_git(parent_gwt.worktree, 'merge', '--abort', check=False)
|
|
367
|
+
except Exception as _exc:
|
|
368
|
+
logger.error('Failed merge --abort')
|
|
369
|
+
return (False, str(exc))
|
|
@@ -387,10 +387,11 @@ def is_stuck(tokens, pattern_size=100, min_repeats=3):
|
|
|
387
387
|
if len(tokens) < pattern_size * min_repeats:
|
|
388
388
|
return False
|
|
389
389
|
pattern = tuple(tokens[-pattern_size:])
|
|
390
|
+
window = tokens[-(pattern_size * min_repeats + pattern_size):]
|
|
390
391
|
positions = []
|
|
391
|
-
limit = len(
|
|
392
|
+
limit = len(window) - pattern_size
|
|
392
393
|
for i in range(limit):
|
|
393
|
-
if tuple(
|
|
394
|
+
if tuple(window[i:i + pattern_size]) == pattern:
|
|
394
395
|
positions.append(i)
|
|
395
396
|
if len(positions) < min_repeats:
|
|
396
397
|
return False
|
|
@@ -148,16 +148,17 @@ class Agent:
|
|
|
148
148
|
r['content'].insert(0, {'type': 'text', 'text': warn})
|
|
149
149
|
self.messages.extend(results)
|
|
150
150
|
await self._emit({'type': 'tool_results_ready', 'payload': {}})
|
|
151
|
-
new_gwt, diff_stat = commit_worktree(self.ctx['gwt'], self.messages)
|
|
152
|
-
self.ctx['gwt'] = new_gwt
|
|
153
|
-
if diff_stat:
|
|
154
|
-
sha = new_gwt.commit[:8] if new_gwt else ''
|
|
155
|
-
self.messages.append({'role': 'commit', 'content': f'[{sha}]\n{diff_stat}', 'sha': sha})
|
|
156
|
-
await self._emit({'type': 'commit', 'payload': {'diff_stat': diff_stat, 'sha': sha}})
|
|
157
151
|
if self._signal and self._signal.is_set():
|
|
158
152
|
final['stop_reason'] = 'aborted'
|
|
159
153
|
break
|
|
154
|
+
new_gwt, diff_stat = commit_worktree(self.ctx['gwt'], self.messages)
|
|
155
|
+
self.ctx['gwt'] = new_gwt
|
|
156
|
+
if diff_stat:
|
|
157
|
+
sha = new_gwt.commit[:8] if new_gwt else ''
|
|
158
|
+
self.messages.append({'role': 'commit', 'content': f'[{sha}]\n{diff_stat}', 'sha': sha})
|
|
159
|
+
await self._emit({'type': 'commit', 'payload': {'diff_stat': diff_stat, 'sha': sha}})
|
|
160
160
|
await self._emit({'type': 'agent_end', 'payload': {'message': final}})
|
|
161
|
+
logger.debug(json.dumps(self.messages, indent=2, ensure_ascii=False))
|
|
161
162
|
return final
|
|
162
163
|
|
|
163
164
|
async def _execute_tools(self, calls: list[dict]) -> list[dict]:
|
|
@@ -237,7 +238,7 @@ class UIAdapter(Protocol):
|
|
|
237
238
|
|
|
238
239
|
def exit_app(self, summary: list[dict]) -> None:
|
|
239
240
|
...
|
|
240
|
-
HELP_TEXT = '\nCommands:\n/help show this message\n/clear [--config F] clear conversation; --config reconfigures agent from YAML/JSON\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] [--no-worktree] [prompt]\n open a branch tab; optional prompt runs immediately\n/branches list all tabs/branches\n/abort abort the running agent\n/export [path] export session to JSON\n/verbose toggle verbose mode (show raw tool calls and output)\n/exit /quit [--all] close branch tab, or exit the app\n!command run shell command in worktree (output captured)\n$command run interactive shell command (terminal handed to process)\n e.g. !ls !git diff !cat file.py\n $vim file.py $yazi $less log.txt\n\nKeys (TUI only):\nEnter submit\nCtrl-J insert newline in editor\nCtrl-1 … Ctrl-9 jump directly to tab N\nCtrl-, / Ctrl-. cycle through tabs\nCtrl-C abort running agent\nCtrl-D close branch tab (exit app if last tab)\nCtrl-R recall last prompt into editor\n'
|
|
241
|
+
HELP_TEXT = '\nCommands:\n/help show this message\n/clear [--config F] clear conversation; --config reconfigures agent from YAML/JSON\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] [--no-worktree] [prompt]\n open a branch tab; optional prompt runs immediately\n/branches list all tabs/branches\n/abort abort the running agent\n/export [path] export session to JSON\n/verbose toggle verbose mode (show raw tool calls and output)\n/merge merge this branch into its parent tab, then close\n/exit /quit [--all] close branch tab, or exit the app\n!command run shell command in worktree (output captured)\n$command run interactive shell command (terminal handed to process)\n e.g. !ls !git diff !cat file.py\n $vim file.py $yazi $less log.txt\n\nKeys (TUI only):\nEnter submit\nCtrl-J insert newline in editor\nCtrl-1 … Ctrl-9 jump directly to tab N\nCtrl-, / Ctrl-. cycle through tabs\nCtrl-C abort running agent\nCtrl-D close branch tab (exit app if last tab)\nCtrl-R recall last prompt into editor\n'
|
|
241
242
|
|
|
242
243
|
class CommandEngine:
|
|
243
244
|
|
|
@@ -289,6 +290,15 @@ class CommandEngine:
|
|
|
289
290
|
self._unsubscribers[key]()
|
|
290
291
|
del self._unsubscribers[key]
|
|
291
292
|
|
|
293
|
+
def _find_parent_tab(self, tab: TabModel) -> TabModel | None:
|
|
294
|
+
if not tab.index_path:
|
|
295
|
+
return None
|
|
296
|
+
parent_path = tab.index_path[:-1]
|
|
297
|
+
for t in self.tabs:
|
|
298
|
+
if t.index_path == parent_path:
|
|
299
|
+
return t
|
|
300
|
+
return None
|
|
301
|
+
|
|
292
302
|
async def handle_input(self, text: str) -> None:
|
|
293
303
|
text = text.strip()
|
|
294
304
|
if not text:
|
|
@@ -310,7 +320,7 @@ class CommandEngine:
|
|
|
310
320
|
cmd, _, arg = text.partition(' ')
|
|
311
321
|
cmd = cmd.lower().strip()
|
|
312
322
|
arg = arg.strip()
|
|
313
|
-
handlers = {'/help': self._cmd_help, '/clear': self._cmd_clear, '/history': self._cmd_history, '/diff': self._cmd_diff, '/errors': self._cmd_errors, '/tools': self._cmd_tools, '/abort': self._cmd_abort, '/branch': self._cmd_branch, '/branches': self._cmd_branches, '/tab': self._cmd_tab, '/export': self._cmd_export, '/verbose': self._cmd_verbose, '/exit': self._cmd_exit, '/quit': self._cmd_exit}
|
|
323
|
+
handlers = {'/help': self._cmd_help, '/clear': self._cmd_clear, '/history': self._cmd_history, '/diff': self._cmd_diff, '/errors': self._cmd_errors, '/tools': self._cmd_tools, '/abort': self._cmd_abort, '/branch': self._cmd_branch, '/branches': self._cmd_branches, '/tab': self._cmd_tab, '/export': self._cmd_export, '/verbose': self._cmd_verbose, '/merge': self._cmd_merge, '/exit': self._cmd_exit, '/quit': self._cmd_exit}
|
|
314
324
|
handler = handlers.get(cmd)
|
|
315
325
|
if handler:
|
|
316
326
|
await handler(arg)
|
|
@@ -330,6 +340,9 @@ class CommandEngine:
|
|
|
330
340
|
self.ui.show_error(f'Config error: {e}')
|
|
331
341
|
return
|
|
332
342
|
tab = self.active_tab
|
|
343
|
+
if tab.is_running:
|
|
344
|
+
self.ui.show_error('Agent is running — /abort first before /clear --config.')
|
|
345
|
+
return
|
|
333
346
|
old = tab.agent
|
|
334
347
|
new_ctx = {k: v for k, v in old.ctx.items() if k != 'agent'}
|
|
335
348
|
new_agent = Agent(system=cfg.get('system'), api=cfg.get('api'), model=cfg.get('model'), api_key=cfg.get('api_key'), base_url=cfg.get('base_url'), tool_names=cfg.get('tools'), extra_tool_classes=old._extra_tool_classes, ctx=new_ctx)
|
|
@@ -557,6 +570,36 @@ class CommandEngine:
|
|
|
557
570
|
state = 'on' if self.verbose else 'off'
|
|
558
571
|
self.ui.show_command_result('/verbose', f'Verbose mode {state}.')
|
|
559
572
|
|
|
573
|
+
async def _cmd_merge(self, arg: str) -> None:
|
|
574
|
+
tab = self.active_tab
|
|
575
|
+
if tab.is_main:
|
|
576
|
+
self.ui.show_error('Cannot /merge the main tab — it has no parent.')
|
|
577
|
+
return
|
|
578
|
+
if tab.is_running:
|
|
579
|
+
self.ui.show_error('Agent is running — /abort first.')
|
|
580
|
+
return
|
|
581
|
+
parent = self._find_parent_tab(tab)
|
|
582
|
+
if parent is None:
|
|
583
|
+
self.ui.show_error(f'Cannot find parent tab for {tab.title!r}.')
|
|
584
|
+
return
|
|
585
|
+
child_gwt = tab.agent.ctx.get('gwt')
|
|
586
|
+
parent_gwt = parent.agent.ctx.get('gwt')
|
|
587
|
+
if child_gwt is None or parent_gwt is None:
|
|
588
|
+
self.ui.show_error('Both tabs need git worktrees to merge.')
|
|
589
|
+
return
|
|
590
|
+
commit_worktree(child_gwt, tab.agent.messages)
|
|
591
|
+
from .gits import merge_branch_into_worktree
|
|
592
|
+
success, msg = merge_branch_into_worktree(parent_gwt, child_gwt)
|
|
593
|
+
if not success:
|
|
594
|
+
self.ui.show_error(f'Merge failed: {msg}')
|
|
595
|
+
return
|
|
596
|
+
new_parent_gwt, diff_stat = commit_worktree(parent_gwt, parent.agent.messages)
|
|
597
|
+
parent.agent.ctx['gwt'] = new_parent_gwt
|
|
598
|
+
self.ui.show_command_result('/merge', f'Merged {tab.title!r} into {parent.title!r}.\n' + (f'{diff_stat}' if diff_stat else '(no changes)'))
|
|
599
|
+
self._do_close_or_exit()
|
|
600
|
+
parent_idx = self.tabs.index(parent)
|
|
601
|
+
self.switch_tab(parent_idx)
|
|
602
|
+
|
|
560
603
|
async def _cmd_exit(self, arg: str) -> None:
|
|
561
604
|
if arg == '--all':
|
|
562
605
|
summary = self._build_exit_summary()
|
|
@@ -636,6 +679,12 @@ class CommandEngine:
|
|
|
636
679
|
tab = self.active_tab
|
|
637
680
|
removed_index = self.active_index
|
|
638
681
|
self.detach_agent(tab)
|
|
682
|
+
gwt_ref = tab.agent.ctx.get('gwt')
|
|
683
|
+
if gwt_ref and getattr(gwt_ref, 'worktree', None):
|
|
684
|
+
try:
|
|
685
|
+
cleanup_worktree(gwt_ref, remove_branch=True)
|
|
686
|
+
except Exception:
|
|
687
|
+
logger.error('Failed worktree cleanup')
|
|
639
688
|
self.tabs.pop(removed_index)
|
|
640
689
|
if self.active_index >= len(self.tabs):
|
|
641
690
|
self.active_index = len(self.tabs) - 1
|
|
@@ -386,6 +386,7 @@ class AgentTool(Tool):
|
|
|
386
386
|
parameters = AgentParams
|
|
387
387
|
|
|
388
388
|
async def execute(self, params: AgentParams, signal=None) -> dict:
|
|
389
|
+
from gits import create_worktree, commit_worktree, merge_branch_into_worktree
|
|
389
390
|
parent = self.ctx['agent']
|
|
390
391
|
overrides = {}
|
|
391
392
|
if params.api is not None:
|
|
@@ -394,11 +395,23 @@ class AgentTool(Tool):
|
|
|
394
395
|
overrides['system'] = params.system
|
|
395
396
|
overrides['tool_names'] = params.tools
|
|
396
397
|
child = parent.spawn(**overrides)
|
|
398
|
+
parent_gwt = parent.ctx.get('gwt')
|
|
399
|
+
parent_cwd = parent.ctx.get('cwd', os.getcwd())
|
|
400
|
+
repo_dir = parent_gwt.worktree if parent_gwt else parent_cwd
|
|
401
|
+
child_gwt = create_worktree(repo_dir, prefix='subagent')
|
|
402
|
+
if child_gwt is None:
|
|
403
|
+
return tout('Agent failed: could not create isolated worktree. Check .log.json for git errors.', True)
|
|
404
|
+
child.ctx['gwt'] = child_gwt
|
|
405
|
+
child.ctx['cwd'] = child_gwt.worktree
|
|
406
|
+
if 'env' in child.ctx:
|
|
407
|
+
child.ctx['env']['PWD'] = child_gwt.worktree
|
|
397
408
|
if '_stream_log_fp' in parent.ctx:
|
|
398
409
|
from .stream_log import StreamLogger
|
|
399
410
|
StreamLogger.attach_to_child(child, parent.ctx, tool_name=f'sub-agent-{''.join(random.choices(string.ascii_letters + string.digits, k=2))}')
|
|
400
411
|
try:
|
|
401
412
|
result = await child.run(params.task)
|
|
413
|
+
commit_worktree(child_gwt, child.messages)
|
|
414
|
+
merge_branch_into_worktree(parent_gwt, child_gwt)
|
|
402
415
|
texts = [b.get('text', '') for b in result['content'] if b.get('type') == 'text']
|
|
403
416
|
combined = ''.join(texts).strip()
|
|
404
417
|
if not combined:
|
|
@@ -234,7 +234,6 @@ class Tab(Vertical):
|
|
|
234
234
|
self._stream_blocks.append({'type': 'thinking', 'text': delta})
|
|
235
235
|
self.refresh_stream()
|
|
236
236
|
elif et == 'tool_start':
|
|
237
|
-
self._stream_blocks.append({'type': 'toolCall', 'name': payload.get('name', 'tool'), 'arguments': payload.get('args', {}), 'id': 'streaming'})
|
|
238
237
|
self.refresh_stream()
|
|
239
238
|
elif et == 'tool_end':
|
|
240
239
|
if payload.get('is_error'):
|
|
@@ -597,7 +597,7 @@ def tui(stdscr, entries, log_file, initial_filter='', initial_visible=None):
|
|
|
597
597
|
def main():
|
|
598
598
|
parser = argparse.ArgumentParser(description='TUI viewer for JSON log files')
|
|
599
599
|
parser.add_argument('logfile', nargs='?', default='.log.json', help='Path to log file (default: .log.json)')
|
|
600
|
-
parser.add_argument('-f', '--filter', default=f'lvl:10;file:main,bats,repl,bare,gits,apis,tools', help='Initial filter string (same syntax as in UI)')
|
|
600
|
+
parser.add_argument('-f', '--filter', default=f'lvl:10;file:main,bats,repl,bare,tui,gits,apis,tools', help='Initial filter string (same syntax as in UI)')
|
|
601
601
|
parser.add_argument('-o', '--out', dest='out', metavar='FILE', help='Write marked entries to FILE (JSON lines format) instead of stdout')
|
|
602
602
|
args = parser.parse_args()
|
|
603
603
|
log_path = args.logfile
|
|
@@ -19,7 +19,7 @@ import uvicorn
|
|
|
19
19
|
from .repl import Agent, collect_skills, _make_agent_env
|
|
20
20
|
from .gits import create_worktree, commit_worktree, resume_worktree, cleanup_worktree, git_new_branch, git_new_branch_at, git_switch_branch, GitError, get_diff_between_refs, get_branch_base_sha, find_rev_commit
|
|
21
21
|
logger = logging.getLogger(__name__)
|
|
22
|
-
_WEB_HTML = '<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">\n<meta name="color-scheme" content="dark light">\n<title>MLX Code</title>\n<style>\n:root { color-scheme: dark; }\n*{margin:0;padding:0;box-sizing:border-box}\nhtml, body { height: 100%; }\nbody{font-family:system-ui,-apple-system,sans-serif;background:#0d1117;color:#c9d1d9;height:100vh;height:100dvh;display:flex;flex-direction:column;overscroll-behavior:none;-webkit-tap-highlight-color:transparent}\n#hdr{padding:8px 16px;padding-top:max(8px, env(safe-area-inset-top));background:#161b22;border-bottom:1px solid #30363d;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;gap:8px}\n#hdr h1{font-size:14px;font-weight:600;white-space:nowrap}\n.filters{display:flex;gap:8px;align-items:center;font-size:11px;color:#8b949e;background:#0d1117;padding:4px 8px;border:1px solid #30363d;border-radius:6px}\n.filters label{display:flex;align-items:center;gap:4px;cursor:pointer;user-select:none}\n.filters input{cursor:pointer;accent-color:#58a6ff;width:14px;height:14px}\n#tabbar{display:flex;align-items:center;background:#161b22;border-bottom:1px solid #30363d;padding:6px 16px;gap:6px;overflow-x:auto;flex-shrink:0;-webkit-overflow-scrolling:touch}\n#tabbar::-webkit-scrollbar{height:4px}\n#tabbar::-webkit-scrollbar-thumb{background:#30363d;border-radius:2px}\n.tab{padding:6px 10px;border-radius:6px;cursor:pointer;white-space:nowrap;font-size:13px;color:#8b949e;display:flex;align-items:center;gap:6px;border:1px solid transparent;flex-shrink:0;min-height:32px}\n.tab:hover{background:#21262d}\n.tab.active{background:rgba(56,139,253,0.15);color:#58a6ff;border-color:rgba(56,139,253,0.3)}\n.tab-marker{color:#3fb950;font-size:10px}\n.tab.running .tab-marker{color:#d29922;animation:pulse 1.5s infinite}\n@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}\n.tab-num{color:#484f58;font-size:11px}\n.close-btn{color:#484f58;margin-left:2px;font-size:14px;line-height:1;padding:2px 4px;cursor:pointer}\n.close-btn:hover{color:#f85149}\n#newTabBtn{padding:6px 12px;border-radius:6px;cursor:pointer;color:#8b949e;border:1px solid #30363d;background:transparent;font-size:13px;flex-shrink:0;min-height:32px}\n#newTabBtn:hover{background:#21262d;color:#c9d1d9}\n#chat{flex:1;overflow-y:auto;padding:clamp(8px, 3vw, 16px);padding-bottom:8px;-webkit-overflow-scrolling:touch}\n.chat-inner{max-width:920px;margin:0 auto}\n.msg{margin-bottom:14px}\n.msg-role{font-size:12px;color:#8b949e;margin-bottom:3px}\n.msg-body{padding:10px 14px;border-radius:8px;line-height:1.6;white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;font-size:14px}\n.msg-user .msg-body{background:#1c2128;border:1px solid #30363d}\n.msg-assistant .msg-body{background:#161b22;border:1px solid #30363d}\n.msg-thinking .msg-body{color:#6e7681;font-style:italic;background:rgba(136,144,150,0.05);border-left:2px solid #30363d;font-size:13px}\n.msg-tool .msg-body{background:rgba(210,153,34,0.08);border-left:2px solid #d29922;font-family:ui-monospace,SFMono-Regular,monospace;font-size:13px;overflow-x:auto}\n.msg-tool-result .msg-body{background:rgba(35,134,54,0.08);border-left:2px solid #238636;font-family:ui-monospace,SFMono-Regular,monospace;font-size:13px;overflow-x:auto}\n.msg-commit .msg-body{background:rgba(56,139,253,0.08);border-left:2px solid #388bfd;color:#8b949e;font-size:13px;overflow-x:auto}\n.msg-error .msg-body{color:#f85149;background:rgba(248,81,73,0.05)}\n.cursor{display:inline-block;width:7px;height:15px;background:#58a6ff;animation:blink 1s steps(2) infinite;vertical-align:text-bottom;margin-left:2px;border-radius:1px}\n@keyframes blink{50%{opacity:0}}\n#input-area{padding:12px 16px;padding-bottom:max(12px, env(safe-area-inset-bottom));background:#161b22;border-top:1px solid #30363d;flex-shrink:0}\n.input-inner{max-width:920px;margin:0 auto;display:flex;gap:8px}\n#input{flex:1;background:#0d1117;color:#c9d1d9;border:1px solid #30363d;border-radius:8px;padding:10px 14px;font-family:inherit;font-size:16px;resize:none;height:44px;max-height:200px;line-height:1.5}\n#input:focus{outline:none;border-color:#58a6ff}\n#send{background:#238636;color:#fff;border:none;border-radius:8px;padding:0 20px;cursor:pointer;font-size:14px;font-weight:500;white-space:nowrap;min-width:80px;height:44px}\n#send:hover{background:#2ea043}\n#send.abort{background:#da3633}\n#send.abort:hover{background:#f85149}\n.hide-thinking .msg-thinking, .hide-tools .msg-tool, .hide-results .msg-tool-result, .hide-commits .msg-commit { display: none; }\n\n/* Mobile compactness */\n@media (max-width: 600px) {\n .filters label span { display: none; }\n #hdr h1 { font-size: 12px; }\n #hdr { padding: 6px 10px; padding-top:max(6px, env(safe-area-inset-top)); }\n #tabbar { padding: 6px 10px; }\n #input-area { padding: 8px 10px; padding-bottom:max(8px, env(safe-area-inset-bottom)); }\n .msg-body { font-size: 15px; }\n}\n</style>\n</head>\n<body>\n<div id="hdr">\n <h1>⚡ MLX Code</h1>\n <div class="filters">\n <label><input type="checkbox" id="f-thinking" checked><span>Thinking</span></label>\n <label><input type="checkbox" id="f-tools" checked><span>Tools</span></label>\n <label><input type="checkbox" id="f-results" checked><span>Results</span></label>\n <label><input type="checkbox" id="f-commits" checked><span>Commits</span></label>\n </div>\n</div>\n<div id="tabbar">\n <button id="newTabBtn" title="Branch from current tab">+ Branch</button>\n</div>\n<div id="chat">\n <div class="chat-inner" id="chatInner"></div>\n <div class="chat-inner" id="streamInner"></div>\n</div>\n<div id="input-area">\n <div class="input-inner">\n <textarea id="input" placeholder="Send a message... (Enter=send, Shift+Enter=newline, /clear, /abort)" rows="1"></textarea>\n <button id="send">Send</button>\n </div>\n</div>\n<script>\nconst chatEl=document.getElementById(\'chat\');\nconst innerEl=document.getElementById(\'chatInner\');\nconst streamInnerEl=document.getElementById(\'streamInner\');\nconst inputEl=document.getElementById(\'input\');\nconst sendBtn=document.getElementById(\'send\');\nconst tabbar=document.getElementById(\'tabbar\');\nconst newTabBtn=document.getElementById(\'newTabBtn\');\n\nlet activeTab = 0;\nlet tabs = [];\nconst tabState = {};\nlet historyFetchInProgress = false;\nlet activeStreamNodes = []; \n\n[\'thinking\',\'tools\',\'results\',\'commits\'].forEach(t => {\n const el = document.getElementById(\'f-\'+t);\n el.addEventListener(\'change\', () => chatEl.classList.toggle(\'hide-\'+t, !el.checked));\n});\n\nfunction getTabState(tabId) {\n if (!tabState[tabId]) {\n tabState[tabId] = { streamBlocks: [], status: \'idle\', toolCallBuf: \'\' };\n }\n return tabState[tabId];\n}\n\nfunction connect() {\n const evtSource = new EventSource(\'/events\');\n evtSource.onmessage = (e) => { handleEvent(JSON.parse(e.data)); };\n evtSource.onerror = () => { evtSource.close(); setTimeout(connect, 1000); };\n}\n\nlet scrollPending = false;\nfunction scrollBottom(){\n if (scrollPending) return;\n scrollPending = true;\n requestAnimationFrame(() => {\n chatEl.scrollTop = chatEl.scrollHeight;\n scrollPending = false;\n });\n}\n\nfunction addMsg(role,label,parentEl){\n const d=document.createElement(\'div\');d.className=\'msg msg-\'+role;\n const r=document.createElement(\'div\');r.className=\'msg-role\';r.textContent=label;\n const b=document.createElement(\'div\');b.className=\'msg-body\';\n d.appendChild(r);d.appendChild(b);parentEl.appendChild(d);return b;\n}\n\nfunction stripToolXml(text){\n text=text.replace(/<tool_call>[\\s\\S]*?<\\/tool_call>/g,\'\');\n const idx=text.lastIndexOf(\'<tool_call>\');\n if(idx!==-1&&text.indexOf(\'</tool_call>\',idx)===-1)return text.substring(0,idx);\n const tag=\'<tool_call>\';\n for(let i=tag.length-1;i>0;i--){if(text.endsWith(tag.substring(0,i)))return text.substring(0,text.length-i);}\n return text;\n}\n\nfunction cleanDisplay(text){ return text.replace(/^\\n+/, \'\').replace(/\\n+$/, \'\'); }\n\nfunction handleEvent(data){\n const type = data.type;\n const payload = data.payload || {};\n const tabId = data.tab_id;\n\n if (type === \'tab_list\') {\n const prevActive = activeTab;\n tabs = payload.tabs || [];\n activeTab = payload.active_id;\n if (activeTab !== prevActive) {\n const state = getTabState(activeTab);\n renderStream(state);\n updateStatus(state);\n refreshHistory();\n }\n renderTabs();\n return;\n }\n\n if (type === \'history\') {\n if (tabId === activeTab) {\n renderHistory(payload.messages || []);\n }\n return;\n }\n\n const state = getTabState(tabId);\n\n switch (type) {\n case \'agent_start\':\n state.status = \'running\';\n state.streamBlocks = [];\n state.toolCallBuf = \'\';\n break;\n case \'turn_start\':\n state.streamBlocks = [];\n state.toolCallBuf = \'\';\n break;\n case \'text_delta\':\n state.toolCallBuf += payload.delta || \'\';\n var cleaned = state.toolCallBuf.replace(/<tool_call>[\\s\\S]*?<\\/tool_call>/g, \'\');\n var emit;\n var idx = cleaned.indexOf(\'<tool_call>\');\n if (idx !== -1) {\n emit = cleaned.substring(0, idx);\n state.toolCallBuf = cleaned.substring(idx);\n } else {\n emit = cleaned;\n state.toolCallBuf = \'\';\n }\n if (emit) {\n var last = state.streamBlocks[state.streamBlocks.length - 1];\n if (last && last.type === \'text\' && !last.isError) {\n last.text += emit;\n } else {\n state.streamBlocks.push({ type: \'text\', text: emit });\n }\n }\n break;\n case \'thinking_delta\':\n var tDelta = payload.delta || \'\';\n if (tDelta) {\n var last = state.streamBlocks[state.streamBlocks.length - 1];\n if (last && last.type === \'thinking\') {\n last.text += tDelta;\n } else {\n state.streamBlocks.push({ type: \'thinking\', text: tDelta });\n }\n }\n break;\n case \'tool_start\':\n state.streamBlocks.push({\n type: \'toolCall\',\n name: payload.name || \'tool\',\n arguments: payload.args || {}\n });\n break;\n case \'tool_end\':\n var result = payload.result || {};\n var content = result.content || [];\n var outText = \'\';\n if (typeof content === \'string\') {\n outText = content;\n } else if (Array.isArray(content)) {\n outText = content.filter(b => b.type === \'text\').map(b => b.text || \'\').join(\'\\n\').trim();\n }\n if (payload.is_error) {\n if (!outText) outText = (payload.name || \'tool\') + \' failed\';\n state.streamBlocks.push({ type: \'text\', text: outText, isError: true });\n } else if (outText) {\n state.streamBlocks.push({ type: \'toolResult\', text: outText });\n }\n break;\n case \'commit\':\n state.streamBlocks.push({\n type: \'commit\',\n sha: payload.sha || \'\',\n diff: payload.diff_stat || \'\'\n });\n if (tabId === activeTab) refreshHistory();\n break;\n case \'error\':\n var errObj = payload.error || payload;\n var errMsg = (errObj && (errObj.error_message || errObj.message)) || String(errObj);\n state.streamBlocks.push({ type: \'text\', text: errMsg, isError: true });\n break;\n case \'turn_end\':\n state.streamBlocks = [];\n if (tabId === activeTab) refreshHistory();\n break;\n case \'tool_results_ready\':\n if (tabId === activeTab) refreshHistory();\n break;\n case \'agent_end\':\n state.status = \'idle\';\n state.streamBlocks = [];\n if (tabId === activeTab) refreshHistory();\n break;\n case \'command_output\':\n state.streamBlocks.push({\n type: \'command\',\n command: payload.command || \'\',\n output: payload.output || \'\'\n });\n break;\n case \'shell_output\':\n state.streamBlocks.push({\n type: \'shell\',\n command: payload.command || \'\',\n output: payload.output || \'\'\n });\n break;\n }\n\n if (tabId === activeTab) {\n renderStream(state);\n updateStatus(state);\n }\n\n if (type === \'agent_start\' || type === \'agent_end\') {\n renderTabs();\n }\n}\n\nfunction renderTabs() {\n var existing = tabbar.querySelectorAll(\'.tab\');\n existing.forEach(function(el) { el.remove(); });\n\n tabs.forEach(function(t, i) {\n var el = document.createElement(\'div\');\n el.className = \'tab\' + (t.id === activeTab ? \' active\' : \'\') + (t.is_running ? \' running\' : \'\');\n var marker = \'\\u25CF\';\n el.innerHTML = \'<span class="tab-marker">\' + marker + \'</span>\' +\n \'<span class="tab-title">\' + (t.title) + \'</span>\' +\n \'<span class="tab-num">\' + (i + 1) + \'</span>\';\n if (!t.is_main) {\n var closeBtn = document.createElement(\'span\');\n closeBtn.className = \'close-btn\';\n closeBtn.textContent = \'\\u00D7\';\n closeBtn.onclick = function(e) {\n e.stopPropagation();\n closeTab(t.id);\n };\n el.appendChild(closeBtn);\n }\n el.onclick = function() { switchTab(t.id); };\n tabbar.insertBefore(el, newTabBtn);\n });\n}\n\nfunction renderHistory(messages) {\n innerEl.innerHTML = \'\';\n for (const msg of messages) {\n const role = msg.role;\n const content = msg.content;\n const isError = msg.is_error || false;\n \n if (role === \'commit\') {\n addMsg(\'commit\', \'◇ Commit\', innerEl).textContent = cleanDisplay(\'◇ [\' + (msg.sha || \'\') + \'] committed\');\n } else if (typeof content === \'string\') {\n if (role === \'user\') addMsg(\'user\', \'≫ You\', innerEl).textContent = cleanDisplay(content);\n else if (role === \'system\') addMsg(\'assistant\', \'· System\', innerEl).textContent = cleanDisplay(content);\n } else if (Array.isArray(content)) {\n if (role === \'toolResult\') {\n let t = content.map(b => b.text || \'\').filter(Boolean).join(\'\\n\');\n addMsg(isError ? \'error\' : \'tool-result\', isError ? \'✗ Error\' : \'→ Result\', innerEl).textContent = cleanDisplay((isError ? \'✗ \' : \'→ \') + (t || \'(no output)\'));\n } else {\n for (const block of content) {\n if (block.type === \'thinking\') addMsg(\'thinking\', \'◌ Thinking\', innerEl).textContent = cleanDisplay(block.thinking || \'\');\n else if (block.type === \'text\') addMsg(\'assistant\', \'○ Assistant\', innerEl).textContent = cleanDisplay(stripToolXml(block.text || \'\'));\n else if (block.type === \'toolCall\') {\n const b = addMsg(\'tool\', \'⚙ \' + (block.name || \'\'), innerEl);\n b.textContent = cleanDisplay(\'⚙ \' + (block.name || \'\') + \'\\n\' + JSON.stringify(block.arguments || {}, null, 2));\n }\n }\n }\n }\n }\n scrollBottom();\n}\n\nfunction renderStream(state) {\n if (state.streamBlocks.length === 0) {\n streamInnerEl.innerHTML = \'\';\n activeStreamNodes = [];\n return;\n }\n\n // If we have fewer DOM nodes than stream blocks, create the missing ones\n while (activeStreamNodes.length < state.streamBlocks.length) {\n const idx = activeStreamNodes.length;\n const block = state.streamBlocks[idx];\n const bodyEl = createStreamBlock(block);\n activeStreamNodes.push(bodyEl);\n }\n\n // Update the last node\'s text (for streaming tokens)\n const lastIdx = state.streamBlocks.length - 1;\n const lastBlock = state.streamBlocks[lastIdx];\n const lastNode = activeStreamNodes[lastIdx];\n \n if (lastNode) {\n if (lastBlock.type === \'text\' || lastBlock.type === \'thinking\') {\n lastNode.textContent = cleanDisplay(stripToolXml(lastBlock.text || \'\'));\n } else if (lastBlock.type === \'toolResult\') {\n lastNode.textContent = cleanDisplay(\'→ \' + (lastBlock.text || \'\'));\n }\n }\n scrollBottom();\n}\n\nfunction createStreamBlock(block) {\n let label = \'\';\n let role = \'\';\n \n if (block.type === \'text\') {\n role = \'assistant\'; label = \'○ Assistant\';\n } else if (block.type === \'thinking\') {\n role = \'thinking\'; label = \'◌ Thinking\';\n } else if (block.type === \'toolCall\') {\n role = \'tool\'; label = \'⚙ \' + block.name;\n } else if (block.type === \'toolResult\') {\n role = \'tool-result\'; label = \'→ Result\';\n } else if (block.type === \'commit\') {\n role = \'commit\'; label = \'◇ Commit\';\n } else if (block.type === \'command\') {\n role = \'tool\'; label = \'✓ \' + block.command;\n } else if (block.type === \'shell\') {\n role = \'tool\'; label = \'! \' + block.command;\n }\n \n const bodyEl = addMsg(role, label, streamInnerEl);\n \n if (block.type === \'text\' || block.type === \'thinking\') {\n bodyEl.textContent = cleanDisplay(stripToolXml(block.text || \'\'));\n } else if (block.type === \'toolCall\') {\n bodyEl.textContent = cleanDisplay(\'⚙ \' + block.name + \'\\n\' + JSON.stringify(block.arguments, null, 2));\n } else if (block.type === \'toolResult\') {\n bodyEl.textContent = cleanDisplay(\'→ \' + (block.text || \'\'));\n } else if (block.type === \'commit\') {\n bodyEl.textContent = cleanDisplay(\'◇ [\' + (block.sha || \'\') + \'] committed\');\n } else if (block.type === \'command\' || block.type === \'shell\') {\n bodyEl.textContent = cleanDisplay(block.output || \'\');\n }\n return bodyEl;\n}\n\nfunction updateStatus(state) {\n if (state.status === \'running\') {\n sendBtn.textContent = \'Abort\';\n sendBtn.classList.add(\'abort\');\n } else {\n sendBtn.textContent = \'Send\';\n sendBtn.classList.remove(\'abort\');\n }\n}\n\nasync function send(){\n const text=inputEl.value.trim();if(!text)return;\n var state = getTabState(activeTab);\n if (state.status === \'running\') return;\n \n inputEl.value=\'\';inputEl.style.height=\'auto\';\n \n if(!text.startsWith(\'/\') && !text.startsWith(\'!\')) {\n addMsg(\'user\', \'≫ You\', innerEl).textContent = text;\n scrollBottom();\n }\n \n try {\n await fetch(\'/send\', {\n method: \'POST\',\n headers: {\'Content-Type\': \'application/json\'},\n body: JSON.stringify({ text: text, tab_id: activeTab })\n });\n } catch (e) {\n addMsg(\'error\', \'✗ Error\', streamInnerEl).textContent = cleanDisplay(\'✗ \' + e.message);\n }\n}\n\nfunction abortAgent() {\n fetch(\'/abort\', {\n method: \'POST\',\n headers: {\'Content-Type\': \'application/json\'},\n body: JSON.stringify({ tab_id: activeTab })\n });\n}\n\nfunction switchTab(tabId) {\n if (tabId === activeTab) return;\n fetch(\'/switch_tab\', {\n method: \'POST\',\n headers: {\'Content-Type\': \'application/json\'},\n body: JSON.stringify({ tab_id: tabId })\n }).then(r => r.json()).then(data => {\n if (data.ok) {\n activeTab = tabId;\n var state = getTabState(activeTab);\n renderStream(state);\n updateStatus(state);\n renderHistory(data.messages || []);\n renderTabs();\n }\n });\n}\n\nfunction closeTab(tabId) {\n fetch(\'/close_tab\', {\n method: \'POST\',\n headers: {\'Content-Type\': \'application/json\'},\n body: JSON.stringify({ tab_id: tabId })\n });\n}\n\nfunction refreshHistory() {\n if (historyFetchInProgress) return;\n historyFetchInProgress = true;\n fetch(\'/history/\' + activeTab)\n .then(r => r.json())\n .then(data => { renderHistory(data.messages || []); })\n .catch(e => console.error(\'History fetch error:\', e))\n .finally(() => { historyFetchInProgress = false; });\n}\n\ninputEl.addEventListener(\'keydown\',e=>{\n if(e.key===\'Enter\'&&!e.shiftKey) {\n e.preventDefault();\n var state = getTabState(activeTab);\n if (state.status === \'running\') {\n abortAgent();\n } else {\n send();\n }\n }\n});\ninputEl.addEventListener(\'input\',()=>{inputEl.style.height=\'auto\';inputEl.style.height=Math.min(inputEl.scrollHeight,200)+\'px\';});\n\nsendBtn.addEventListener(\'click\', () => {\n var state = getTabState(activeTab);\n if (state.status === \'running\') {\n abortAgent();\n } else {\n send();\n }\n});\n\nnewTabBtn.addEventListener(\'click\', () => {\n fetch(\'/branch\', {\n method: \'POST\',\n headers: {\'Content-Type\': \'application/json\'},\n body: JSON.stringify({ tab_id: activeTab, prompt: \'\' })\n });\n});\n\ndocument.addEventListener(\'keydown\', e => {\n if (e.altKey && e.key >= \'1\' && e.key <= \'9\') {\n e.preventDefault();\n var idx = parseInt(e.key) - 1;\n if (idx < tabs.length) switchTab(tabs[idx].id);\n }\n});\n\ninputEl.focus();\nconnect();\n</script>\n</body>\n</html>'
|
|
22
|
+
_WEB_HTML = '<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">\n<meta name="color-scheme" content="dark light">\n<title>MLX Code</title>\n<style>\n:root { color-scheme: dark; }\n*{margin:0;padding:0;box-sizing:border-box}\nhtml, body { height: 100%; }\nbody{font-family:system-ui,-apple-system,sans-serif;background:#0d1117;color:#c9d1d9;height:100vh;height:100dvh;display:flex;flex-direction:column;overscroll-behavior:none;-webkit-tap-highlight-color:transparent}\n#hdr{padding:8px 16px;padding-top:max(8px, env(safe-area-inset-top));background:#161b22;border-bottom:1px solid #30363d;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;gap:8px}\n#hdr h1{font-size:14px;font-weight:600;white-space:nowrap}\n.filters{display:flex;gap:8px;align-items:center;font-size:11px;color:#8b949e;background:#0d1117;padding:4px 8px;border:1px solid #30363d;border-radius:6px}\n.filters label{display:flex;align-items:center;gap:4px;cursor:pointer;user-select:none}\n.filters input{cursor:pointer;accent-color:#58a6ff;width:14px;height:14px}\n#tabbar{display:flex;align-items:center;background:#161b22;border-bottom:1px solid #30363d;padding:6px 16px;gap:6px;overflow-x:auto;flex-shrink:0;-webkit-overflow-scrolling:touch}\n#tabbar::-webkit-scrollbar{height:4px}\n#tabbar::-webkit-scrollbar-thumb{background:#30363d;border-radius:2px}\n.tab{padding:6px 10px;border-radius:6px;cursor:pointer;white-space:nowrap;font-size:13px;color:#8b949e;display:flex;align-items:center;gap:6px;border:1px solid transparent;flex-shrink:0;min-height:32px}\n.tab:hover{background:#21262d}\n.tab.active{background:rgba(56,139,253,0.15);color:#58a6ff;border-color:rgba(56,139,253,0.3)}\n.tab-marker{color:#3fb950;font-size:10px}\n.tab.running .tab-marker{color:#d29922;animation:pulse 1.5s infinite}\n@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}\n.tab-num{color:#484f58;font-size:11px}\n.close-btn{color:#484f58;margin-left:2px;font-size:14px;line-height:1;padding:2px 4px;cursor:pointer}\n.close-btn:hover{color:#f85149}\n#newTabBtn{padding:6px 12px;border-radius:6px;cursor:pointer;color:#8b949e;border:1px solid #30363d;background:transparent;font-size:13px;flex-shrink:0;min-height:32px}\n#newTabBtn:hover{background:#21262d;color:#c9d1d9}\n#chat{flex:1;overflow-y:auto;padding:clamp(8px, 3vw, 16px);padding-bottom:8px;-webkit-overflow-scrolling:touch}\n.chat-inner{max-width:920px;margin:0 auto}\n.msg{margin-bottom:14px}\n.msg-role{font-size:12px;color:#8b949e;margin-bottom:3px}\n.msg-body{padding:10px 14px;border-radius:8px;line-height:1.6;white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;font-size:14px}\n.msg-user .msg-body{background:#1c2128;border:1px solid #30363d}\n.msg-assistant .msg-body{background:#161b22;border:1px solid #30363d}\n.msg-thinking .msg-body{color:#6e7681;font-style:italic;background:rgba(136,144,150,0.05);border-left:2px solid #30363d;font-size:13px}\n.msg-tool .msg-body{background:rgba(210,153,34,0.08);border-left:2px solid #d29922;font-family:ui-monospace,SFMono-Regular,monospace;font-size:13px;overflow-x:auto}\n.msg-tool-result .msg-body{background:rgba(35,134,54,0.08);border-left:2px solid #238636;font-family:ui-monospace,SFMono-Regular,monospace;font-size:13px;overflow-x:auto}\n.msg-commit .msg-body{background:rgba(56,139,253,0.08);border-left:2px solid #388bfd;color:#8b949e;font-size:13px;overflow-x:auto}\n.msg-error .msg-body{color:#f85149;background:rgba(248,81,73,0.05)}\n.cursor{display:inline-block;width:7px;height:15px;background:#58a6ff;animation:blink 1s steps(2) infinite;vertical-align:text-bottom;margin-left:2px;border-radius:1px}\n@keyframes blink{50%{opacity:0}}\n#input-area{padding:12px 16px;padding-bottom:max(12px, env(safe-area-inset-bottom));background:#161b22;border-top:1px solid #30363d;flex-shrink:0}\n.input-inner{max-width:920px;margin:0 auto;display:flex;gap:8px}\n#input{flex:1;background:#0d1117;color:#c9d1d9;border:1px solid #30363d;border-radius:8px;padding:10px 14px;font-family:inherit;font-size:16px;resize:none;height:46px;max-height:200px;line-height:1.5}\n#input:focus{outline:none;border-color:#58a6ff}\n#send{background:#238636;color:#fff;border:none;border-radius:8px;padding:0 20px;cursor:pointer;font-size:14px;font-weight:500;white-space:nowrap;min-width:80px;height:46px}\n#send:hover{background:#2ea043}\n#send.abort{background:#da3633}\n#send.abort:hover{background:#f85149}\n.hide-thinking .msg-thinking, .hide-tools .msg-tool, .hide-results .msg-tool-result, .hide-commits .msg-commit { display: none; }\n\n/* Mobile compactness */\n@media (max-width: 600px) {\n .filters label .hide-mobile { display: none; }\n #hdr h1 { font-size: 12px; }\n #hdr { padding: 6px 10px; padding-top:max(6px, env(safe-area-inset-top)); }\n #tabbar { padding: 6px 10px; }\n #input-area { padding: 8px 10px; padding-bottom:max(8px, env(safe-area-inset-bottom)); }\n .msg-body { font-size: 15px; }\n}\n</style>\n</head>\n<body>\n<div id="hdr">\n <h1>⚡ MLX Code</h1>\n <div class="filters">\n <label><input type="checkbox" id="f-thinking" checked><span class="hide-mobile">Thinking</span></label>\n <label><input type="checkbox" id="f-tools" checked><span class="hide-mobile">Tools</span></label>\n <label><input type="checkbox" id="f-results" checked><span class="hide-mobile">Results</span></label>\n <label><input type="checkbox" id="f-commits" checked><span class="hide-mobile">Commits</span></label>\n <label><input type="checkbox" id="f-autoscroll" checked><span>Auto-scroll</span></label>\n </div>\n</div>\n<div id="tabbar">\n <button id="newTabBtn" title="Branch from current tab">+ Branch</button>\n</div>\n<div id="chat">\n <div class="chat-inner" id="chatInner"></div>\n <div class="chat-inner" id="streamInner"></div>\n</div>\n<div id="input-area">\n <div class="input-inner">\n <textarea id="input" placeholder="Send a message..." rows="1"></textarea>\n <button id="send">Send</button>\n </div>\n</div>\n<script>\nconst chatEl=document.getElementById(\'chat\');\nconst innerEl=document.getElementById(\'chatInner\');\nconst streamInnerEl=document.getElementById(\'streamInner\');\nconst inputEl=document.getElementById(\'input\');\nconst sendBtn=document.getElementById(\'send\');\nconst tabbar=document.getElementById(\'tabbar\');\nconst newTabBtn=document.getElementById(\'newTabBtn\');\nconst autoScrollChk = document.getElementById(\'f-autoscroll\');\n\nlet activeTab = 0;\nlet tabs = [];\nconst tabState = {};\nlet historyFetchInProgress = false;\nlet activeStreamNodes = [];\nlet pendingForceScroll = false; // FIX: Track if we need to force scroll on the next render\n\n[\'thinking\',\'tools\',\'results\',\'commits\'].forEach(t => {\n const el = document.getElementById(\'f-\'+t);\n el.addEventListener(\'change\', () => chatEl.classList.toggle(\'hide-\'+t, !el.checked));\n});\n\nfunction getTabState(tabId) {\n if (!tabState[tabId]) {\n tabState[tabId] = { streamBlocks: [], status: \'idle\', toolCallBuf: \'\' };\n }\n return tabState[tabId];\n}\n\nfunction connect() {\n const evtSource = new EventSource(\'/events\');\n evtSource.onmessage = (e) => { handleEvent(JSON.parse(e.data)); };\n evtSource.onerror = () => { evtSource.close(); setTimeout(connect, 1000); };\n}\n\nlet scrollPending = false;\nlet userScrolledUp = false;\n\nfunction isNearBottom() {\n return chatEl.scrollHeight - chatEl.scrollTop - chatEl.clientHeight < 150;\n}\n\nchatEl.addEventListener(\'scroll\', () => {\n if (!isNearBottom()) {\n userScrolledUp = true;\n } else {\n userScrolledUp = false;\n }\n});\n\nfunction scrollBottom(force = false){\n if (scrollPending) return;\n if (!force && userScrolledUp) return;\n\n scrollPending = true;\n requestAnimationFrame(() => {\n chatEl.scrollTop = chatEl.scrollHeight;\n scrollPending = false;\n if (force) userScrolledUp = false; // Reset state if we forced it\n });\n}\n\nfunction addMsg(role,label,parentEl){\n const d=document.createElement(\'div\');d.className=\'msg msg-\'+role;\n const r=document.createElement(\'div\');r.className=\'msg-role\';r.textContent=label;\n const b=document.createElement(\'div\');b.className=\'msg-body\';\n d.appendChild(r);d.appendChild(b);parentEl.appendChild(d);return b;\n}\n\nfunction stripToolXml(text){\n text=text.replace(/<tool_call>[\\s\\S]*?<\\/tool_call>/g,\'\');\n const idx=text.lastIndexOf(\'<tool_call>\');\n if(idx!==-1&&text.indexOf(\'</tool_call>\',idx)===-1)return text.substring(0,idx);\n const tag=\'<tool_call>\';\n for(let i=tag.length-1;i>0;i--){if(text.endsWith(tag.substring(0,i)))return text.substring(0,text.length-i);}\n return text;\n}\n\nfunction cleanDisplay(text){ return text.replace(/^\\n+/, \'\').replace(/\\n+$/, \'\'); }\n\nfunction handleEvent(data){\n const type = data.type;\n const payload = data.payload || {};\n const tabId = data.tab_id;\n\n if (type === \'tab_list\') {\n const prevActive = activeTab;\n tabs = payload.tabs || [];\n activeTab = payload.active_id;\n if (activeTab !== prevActive) {\n const state = getTabState(activeTab);\n renderStream(state);\n updateStatus(state);\n refreshHistory(true);\n }\n renderTabs();\n return;\n }\n\n if (type === \'history\') {\n if (tabId === activeTab) {\n renderHistory(payload.messages || [], true);\n }\n return;\n }\n\n const state = getTabState(tabId);\n\n switch (type) {\n case \'agent_start\':\n state.status = \'running\';\n state.streamBlocks = [];\n state.toolCallBuf = \'\';\n break;\n case \'turn_start\':\n state.streamBlocks = [];\n state.toolCallBuf = \'\';\n break;\n case \'text_delta\':\n state.toolCallBuf += payload.delta || \'\';\n var cleaned = state.toolCallBuf.replace(/<tool_call>[\\s\\S]*?<\\/tool_call>/g, \'\');\n var emit;\n var idx = cleaned.indexOf(\'<tool_call>\');\n if (idx !== -1) {\n emit = cleaned.substring(0, idx);\n state.toolCallBuf = cleaned.substring(idx);\n } else {\n emit = cleaned;\n state.toolCallBuf = \'\';\n }\n if (emit.trim()) {\n var last = state.streamBlocks[state.streamBlocks.length - 1];\n if (last && last.type === \'text\' && !last.isError) {\n last.text += emit;\n } else {\n state.streamBlocks.push({ type: \'text\', text: emit });\n }\n }\n break;\n case \'thinking_delta\':\n var tDelta = payload.delta || \'\';\n if (tDelta) {\n var last = state.streamBlocks[state.streamBlocks.length - 1];\n if (last && last.type === \'thinking\') {\n last.text += tDelta;\n } else {\n state.streamBlocks.push({ type: \'thinking\', text: tDelta });\n }\n }\n break;\n case \'tool_start\':\n state.streamBlocks.push({\n type: \'toolCall\',\n name: payload.name || \'tool\',\n arguments: payload.args || {}\n });\n break;\n case \'tool_end\':\n var result = payload.result || {};\n var content = result.content || [];\n var outText = \'\';\n if (typeof content === \'string\') {\n outText = content;\n } else if (Array.isArray(content)) {\n outText = content.filter(b => b.type === \'text\').map(b => b.text || \'\').join(\'\\n\').trim();\n }\n if (payload.is_error) {\n if (!outText) outText = (payload.name || \'tool\') + \' failed\';\n state.streamBlocks.push({ type: \'text\', text: outText, isError: true });\n } else if (outText) {\n state.streamBlocks.push({ type: \'toolResult\', text: outText });\n }\n break;\n case \'commit\':\n state.streamBlocks.push({\n type: \'commit\',\n sha: payload.sha || \'\',\n diff: payload.diff_stat || \'\'\n });\n if (tabId === activeTab) refreshHistory(false);\n break;\n case \'error\':\n var errObj = payload.error || payload;\n var errMsg = (errObj && (errObj.error_message || errObj.message)) || String(errObj);\n state.streamBlocks.push({ type: \'text\', text: errMsg, isError: true });\n break;\n case \'turn_end\':\n state.streamBlocks = [];\n if (tabId === activeTab) refreshHistory(false);\n break;\n case \'tool_results_ready\':\n if (tabId === activeTab) refreshHistory(false);\n break;\n case \'agent_end\':\n state.status = \'idle\';\n state.streamBlocks = [];\n // FIX: Only force scroll if the Auto-scroll checkbox is checked\n if (tabId === activeTab) refreshHistory(autoScrollChk.checked); \n break;\n case \'command_output\':\n state.streamBlocks.push({\n type: \'command\',\n command: payload.command || \'\',\n output: payload.output || \'\'\n });\n break;\n case \'shell_output\':\n state.streamBlocks.push({\n type: \'shell\',\n command: payload.command || \'\',\n output: payload.output || \'\'\n });\n break;\n }\n\n if (tabId === activeTab) {\n renderStream(state);\n updateStatus(state);\n }\n\n if (type === \'agent_start\' || type === \'agent_end\') {\n renderTabs();\n }\n}\n\nfunction renderTabs() {\n var existing = tabbar.querySelectorAll(\'.tab\');\n existing.forEach(function(el) { el.remove(); });\n\n tabs.forEach(function(t, i) {\n var el = document.createElement(\'div\');\n el.className = \'tab\' + (t.id === activeTab ? \' active\' : \'\') + (t.is_running ? \' running\' : \'\');\n var marker = \'\\u25CF\';\n el.innerHTML = \'<span class="tab-marker">\' + marker + \'</span>\' +\n \'<span class="tab-title">\' + (t.title) + \'</span>\' +\n \'<span class="tab-num">\' + (i + 1) + \'</span>\';\n if (!t.is_main) {\n var closeBtn = document.createElement(\'span\');\n closeBtn.className = \'close-btn\';\n closeBtn.textContent = \'\\u00D7\';\n closeBtn.onclick = function(e) {\n e.stopPropagation();\n closeTab(t.id);\n };\n el.appendChild(closeBtn);\n }\n el.onclick = function() { switchTab(t.id); };\n tabbar.insertBefore(el, newTabBtn);\n });\n}\n\nfunction renderHistory(messages, force = false) {\n innerEl.innerHTML = \'\';\n for (const msg of messages) {\n const role = msg.role;\n const content = msg.content;\n const isError = msg.is_error || false;\n\n if (role === \'commit\') {\n addMsg(\'commit\', \'◇ Commit\', innerEl).textContent = cleanDisplay(\'◇ [\' + (msg.sha || \'\') + \'] committed\');\n } else if (typeof content === \'string\') {\n if (role === \'user\') addMsg(\'user\', \'≫ You\', innerEl).textContent = cleanDisplay(content);\n else if (role === \'system\') addMsg(\'assistant\', \'· System\', innerEl).textContent = cleanDisplay(content);\n } else if (Array.isArray(content)) {\n if (role === \'toolResult\') {\n let t = content.map(b => b.text || \'\').filter(Boolean).join(\'\\n\');\n addMsg(isError ? \'error\' : \'tool-result\', isError ? \'✗ Error\' : \'→ Result\', innerEl).textContent = cleanDisplay((isError ? \'✗ \' : \'→ \') + (t || \'(no output)\'));\n } else {\n for (const block of content) {\n if (block.type === \'thinking\') addMsg(\'thinking\', \'◌ Thinking\', innerEl).textContent = cleanDisplay(block.thinking || \'\');\n else if (block.type === \'text\') {\n const txt = cleanDisplay(stripToolXml(block.text || \'\'));\n if (txt.trim()) {\n addMsg(\'assistant\', \'○ Assistant\', innerEl).textContent = txt;\n }\n }\n else if (block.type === \'toolCall\') {\n if (getTabState(activeTab).status === \'running\' && msg === messages[messages.length - 1]) {\n continue;\n }\n const b = addMsg(\'tool\', \'⚙ \' + (block.name || \'\'), innerEl);\n b.textContent = cleanDisplay(\'⚙ \' + (block.name || \'\') + \'\\n\' + JSON.stringify(block.arguments || {}, null, 2));\n }\n }\n }\n }\n }\n scrollBottom(force);\n}\n\nfunction renderStream(state) {\n if (state.streamBlocks.length === 0) {\n streamInnerEl.innerHTML = \'\';\n activeStreamNodes = [];\n return;\n }\n\n while (activeStreamNodes.length < state.streamBlocks.length) {\n const idx = activeStreamNodes.length;\n const block = state.streamBlocks[idx];\n const bodyEl = createStreamBlock(block);\n activeStreamNodes.push(bodyEl);\n }\n\n const lastIdx = state.streamBlocks.length - 1;\n const lastBlock = state.streamBlocks[lastIdx];\n const lastNode = activeStreamNodes[lastIdx];\n\n if (lastNode) {\n if (lastBlock.type === \'text\' || lastBlock.type === \'thinking\') {\n const txt = cleanDisplay(stripToolXml(lastBlock.text || \'\'));\n lastNode.textContent = txt;\n if (lastBlock.type === \'text\') {\n lastNode.parentElement.style.display = txt.trim() ? \'\' : \'none\';\n }\n } else if (lastBlock.type === \'toolResult\') {\n lastNode.textContent = cleanDisplay(\'→ \' + (lastBlock.text || \'\'));\n }\n }\n scrollBottom(false);\n}\n\nfunction createStreamBlock(block) {\n let label = \'\';\n let role = \'\';\n\n if (block.type === \'text\') {\n role = \'assistant\'; label = \'○ Assistant\';\n } else if (block.type === \'thinking\') {\n role = \'thinking\'; label = \'◌ Thinking\';\n } else if (block.type === \'toolCall\') {\n role = \'tool\'; label = \'⚙ \' + block.name;\n } else if (block.type === \'toolResult\') {\n role = \'tool-result\'; label = \'→ Result\';\n } else if (block.type === \'commit\') {\n role = \'commit\'; label = \'◇ Commit\';\n } else if (block.type === \'command\') {\n role = \'tool\'; label = \'✓ \' + block.command;\n } else if (block.type === \'shell\') {\n role = \'tool\'; label = \'! \' + block.command;\n }\n\n const bodyEl = addMsg(role, label, streamInnerEl);\n\n if (block.type === \'text\' || block.type === \'thinking\') {\n const txt = cleanDisplay(stripToolXml(block.text || \'\'));\n bodyEl.textContent = txt;\n if (block.type === \'text\' && !txt.trim()) {\n bodyEl.parentElement.style.display = \'none\';\n }\n } else if (block.type === \'toolCall\') {\n bodyEl.textContent = cleanDisplay(\'⚙ \' + block.name + \'\\n\' + JSON.stringify(block.arguments, null, 2));\n } else if (block.type === \'toolResult\') {\n bodyEl.textContent = cleanDisplay(\'→ \' + (block.text || \'\'));\n } else if (block.type === \'commit\') {\n bodyEl.textContent = cleanDisplay(\'◇ [\' + (block.sha || \'\') + \'] committed\');\n } else if (block.type === \'command\' || block.type === \'shell\') {\n bodyEl.textContent = cleanDisplay(block.output || \'\');\n }\n return bodyEl;\n}\n\nfunction updateStatus(state) {\n if (state.status === \'running\') {\n sendBtn.textContent = \'Abort\';\n sendBtn.classList.add(\'abort\');\n } else {\n sendBtn.textContent = \'Send\';\n sendBtn.classList.remove(\'abort\');\n }\n}\n\n// FIX: Robust auto-resize without jitter\nfunction autoResizeInput() {\n inputEl.style.height = \'auto\'; // collapse for measurement\n let h = inputEl.scrollHeight + 2; // +2px to account for top and bottom borders\n if (h < 46) h = 46; // Enforce the base CSS height\n inputEl.style.height = h + \'px\';\n}\n\nasync function send(){\n const text=inputEl.value.trim();if(!text)return;\n var state = getTabState(activeTab);\n if (state.status === \'running\') return;\n\n inputEl.value=\'\';\n inputEl.style.height=\'46px\'; // Reset exactly to original CSS height\n userScrolledUp = false;\n\n if(!text.startsWith(\'/\') && !text.startsWith(\'!\')) {\n addMsg(\'user\', \'≫ You\', innerEl).textContent = text;\n scrollBottom(true);\n }\n\n try {\n await fetch(\'/send\', {\n method: \'POST\',\n headers: {\'Content-Type\': \'application/json\'},\n body: JSON.stringify({ text: text, tab_id: activeTab })\n });\n } catch (e) {\n addMsg(\'error\', \'✗ Error\', streamInnerEl).textContent = cleanDisplay(\'✗ \' + e.message);\n }\n}\n\nfunction abortAgent() {\n fetch(\'/abort\', {\n method: \'POST\',\n headers: {\'Content-Type\': \'application/json\'},\n body: JSON.stringify({ tab_id: activeTab })\n });\n}\n\nfunction switchTab(tabId) {\n if (tabId === activeTab) return;\n fetch(\'/switch_tab\', {\n method: \'POST\',\n headers: {\'Content-Type\': \'application/json\'},\n body: JSON.stringify({ tab_id: tabId })\n }).then(r => r.json()).then(data => {\n if (data.ok) {\n activeTab = tabId;\n var state = getTabState(activeTab);\n userScrolledUp = false;\n inputEl.value = \'\'; // Clear input on tab switch\n inputEl.style.height = \'46px\'; // Reset height on tab switch\n renderStream(state);\n updateStatus(state);\n renderHistory(data.messages || [], true);\n renderTabs();\n }\n });\n}\n\nfunction closeTab(tabId) {\n fetch(\'/close_tab\', {\n method: \'POST\',\n headers: {\'Content-Type\': \'application/json\'},\n body: JSON.stringify({ tab_id: tabId })\n });\n}\n\n// FIX: Carry over the force flag if a fetch is already in progress\nfunction refreshHistory(force = false) {\n if (force) pendingForceScroll = true;\n if (historyFetchInProgress) return;\n historyFetchInProgress = true;\n fetch(\'/history/\' + activeTab)\n .then(r => r.json())\n .then(data => { \n const shouldForce = pendingForceScroll;\n pendingForceScroll = false;\n renderHistory(data.messages || [], shouldForce); \n })\n .catch(e => console.error(\'History fetch error:\', e))\n .finally(() => { historyFetchInProgress = false; });\n}\n\ninputEl.addEventListener(\'keydown\',e=>{\n if(e.key===\'Enter\'&&!e.shiftKey) {\n e.preventDefault();\n var state = getTabState(activeTab);\n if (state.status === \'running\') {\n abortAgent();\n } else {\n send();\n }\n }\n});\ninputEl.addEventListener(\'input\', autoResizeInput); // Use the robust resize function\n\nsendBtn.addEventListener(\'click\', () => {\n var state = getTabState(activeTab);\n if (state.status === \'running\') {\n abortAgent();\n } else {\n send();\n }\n});\n\nnewTabBtn.addEventListener(\'click\', () => {\n fetch(\'/branch\', {\n method: \'POST\',\n headers: {\'Content-Type\': \'application/json\'},\n body: JSON.stringify({ tab_id: activeTab, prompt: \'\' })\n });\n});\n\ndocument.addEventListener(\'keydown\', e => {\n if (e.altKey && e.key >= \'1\' && e.key <= \'9\') {\n e.preventDefault();\n var idx = parseInt(e.key) - 1;\n if (idx < tabs.length) switchTab(tabs[idx].id);\n }\n});\n\ninputEl.focus();\nconnect();\n</script>\n</body>\n</html>'
|
|
23
23
|
|
|
24
24
|
def _branch_index_title(parent_path: tuple[int, ...], existing_tabs: list['WebTab']) -> tuple[tuple[int, ...], str]:
|
|
25
25
|
depth = len(parent_path) + 1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mlx-code
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.30
|
|
4
4
|
Summary: Coding Agent for Mac
|
|
5
5
|
Home-page: https://josefalbers.github.io/mlx-code/
|
|
6
6
|
Author: J Joe
|
|
@@ -483,6 +483,7 @@ agent = Agent(extra_tool_classes=[LiveDBTool], tool_names=["QueryDB"])
|
|
|
483
483
|
| `/abort` | Abort the running agent |
|
|
484
484
|
| `/errors` | Show timestamped error log for the current tab |
|
|
485
485
|
| `/export [path]` | Export session to JSON |
|
|
486
|
+
| `/merge` | Merge current branch into parent tab |
|
|
486
487
|
| `/exit [--all]` | Close branch tab, or exit the app |
|
|
487
488
|
| `/help` | Show command reference |
|
|
488
489
|
| `!command` | Run a shell command; output captured in the TUI (eg, `ls`, `cat hello.c`) |
|
|
@@ -548,8 +549,18 @@ mlc --leash claude # claude code
|
|
|
548
549
|
- subdomain: jjoe
|
|
549
550
|
- domain: mlx-code.com
|
|
550
551
|
- service url: http://host.containers.internal:8080
|
|
552
|
+
|
|
553
|
+
[protect & connect]-[zero trust]-[access controls]-[applications]-[create new application]-[self-hosted and private]:
|
|
554
|
+
- subdomain: jjoe
|
|
555
|
+
- domain: mlx-code.com
|
|
556
|
+
- [create new policy]
|
|
557
|
+
- policy name: Kaputt
|
|
558
|
+
- action: Allow
|
|
559
|
+
- [policy rules]-[(or) include]-[selector is]
|
|
560
|
+
|
|
551
561
|
mlc --host 0.0.0.0 --engine batch --web &
|
|
552
562
|
podman run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token $JJ_CFD_TOKEN
|
|
563
|
+
|
|
553
564
|
phone http://jjoe.mlx-code.com
|
|
554
565
|
```
|
|
555
566
|
|
|
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
|