mlx-code 0.0.27__tar.gz → 0.0.29__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.27 → mlx_code-0.0.29}/PKG-INFO +26 -13
- {mlx_code-0.0.27 → mlx_code-0.0.29}/README.md +25 -12
- mlx_code-0.0.29/mlx_code/bare.py +276 -0
- {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/main.py +9 -2
- mlx_code-0.0.29/mlx_code/repl.py +808 -0
- mlx_code-0.0.29/mlx_code/tui.py +576 -0
- mlx_code-0.0.29/mlx_code/web.py +485 -0
- {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code.egg-info/PKG-INFO +26 -13
- {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code.egg-info/SOURCES.txt +2 -0
- {mlx_code-0.0.27 → mlx_code-0.0.29}/setup.py +1 -2
- mlx_code-0.0.27/mlx_code/bare.py +0 -484
- mlx_code-0.0.27/mlx_code/repl.py +0 -1149
- {mlx_code-0.0.27 → mlx_code-0.0.29}/LICENSE +0 -0
- {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/__init__.py +0 -0
- {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/apis.py +0 -0
- {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/bats.py +0 -0
- {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/gits.py +0 -0
- {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/lsp_tool.py +0 -0
- {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/mcb.py +0 -0
- {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/mcb_tool.py +0 -0
- {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/stream_log.py +0 -0
- {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/tools.py +0 -0
- {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/util.py +0 -0
- {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/view_git.py +0 -0
- {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/view_log.py +0 -0
- {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code.egg-info/dependency_links.txt +0 -0
- {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code.egg-info/entry_points.txt +0 -0
- {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code.egg-info/requires.txt +0 -0
- {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code.egg-info/top_level.txt +0 -0
- {mlx_code-0.0.27 → mlx_code-0.0.29}/setup.cfg +0 -0
- {mlx_code-0.0.27 → mlx_code-0.0.29}/tests/__init__.py +0 -0
- {mlx_code-0.0.27 → mlx_code-0.0.29}/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.29
|
|
4
4
|
Summary: Coding Agent for Mac
|
|
5
5
|
Home-page: https://josefalbers.github.io/mlx-code/
|
|
6
6
|
Author: J Joe
|
|
@@ -40,7 +40,7 @@ Dynamic: summary
|
|
|
40
40
|
|
|
41
41
|
A Git-native coding agent that can run entirely on your Mac. No API keys, no cloud, and no data leaving your machine. Powered by Apple MLX, it turns commits, branches, and worktrees into the agent’s state, history, and execution model
|
|
42
42
|
|
|
43
|
-
[](https://youtube.com/shorts/1LuifKFKixc)
|
|
44
44
|
|
|
45
45
|
---
|
|
46
46
|
|
|
@@ -49,7 +49,7 @@ A Git-native coding agent that can run entirely on your Mac. No API keys, no clo
|
|
|
49
49
|
```
|
|
50
50
|
Worktrees:
|
|
51
51
|
|
|
52
|
-
main ──●──●──●──●──●──●──●──●──●──●──●──●──●──●───────────► Node = git commit + chat
|
|
52
|
+
main ──●──●──●──●──●──●──●──●──●──●──●──●──●──●───────────► Node = git commit + chat hx
|
|
53
53
|
│ │
|
|
54
54
|
│ └── branch-1 ──●──●──●
|
|
55
55
|
│ │ ┌────────────┐
|
|
@@ -57,16 +57,14 @@ Worktrees:
|
|
|
57
57
|
│ └─────┬──────┘
|
|
58
58
|
└── branch-0 ──●──●──● │
|
|
59
59
|
│
|
|
60
|
-
│
|
|
61
60
|
Tabs: ├────────────► Tab = git branch + Agent
|
|
62
61
|
│
|
|
63
|
-
|
|
64
|
-
┌──────────────────────────────────────────────┼─────────┐
|
|
62
|
+
┌──────────────────────────────────────────────│─────────┐
|
|
65
63
|
│ TUI tabs │ │
|
|
66
64
|
│ ┌──────┐ ┌──────────┐ ┌──────────┐ ┌─────┴──────┐ │
|
|
67
65
|
│ │ main │ │ branch-0 │ │ branch-1 │ │ branch-1-0 │ │
|
|
68
66
|
│ └──────┘ └────┬─────┘ └──────────┘ └────────────┘ │
|
|
69
|
-
|
|
67
|
+
└─────────────────│──────────────────────────────────────┘
|
|
70
68
|
│
|
|
71
69
|
Agents: ├─────────────────────────────────────────► Each tab runs its own Agent
|
|
72
70
|
│
|
|
@@ -522,10 +520,10 @@ All file tools enforce path sandboxing. The agent cannot read or write outside t
|
|
|
522
520
|
|
|
523
521
|
| Backend | Flag | Notes |
|
|
524
522
|
|---------|------|-------|
|
|
525
|
-
| MLX (local) | `--api noapi` | Default. Runs on-device, no API key needed |
|
|
523
|
+
| MLX-LM (local) | `--api noapi` | Default. Runs on-device, no API key needed |
|
|
526
524
|
| Claude | `--api claude` | Requires `ANTHROPIC_API_KEY` |
|
|
527
525
|
| Gemini | `--api gemini` | Requires `GOOGLE_API_KEY` |
|
|
528
|
-
| DeepSeek | `--api deepseek` |
|
|
526
|
+
| DeepSeek | `--api deepseek` | Requires `DEEPSEEK_API_KEY` |
|
|
529
527
|
| Codex | `--api codex` | OpenAI Codex CLI integration |
|
|
530
528
|
| OpenAI | `--api openai` | Any OpenAI-compatible endpoint |
|
|
531
529
|
|
|
@@ -534,10 +532,25 @@ All file tools enforce path sandboxing. The agent cannot read or write outside t
|
|
|
534
532
|
The local MLX server speaks OpenAI, Anthropic, and Gemini wire formats simultaneously, so you can use any compatible CLI as the frontend:
|
|
535
533
|
|
|
536
534
|
```bash
|
|
537
|
-
mlc
|
|
538
|
-
mlc --
|
|
539
|
-
mlc --
|
|
540
|
-
mlc --leash none #
|
|
535
|
+
mlc # default
|
|
536
|
+
mlc --web # web UI (api.mlx-code.com)
|
|
537
|
+
mlc --bare # no TUI
|
|
538
|
+
mlc --leash none # no harness
|
|
539
|
+
mlc --leash codex # codex CLI
|
|
540
|
+
mlc --leash gemini # gemini CLI
|
|
541
|
+
mlc --leash claude # claude code
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
#### WebUI
|
|
545
|
+
|
|
546
|
+
```bash
|
|
547
|
+
[protect & connect]-[networking]-[tunnels]-[add route]-[add published application]:
|
|
548
|
+
- subdomain: jjoe
|
|
549
|
+
- domain: mlx-code.com
|
|
550
|
+
- service url: http://host.containers.internal:8080
|
|
551
|
+
mlc --host 0.0.0.0 --engine batch --web &
|
|
552
|
+
podman run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token $JJ_CFD_TOKEN
|
|
553
|
+
phone http://jjoe.mlx-code.com
|
|
541
554
|
```
|
|
542
555
|
|
|
543
556
|
---
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A Git-native coding agent that can run entirely on your Mac. No API keys, no cloud, and no data leaving your machine. Powered by Apple MLX, it turns commits, branches, and worktrees into the agent’s state, history, and execution model
|
|
4
4
|
|
|
5
|
-
[](https://youtube.com/shorts/1LuifKFKixc)
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -11,7 +11,7 @@ A Git-native coding agent that can run entirely on your Mac. No API keys, no clo
|
|
|
11
11
|
```
|
|
12
12
|
Worktrees:
|
|
13
13
|
|
|
14
|
-
main ──●──●──●──●──●──●──●──●──●──●──●──●──●──●───────────► Node = git commit + chat
|
|
14
|
+
main ──●──●──●──●──●──●──●──●──●──●──●──●──●──●───────────► Node = git commit + chat hx
|
|
15
15
|
│ │
|
|
16
16
|
│ └── branch-1 ──●──●──●
|
|
17
17
|
│ │ ┌────────────┐
|
|
@@ -19,16 +19,14 @@ Worktrees:
|
|
|
19
19
|
│ └─────┬──────┘
|
|
20
20
|
└── branch-0 ──●──●──● │
|
|
21
21
|
│
|
|
22
|
-
│
|
|
23
22
|
Tabs: ├────────────► Tab = git branch + Agent
|
|
24
23
|
│
|
|
25
|
-
|
|
26
|
-
┌──────────────────────────────────────────────┼─────────┐
|
|
24
|
+
┌──────────────────────────────────────────────│─────────┐
|
|
27
25
|
│ TUI tabs │ │
|
|
28
26
|
│ ┌──────┐ ┌──────────┐ ┌──────────┐ ┌─────┴──────┐ │
|
|
29
27
|
│ │ main │ │ branch-0 │ │ branch-1 │ │ branch-1-0 │ │
|
|
30
28
|
│ └──────┘ └────┬─────┘ └──────────┘ └────────────┘ │
|
|
31
|
-
|
|
29
|
+
└─────────────────│──────────────────────────────────────┘
|
|
32
30
|
│
|
|
33
31
|
Agents: ├─────────────────────────────────────────► Each tab runs its own Agent
|
|
34
32
|
│
|
|
@@ -484,10 +482,10 @@ All file tools enforce path sandboxing. The agent cannot read or write outside t
|
|
|
484
482
|
|
|
485
483
|
| Backend | Flag | Notes |
|
|
486
484
|
|---------|------|-------|
|
|
487
|
-
| MLX (local) | `--api noapi` | Default. Runs on-device, no API key needed |
|
|
485
|
+
| MLX-LM (local) | `--api noapi` | Default. Runs on-device, no API key needed |
|
|
488
486
|
| Claude | `--api claude` | Requires `ANTHROPIC_API_KEY` |
|
|
489
487
|
| Gemini | `--api gemini` | Requires `GOOGLE_API_KEY` |
|
|
490
|
-
| DeepSeek | `--api deepseek` |
|
|
488
|
+
| DeepSeek | `--api deepseek` | Requires `DEEPSEEK_API_KEY` |
|
|
491
489
|
| Codex | `--api codex` | OpenAI Codex CLI integration |
|
|
492
490
|
| OpenAI | `--api openai` | Any OpenAI-compatible endpoint |
|
|
493
491
|
|
|
@@ -496,10 +494,25 @@ All file tools enforce path sandboxing. The agent cannot read or write outside t
|
|
|
496
494
|
The local MLX server speaks OpenAI, Anthropic, and Gemini wire formats simultaneously, so you can use any compatible CLI as the frontend:
|
|
497
495
|
|
|
498
496
|
```bash
|
|
499
|
-
mlc
|
|
500
|
-
mlc --
|
|
501
|
-
mlc --
|
|
502
|
-
mlc --leash none #
|
|
497
|
+
mlc # default
|
|
498
|
+
mlc --web # web UI (api.mlx-code.com)
|
|
499
|
+
mlc --bare # no TUI
|
|
500
|
+
mlc --leash none # no harness
|
|
501
|
+
mlc --leash codex # codex CLI
|
|
502
|
+
mlc --leash gemini # gemini CLI
|
|
503
|
+
mlc --leash claude # claude code
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
#### WebUI
|
|
507
|
+
|
|
508
|
+
```bash
|
|
509
|
+
[protect & connect]-[networking]-[tunnels]-[add route]-[add published application]:
|
|
510
|
+
- subdomain: jjoe
|
|
511
|
+
- domain: mlx-code.com
|
|
512
|
+
- service url: http://host.containers.internal:8080
|
|
513
|
+
mlc --host 0.0.0.0 --engine batch --web &
|
|
514
|
+
podman run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token $JJ_CFD_TOKEN
|
|
515
|
+
phone http://jjoe.mlx-code.com
|
|
503
516
|
```
|
|
504
517
|
|
|
505
518
|
---
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import asyncio
|
|
3
|
+
import datetime
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Callable
|
|
10
|
+
from .repl import Agent, TabModel, CommandEngine, UIAdapter, HELP_TEXT
|
|
11
|
+
from .gits import GitError, get_branch_base_sha, get_diff_between_refs, get_commit_history_with_stats, find_rev_commit, create_worktree, git_new_branch, git_new_branch_at
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
class BareAdapter:
|
|
15
|
+
|
|
16
|
+
def __init__(self, repl: 'BareRepl'):
|
|
17
|
+
self.repl = repl
|
|
18
|
+
|
|
19
|
+
def show_error(self, text: str) -> None:
|
|
20
|
+
print(f'\n✗ {text}', flush=True)
|
|
21
|
+
|
|
22
|
+
def show_command_result(self, cmd: str, content: str | object) -> None:
|
|
23
|
+
if isinstance(content, str):
|
|
24
|
+
print(content)
|
|
25
|
+
else:
|
|
26
|
+
print(str(content))
|
|
27
|
+
|
|
28
|
+
def show_diff(self, diff_text: str, ref1_label: str, ref2_label: str) -> None:
|
|
29
|
+
print(diff_text)
|
|
30
|
+
|
|
31
|
+
def show_history_list(self, lines: list[str]) -> None:
|
|
32
|
+
print('\n'.join(lines))
|
|
33
|
+
|
|
34
|
+
def show_history_raw(self, json_text: str) -> None:
|
|
35
|
+
print(json_text)
|
|
36
|
+
|
|
37
|
+
async def add_tab(self, tab: TabModel) -> None:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
def remove_tab(self, removed_index: int) -> None:
|
|
41
|
+
if self.repl.engine.active_index >= len(self.repl.engine.tabs):
|
|
42
|
+
self.repl.engine.active_index = len(self.repl.engine.tabs) - 1
|
|
43
|
+
|
|
44
|
+
def switch_to_tab(self, index: int) -> None:
|
|
45
|
+
self.repl._render_tab_delimiter()
|
|
46
|
+
self.repl._print_history_for_tab(self.repl.engine.tabs[index])
|
|
47
|
+
|
|
48
|
+
def refresh_chrome(self) -> None:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
def clear_tab_display(self, tab: TabModel) -> None:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
def on_agent_event(self, event: dict, tab: TabModel) -> None:
|
|
55
|
+
self.repl._handle_event(event, tab)
|
|
56
|
+
|
|
57
|
+
async def run_captured_shell(self, command: str, cwd: str, env: dict | None) -> str:
|
|
58
|
+
proc = await asyncio.create_subprocess_shell(command, cwd=cwd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, env=env)
|
|
59
|
+
stdout, stderr = await proc.communicate()
|
|
60
|
+
out = stdout.decode(errors='replace').rstrip('\n')
|
|
61
|
+
err = stderr.decode(errors='replace').rstrip('\n')
|
|
62
|
+
body = out
|
|
63
|
+
if err:
|
|
64
|
+
body = (body + '\n' if body else '') + f'[stderr]\n{err}'
|
|
65
|
+
if proc.returncode:
|
|
66
|
+
body += f'\n[exit {proc.returncode}]'
|
|
67
|
+
return body
|
|
68
|
+
|
|
69
|
+
async def run_interactive_shell(self, command: str, cwd: str, env: dict | None) -> int:
|
|
70
|
+
proc = await asyncio.create_subprocess_shell(command, cwd=cwd, stdin=None, stdout=None, stderr=None, env=env)
|
|
71
|
+
await proc.wait()
|
|
72
|
+
return proc.returncode or 0
|
|
73
|
+
|
|
74
|
+
def exit_app(self, summary: list[dict]) -> None:
|
|
75
|
+
raise SystemExit
|
|
76
|
+
|
|
77
|
+
class BareRepl:
|
|
78
|
+
|
|
79
|
+
def __init__(self, engine: CommandEngine, init_prompt: str | None=None):
|
|
80
|
+
self.engine = engine
|
|
81
|
+
self.adapter = BareAdapter(self)
|
|
82
|
+
self.engine.bind(self.adapter)
|
|
83
|
+
self.engine.attach_agent(self.engine.tabs[0])
|
|
84
|
+
self.init_prompt = init_prompt
|
|
85
|
+
self._pending_nls: int = 0
|
|
86
|
+
self._awaiting_content: bool = False
|
|
87
|
+
self._has_output: bool = False
|
|
88
|
+
self._last_stream_type: str | None = None
|
|
89
|
+
|
|
90
|
+
async def run(self) -> None:
|
|
91
|
+
loop = asyncio.get_running_loop()
|
|
92
|
+
if self.init_prompt:
|
|
93
|
+
p, self.init_prompt = (self.init_prompt, None)
|
|
94
|
+
await self.engine.active_tab.agent.run(p)
|
|
95
|
+
while True:
|
|
96
|
+
try:
|
|
97
|
+
line = await loop.run_in_executor(None, self._read_input)
|
|
98
|
+
except KeyboardInterrupt:
|
|
99
|
+
print('\n(Use /exit or Ctrl-D to quit)')
|
|
100
|
+
continue
|
|
101
|
+
except EOFError:
|
|
102
|
+
print()
|
|
103
|
+
break
|
|
104
|
+
if line is None:
|
|
105
|
+
break
|
|
106
|
+
line = line.strip()
|
|
107
|
+
if not line:
|
|
108
|
+
continue
|
|
109
|
+
if line.lower() in {'exit', 'quit'}:
|
|
110
|
+
break
|
|
111
|
+
await self.engine.handle_input(line)
|
|
112
|
+
tab = self.engine.active_tab
|
|
113
|
+
if tab.running_task is not None:
|
|
114
|
+
try:
|
|
115
|
+
await tab.running_task
|
|
116
|
+
except asyncio.CancelledError:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
def _read_input(self) -> str | None:
|
|
120
|
+
tab = self.engine.active_tab
|
|
121
|
+
prompt = f'[{tab.title}] ≫ '
|
|
122
|
+
lines: list[str] = []
|
|
123
|
+
while True:
|
|
124
|
+
try:
|
|
125
|
+
line = input(prompt)
|
|
126
|
+
except EOFError:
|
|
127
|
+
return None
|
|
128
|
+
lines.append(line)
|
|
129
|
+
if line.endswith('\\'):
|
|
130
|
+
lines[-1] = line[:-1]
|
|
131
|
+
prompt = '... '
|
|
132
|
+
else:
|
|
133
|
+
break
|
|
134
|
+
return '\n'.join(lines)
|
|
135
|
+
|
|
136
|
+
def _handle_event(self, event: dict, tab: TabModel) -> None:
|
|
137
|
+
t, p = (event['type'], event.get('payload', {}))
|
|
138
|
+
if t in ('text_delta', 'thinking_delta'):
|
|
139
|
+
delta = p.get('delta', '')
|
|
140
|
+
if delta:
|
|
141
|
+
self._write_delta(delta, t)
|
|
142
|
+
elif t == 'tool_start':
|
|
143
|
+
self._pending_nls = 0
|
|
144
|
+
self._awaiting_content = False
|
|
145
|
+
self._has_output = True
|
|
146
|
+
self._last_stream_type = t
|
|
147
|
+
elif t == 'tool_end':
|
|
148
|
+
result_msg = p.get('result', {})
|
|
149
|
+
content = result_msg.get('content')
|
|
150
|
+
is_err = p.get('is_error', False)
|
|
151
|
+
out_text = ''
|
|
152
|
+
if content:
|
|
153
|
+
parts: list[str] = []
|
|
154
|
+
if isinstance(content, str):
|
|
155
|
+
parts.append(content)
|
|
156
|
+
elif isinstance(content, list):
|
|
157
|
+
for block in content:
|
|
158
|
+
if isinstance(block, dict) and block.get('type') == 'text':
|
|
159
|
+
parts.append(block.get('text', ''))
|
|
160
|
+
out_text = '\n'.join(parts).strip('\n')
|
|
161
|
+
if is_err:
|
|
162
|
+
prefix = '✗ '
|
|
163
|
+
if not out_text:
|
|
164
|
+
out_text = f'{p.get('name', '?')} failed'
|
|
165
|
+
else:
|
|
166
|
+
prefix = '→ ' if out_text else ''
|
|
167
|
+
if out_text:
|
|
168
|
+
self._write_delta(prefix + out_text, 'tool_result')
|
|
169
|
+
self._last_stream_type = t
|
|
170
|
+
print()
|
|
171
|
+
elif t == 'commit':
|
|
172
|
+
self._pending_nls = 0
|
|
173
|
+
self._awaiting_content = False
|
|
174
|
+
self._has_output = True
|
|
175
|
+
print(f'\n◇ [{p.get('sha', '')}] committed', flush=True)
|
|
176
|
+
self._last_stream_type = t
|
|
177
|
+
elif t == 'error':
|
|
178
|
+
self._pending_nls = 0
|
|
179
|
+
self._awaiting_content = False
|
|
180
|
+
self._has_output = True
|
|
181
|
+
err = str(p.get('error', p))
|
|
182
|
+
print(f'\n✗ {err}', flush=True)
|
|
183
|
+
self._last_stream_type = t
|
|
184
|
+
elif t in ('agent_start', 'turn_start'):
|
|
185
|
+
self._pending_nls = 0
|
|
186
|
+
self._awaiting_content = False
|
|
187
|
+
self._has_output = False
|
|
188
|
+
self._last_stream_type = None
|
|
189
|
+
elif t == 'agent_end':
|
|
190
|
+
self._pending_nls = 0
|
|
191
|
+
if self._has_output:
|
|
192
|
+
print()
|
|
193
|
+
self._last_stream_type = None
|
|
194
|
+
self._has_output = False
|
|
195
|
+
self._awaiting_content = False
|
|
196
|
+
|
|
197
|
+
def _write_delta(self, text: str, delta_type: str) -> None:
|
|
198
|
+
if delta_type != self._last_stream_type:
|
|
199
|
+
self._pending_nls = 0
|
|
200
|
+
self._awaiting_content = True
|
|
201
|
+
self._last_stream_type = delta_type
|
|
202
|
+
if self._awaiting_content:
|
|
203
|
+
text = text.lstrip('\n')
|
|
204
|
+
if not text:
|
|
205
|
+
return
|
|
206
|
+
if self._awaiting_content:
|
|
207
|
+
if self._has_output:
|
|
208
|
+
print()
|
|
209
|
+
self._awaiting_content = False
|
|
210
|
+
if not self._awaiting_content and self._pending_nls > 0:
|
|
211
|
+
print('\n' * self._pending_nls, end='', flush=True)
|
|
212
|
+
self._pending_nls = 0
|
|
213
|
+
rstripped = text.rstrip('\n')
|
|
214
|
+
if rstripped:
|
|
215
|
+
if delta_type == 'thinking_delta':
|
|
216
|
+
print(f'\x1b[2m{rstripped}\x1b[0m', end='', flush=True)
|
|
217
|
+
else:
|
|
218
|
+
print(rstripped, end='', flush=True)
|
|
219
|
+
self._has_output = True
|
|
220
|
+
self._pending_nls = len(text) - len(rstripped)
|
|
221
|
+
|
|
222
|
+
def _render_tab_delimiter(self) -> None:
|
|
223
|
+
tab_strs: list[str] = []
|
|
224
|
+
for i, t in enumerate(self.engine.tabs):
|
|
225
|
+
if i == self.engine.active_index:
|
|
226
|
+
tab_strs.append(f'\x1b[1m▶ {i + 1}. {t.title}\x1b[0m')
|
|
227
|
+
else:
|
|
228
|
+
tab_strs.append(f'\x1b[2m▷ {i + 1}. {t.title}\x1b[0m')
|
|
229
|
+
print('\n' + '┗━━┫ ' + ' ┃ '.join(tab_strs) + ' ┃')
|
|
230
|
+
|
|
231
|
+
def _print_history_for_tab(self, tab: TabModel) -> None:
|
|
232
|
+
for msg in tab.agent.messages:
|
|
233
|
+
role = msg.get('role', '')
|
|
234
|
+
content = msg.get('content', '')
|
|
235
|
+
is_error = msg.get('is_error', False)
|
|
236
|
+
if isinstance(content, list):
|
|
237
|
+
blocks = content
|
|
238
|
+
elif isinstance(content, str):
|
|
239
|
+
blocks = [{'type': 'text', 'text': content}]
|
|
240
|
+
else:
|
|
241
|
+
continue
|
|
242
|
+
if role == 'toolResult':
|
|
243
|
+
parts: list[str] = []
|
|
244
|
+
for block in blocks:
|
|
245
|
+
if isinstance(block, dict) and block.get('type') == 'text':
|
|
246
|
+
t = block.get('text', '').strip('\n')
|
|
247
|
+
if t:
|
|
248
|
+
parts.append(t)
|
|
249
|
+
if parts:
|
|
250
|
+
prefix = '✗ ' if is_error else '→ '
|
|
251
|
+
print(prefix + '\n'.join(parts))
|
|
252
|
+
continue
|
|
253
|
+
for block in blocks:
|
|
254
|
+
btype = block.get('type', 'text')
|
|
255
|
+
if btype == 'toolCall':
|
|
256
|
+
args = block.get('arguments', {})
|
|
257
|
+
if isinstance(args, dict):
|
|
258
|
+
args = json.dumps(args, ensure_ascii=False)
|
|
259
|
+
print(f'⚙ {block.get('name', '')} {args}')
|
|
260
|
+
continue
|
|
261
|
+
text = block.get('text', '') or block.get('thinking', '') or ''
|
|
262
|
+
text = text.strip('\n')
|
|
263
|
+
if not text:
|
|
264
|
+
continue
|
|
265
|
+
if btype == 'thinking':
|
|
266
|
+
print(f'\x1b[2m{text}\x1b[0m')
|
|
267
|
+
elif is_error:
|
|
268
|
+
print(f'✗ {text}')
|
|
269
|
+
elif role == 'user':
|
|
270
|
+
print(f'≫ {text}')
|
|
271
|
+
elif role == 'commit':
|
|
272
|
+
print(f'◇ {text}')
|
|
273
|
+
elif role == 'toolResult':
|
|
274
|
+
print(f'→ {text}')
|
|
275
|
+
else:
|
|
276
|
+
print(text)
|
|
@@ -944,6 +944,8 @@ def main():
|
|
|
944
944
|
parser.add_argument('--skips', nargs='+', default=['(?m)^\\[SUGGESTION MODE[\\s\\S]*', '(?m)^<system-reminder>[\\s\\S]*?^</system-reminder>\\s*'], help='Regex patterns stripped from model output before it is returned to the client')
|
|
945
945
|
parser.add_argument('--stream', default=None, help='File to stream log into')
|
|
946
946
|
parser.add_argument('--bare', action='store_true', help='Use simple terminal REPL instead of TUI')
|
|
947
|
+
parser.add_argument('--web', action='store_true', help='Use web UI instead of TUI')
|
|
948
|
+
parser.add_argument('--web-port', type=int, default=None, help='Port for web UI (default: inference port + 80)')
|
|
947
949
|
args, leash_args = parser.parse_known_args()
|
|
948
950
|
logger.debug(f'args={args!r} leash_args={leash_args!r}')
|
|
949
951
|
if args.engine == 'batch' and args.leash not in ('none', 'noapi'):
|
|
@@ -979,8 +981,13 @@ def main():
|
|
|
979
981
|
if args.engine == 'cache':
|
|
980
982
|
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
981
983
|
if args.leash == 'noapi':
|
|
982
|
-
|
|
983
|
-
|
|
984
|
+
if args.web:
|
|
985
|
+
from .web import run_web
|
|
986
|
+
web_port = args.web_port if args.web_port is not None else port + 80
|
|
987
|
+
run_web(base_url=url, api=args.leash, repo=cwd, env=env, system=args.system, tool_names=args.tools, sdir=args.skill, init_prompt=args.prompt, resume=args.resume, stream=args.stream, host=args.host, port=web_port)
|
|
988
|
+
else:
|
|
989
|
+
from .repl import run_repl
|
|
990
|
+
run_repl(base_url=url, api=args.leash, repo=cwd, env=env, system=args.system, tool_names=args.tools, sdir=args.skill, init_prompt=args.prompt, resume=args.resume, stream=args.stream, bare=args.bare)
|
|
984
991
|
else:
|
|
985
992
|
env['GOOGLE_GEMINI_BASE_URL'] = url
|
|
986
993
|
env['GEMINI_API_KEY'] = 'mc'
|