mlx-code 0.0.28__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.
Files changed (30) hide show
  1. {mlx_code-0.0.28 → mlx_code-0.0.29}/PKG-INFO +1 -1
  2. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code/bats.py +1 -2
  3. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code/web.py +1 -1
  4. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code.egg-info/PKG-INFO +1 -1
  5. {mlx_code-0.0.28 → mlx_code-0.0.29}/setup.py +1 -1
  6. {mlx_code-0.0.28 → mlx_code-0.0.29}/LICENSE +0 -0
  7. {mlx_code-0.0.28 → mlx_code-0.0.29}/README.md +0 -0
  8. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code/__init__.py +0 -0
  9. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code/apis.py +0 -0
  10. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code/bare.py +0 -0
  11. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code/gits.py +0 -0
  12. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code/lsp_tool.py +0 -0
  13. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code/main.py +0 -0
  14. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code/mcb.py +0 -0
  15. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code/mcb_tool.py +0 -0
  16. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code/repl.py +0 -0
  17. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code/stream_log.py +0 -0
  18. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code/tools.py +0 -0
  19. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code/tui.py +0 -0
  20. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code/util.py +0 -0
  21. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code/view_git.py +0 -0
  22. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code/view_log.py +0 -0
  23. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code.egg-info/SOURCES.txt +0 -0
  24. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code.egg-info/dependency_links.txt +0 -0
  25. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code.egg-info/entry_points.txt +0 -0
  26. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code.egg-info/requires.txt +0 -0
  27. {mlx_code-0.0.28 → mlx_code-0.0.29}/mlx_code.egg-info/top_level.txt +0 -0
  28. {mlx_code-0.0.28 → mlx_code-0.0.29}/setup.cfg +0 -0
  29. {mlx_code-0.0.28 → mlx_code-0.0.29}/tests/__init__.py +0 -0
  30. {mlx_code-0.0.28 → 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.28
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
@@ -1,4 +1,3 @@
1
- _UI_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">\n<title>MLX Code</title>\n<style>\n*{margin:0;padding:0;box-sizing:border-box}\nbody{font-family:system-ui,-apple-system,sans-serif;background:#0d1117;color:#c9d1d9;height:100vh;display:flex;flex-direction:column}\n#hdr{padding:8px 16px;background:#161b22;border-bottom:1px solid #30363d;display:flex;justify-content:space-between;align-items:center;flex-shrink:0}\n#hdr h1{font-size:14px;font-weight:600}\n#hdr .info{display:flex;gap:8px;align-items:center;font-size:12px;color:#8b949e}\n#hdr button{background:transparent;color:#8b949e;border:1px solid #30363d;border-radius:6px;padding:4px 10px;cursor:pointer;font-size:12px}\n#hdr button:hover{color:#c9d1d9;border-color:#8b949e}\n#chat{flex:1;overflow-y:auto;padding:16px}\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;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}\n.msg-error .msg-body{color:#f85149}\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;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:14px;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}\n#send:hover{background:#2ea043}\n#send:disabled{background:#21262d;color:#484f58;cursor:not-allowed}\n</style>\n</head>\n<body>\n<div id="hdr">\n <h1>⚡ MLX Code</h1>\n <div class="info">\n <span id="status">connecting...</span>\n <button onclick="clearChat()">Clear</button>\n </div>\n</div>\n<div id="chat"><div class="chat-inner" id="chatInner"></div></div>\n<div id="input-area">\n <div class="input-inner">\n <textarea id="input" placeholder="Send a message... (Enter=send, Shift+Enter=newline)" rows="1"></textarea>\n <button id="send" onclick="send()">Send</button>\n </div>\n</div>\n<script>\nconst chatEl=document.getElementById(\'chat\');\nconst innerEl=document.getElementById(\'chatInner\');\nconst inputEl=document.getElementById(\'input\');\nconst sendBtn=document.getElementById(\'send\');\nconst statusEl=document.getElementById(\'status\');\nconst SYSTEM_PROMPT = \'You are a helpful assistant. You are running in a web chat mode with no tool execution capabilities. Answer the user directly and concisely.\';\nlet messages=[{role:\'system\',content:SYSTEM_PROMPT}];\nlet streaming=false;\n\ninputEl.addEventListener(\'keydown\',e=>{\n if(e.key===\'Enter\'&&!e.shiftKey){e.preventDefault();send();}\n});\ninputEl.addEventListener(\'input\',()=>{\n inputEl.style.height=\'auto\';\n inputEl.style.height=Math.min(inputEl.scrollHeight,200)+\'px\';\n});\n\nfunction scrollBottom(){chatEl.scrollTop=chatEl.scrollHeight;}\n\nfunction addMsg(role,label){\n const d=document.createElement(\'div\');\n 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);\n innerEl.appendChild(d);scrollBottom();return b;\n}\n\nfunction clearChat(){\n if(streaming)return;\n messages=[{role:\'system\',content:SYSTEM_PROMPT}];innerEl.innerHTML=\'\';inputEl.focus();\n}\n\nfunction stripToolXml(text){\n // Remove complete <tool_call>...</tool_call> blocks\n text=text.replace(/<tool_call>[\\s\\S]*?<\\/tool_call>/g,\'\');\n // Handle incomplete <tool_call> at end (closing tag not yet received)\n const idx=text.lastIndexOf(\'<tool_call>\');\n if(idx!==-1&&text.indexOf(\'</tool_call>\',idx)===-1)return text.substring(0,idx);\n // Handle partial opening tag at end (e.g. "<tool", "<tool_c")\n const tag=\'<tool_call>\';\n for(let i=tag.length-1;i>0;i--){\n if(text.endsWith(tag.substring(0,i)))return text.substring(0,text.length-i);\n }\n return text;\n}\n\nfunction checkHealth(){\n fetch(\'/health\').then(r=>r.json()).then(d=>{\n statusEl.textContent=d.model||\'ready\';\n }).catch(()=>{statusEl.textContent=\'offline\';});\n}\n\nasync function send(){\n const text=inputEl.value.trim();\n if(!text||streaming)return;\n inputEl.value=\'\';inputEl.style.height=\'auto\';\n messages.push({role:\'user\',content:text});\n addMsg(\'user\',\'≫ You\').textContent=text;\n streaming=true;sendBtn.disabled=true;statusEl.textContent=\'generating...\';\n\n const aBody=addMsg(\'assistant\',\'○ Assistant\');\n let tBody=null,displayText=\'\',thinkText=\'\',rawText=\'\',toolCalls=[];\n const cursor=document.createElement(\'span\');cursor.className=\'cursor\';aBody.appendChild(cursor);\n\n try{\n const resp=await fetch(\'/v1/chat/completions\',{\n method:\'POST\',\n headers:{\'Content-Type\':\'application/json\'},\n body:JSON.stringify({messages,max_tokens:8192})\n });\n\n if(!resp.ok){\n cursor.remove();\n let msg=\'HTTP \'+resp.status;\n try{const e=await resp.json();msg+=\': \'+(e.error||JSON.stringify(e));}catch(_){try{msg+=\': \'+await resp.text();}catch(_){}}\n aBody.textContent=\'✗ \'+msg;\n aBody.parentElement.classList.add(\'msg-error\');\n messages.pop();return;\n }\n\n const reader=resp.body.getReader();\n const dec=new TextDecoder();\n let buf=\'\';\n\n while(true){\n const{done,value}=await reader.read();\n if(done)break;\n buf+=dec.decode(value,{stream:true});\n const lines=buf.split(\'\\n\');\n buf=lines.pop(); // keep partial line in buffer\n\n for(const line of lines){\n if(!line.startsWith(\'data: \'))continue;\n const data=line.slice(6).trim();\n if(!data||data===\'[DONE]\')continue;\n let ch;try{ch=JSON.parse(data);}catch(_){continue;}\n const delta=ch.choices&&ch.choices[0]&&ch.choices[0].delta;\n if(!delta)continue;\n\n if(delta.reasoning_content){\n if(!tBody){\n cursor.remove();\n tBody=addMsg(\'thinking\',\'◌ Thinking\');\n tBody.appendChild(cursor.cloneNode());\n }\n thinkText+=delta.reasoning_content;\n const c=tBody.querySelector(\'.cursor\');if(c)c.remove();\n tBody.textContent=thinkText;\n tBody.appendChild(cursor.cloneNode());\n scrollBottom();\n }\n\n if(delta.content){\n if(tBody){\n const c=tBody.querySelector(\'.cursor\');if(c)c.remove();\n tBody=null;aBody.appendChild(cursor);\n }\n rawText+=delta.content;\n displayText=stripToolXml(rawText);\n cursor.remove();aBody.textContent=displayText;aBody.appendChild(cursor);\n scrollBottom();\n }\n\n if(delta.tool_calls){\n cursor.remove();\n for(const tc of delta.tool_calls){\n const fn=tc.function||{};\n if(fn.name){\n toolCalls.push({name:fn.name,args:\'\'});\n addMsg(\'tool\',\'⚙ \'+fn.name).textContent=fn.name;\n }\n if(fn.arguments&&toolCalls.length>0){\n toolCalls[toolCalls.length-1].args+=fn.arguments;\n const tbs=innerEl.querySelectorAll(\'.msg-tool .msg-body\');\n if(tbs.length>0){\n let disp=toolCalls[toolCalls.length-1].name+\'\\n\';\n try{disp+=JSON.stringify(JSON.parse(toolCalls[toolCalls.length-1].args),null,2);}\n catch(_){disp+=toolCalls[toolCalls.length-1].args;}\n tbs[tbs.length-1].textContent=disp;\n }\n }\n }\n scrollBottom();\n }\n }\n }\n\n cursor.remove();\n if(displayText.trim()){\n aBody.textContent=displayText;\n messages.push({role:\'assistant\',content:displayText});\n }else{\n aBody.textContent=thinkText?\'(thinking only)\':\'(no output)\';\n messages.push({role:\'assistant\',content:displayText});\n }\n if(toolCalls.length>0){\n addMsg(\'tool\',\'⚠ Note\').textContent=\'Tool calls cannot be executed in the web UI. Use the terminal REPL (--bare) for full tool support.\';\n }\n }catch(e){\n cursor.remove();\n aBody.textContent=\'✗ \'+e.message;\n aBody.parentElement.classList.add(\'msg-error\');\n if(messages.length>0&&messages[messages.length-1].role===\'user\')messages.pop();\n }finally{\n streaming=false;sendBtn.disabled=false;\n checkHealth();inputEl.focus();\n }\n}\n\ncheckHealth();inputEl.focus();\n</script>\n</body>\n</html>'
2
1
  import asyncio
3
2
  import json
4
3
  import queue as _queue
@@ -12,7 +11,7 @@ from pathlib import Path
12
11
  import mlx.core as mx
13
12
  from starlette.applications import Starlette
14
13
  from starlette.requests import Request
15
- from starlette.responses import StreamingResponse, JSONResponse, HTMLResponse
14
+ from starlette.responses import StreamingResponse, JSONResponse
16
15
  from starlette.routing import Route
17
16
  import logging
18
17
  logger = logging.getLogger(__name__)
@@ -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">\n<title>MLX Code</title>\n<style>\n*{margin:0;padding:0;box-sizing:border-box}\nbody{font-family:system-ui,-apple-system,sans-serif;background:#0d1117;color:#c9d1d9;height:100vh;display:flex;flex-direction:column}\n#hdr{padding:8px 16px;background:#161b22;border-bottom:1px solid #30363d;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;flex-wrap:wrap;gap:12px}\n#hdr h1{font-size:14px;font-weight:600;white-space:nowrap}\n.filters{display:flex;gap:12px;align-items:center;font-size:11px;color:#8b949e;background:#0d1117;padding:4px 10px;border:1px solid #30363d;border-radius:6px}\n.filters label{display:flex;align-items:center;gap:5px;cursor:pointer;user-select:none}\n.filters input{cursor:pointer;accent-color:#58a6ff;width:12px;height:12px}\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;}\n#tabbar::-webkit-scrollbar{height:4px}\n#tabbar::-webkit-scrollbar-thumb{background:#30363d;border-radius:2px}\n.tab{padding:4px 12px;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}\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:0 2px}\n.close-btn:hover{color:#f85149}\n#newTabBtn{padding:4px 12px;border-radius:6px;cursor:pointer;color:#8b949e;border:1px solid #30363d;background:transparent;font-size:13px;flex-shrink:0}\n#newTabBtn:hover{background:#21262d;color:#c9d1d9}\n#chat{flex:1;overflow-y:auto;padding:16px}\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;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}\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}\n.msg-commit .msg-body{background:rgba(56,139,253,0.08);border-left:2px solid #388bfd;color:#8b949e;font-size:13px}\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;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:14px;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}\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</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> Thinking</label>\n <label><input type="checkbox" id="f-tools" checked> Tools</label>\n <label><input type="checkbox" id="f-results" checked> Results</label>\n <label><input type="checkbox" id="f-commits" checked> Commits</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;\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\nfunction scrollBottom(){chatEl.scrollTop=chatEl.scrollHeight;}\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);scrollBottom();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 streamInnerEl.innerHTML = \'\';\n state.streamBlocks.forEach(block => {\n if (block.type === \'user\') {\n addMsg(\'user\', \'≫ You\', streamInnerEl).textContent = cleanDisplay(block.text);\n } else if (block.type === \'text\') {\n addMsg(\'assistant\', \'○ Assistant\', streamInnerEl).textContent = cleanDisplay(stripToolXml(block.text));\n } else if (block.type === \'thinking\') {\n addMsg(\'thinking\', \'◌ Thinking\', streamInnerEl).textContent = cleanDisplay(block.text);\n } else if (block.type === \'toolCall\') {\n const b = addMsg(\'tool\', \'⚙ \' + block.name, streamInnerEl);\n b.textContent = cleanDisplay(\'⚙ \' + block.name + \'\\n\' + JSON.stringify(block.arguments, null, 2));\n } else if (block.type === \'toolResult\') {\n addMsg(\'tool-result\', \'→ Result\', streamInnerEl).textContent = cleanDisplay(\'→ \' + block.text);\n } else if (block.type === \'commit\') {\n addMsg(\'commit\', \'◇ Commit\', streamInnerEl).textContent = cleanDisplay(\'◇ [\' + block.sha + \'] committed\');\n } else if (block.type === \'command\') {\n const b = addMsg(\'tool\', \'✓ \' + block.command, streamInnerEl);\n b.textContent = cleanDisplay(block.output || \'\');\n } else if (block.type === \'shell\') {\n const b = addMsg(\'tool\', \'! \' + block.command, streamInnerEl);\n b.textContent = cleanDisplay(block.output || \'\');\n }\n });\n scrollBottom();\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: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>'
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.28
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
@@ -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.28",
14
+ version="0.0.29",
15
15
  readme="README.md",
16
16
  description="Coding Agent for Mac",
17
17
  long_description=open("README.md").read(),
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