|
288 | 288 | .tl { display: flex; gap: 11px; margin: 0 4px; position: relative; } |
289 | 289 | .tl-rail { flex: 0 0 22px; position: relative; } |
290 | 290 | .tl-rail::before { content: ''; position: absolute; left: 11px; top: 0; bottom: 0; width: 2px; transform: translateX(-1px); background: var(--border); } |
| 291 | + /* Thread the rail across #log's flex-gap between CONSECUTIVE top-level rows, so a run of tool / |
| 292 | + approval / group nodes reads as ONE connected line (GitHub/Cursor/Claude-style) instead of the |
| 293 | + disconnected stubs a per-row rail otherwise leaves — this is what made an MCP run (approval → |
| 294 | + tool → approval → tool …) look "cut in the middle". A narration bubble between two rows still |
| 295 | + breaks the rail (intended — a new train of thought). The -12px MUST equal #log's `gap`; it is |
| 296 | + scoped to #log's DIRECT children so group members (nested .tl rows inside a .groupbody, already |
| 297 | + contiguous within their own rail) are never touched. */ |
| 298 | + #log > .tl + .tl > .tl-rail::before { top: -12px; } |
291 | 299 | .tl-node { position: absolute; left: 0; top: 3px; width: 22px; height: 22px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; |
292 | 300 | background: var(--vscode-editor-background, var(--field-bg)); border: 1.5px solid var(--border); color: var(--muted); z-index: 1; } |
293 | 301 | .tl-node .ci { width: 12px; height: 12px; } |
|
1849 | 1857 | function addAgentLine(icon, text, label, kind, path){ |
1850 | 1858 | clearEmpty(); clearStatus(); |
1851 | 1859 | const st = chipStep(icon, text, label, kind, path); st.status = 'done'; |
1852 | | - const d = document.createElement('div'); d.className = 'tl tl-tool'; |
1853 | 1860 | // Inside a group the row reads as the model's sentence ("Read the runAgent call site"); the raw |
1854 | 1861 | // tool text stays as the tooltip so the underlying path/query is never lost. |
1855 | 1862 | const shown = (groupsOn && st.base) ? st.base : String(text || ''); |
1856 | | - d.innerHTML = '<div class="tl-rail"><span class="tl-node">' + (IC[icon] ? codicon(icon) : esc(icon || '•')) + '</span></div>' |
| 1863 | + const rowHtml = '<div class="tl-rail"><span class="tl-node">' + (IC[icon] ? codicon(icon) : esc(icon || '•')) + '</span></div>' |
1857 | 1864 | + '<div class="tl-body"><span class="toolt" title="' + escAttr(text || '') + '">' + esc(shown) + '</span></div>'; |
| 1865 | + // MCP: an APPROVED tool call's run-node folds INTO the approval chip that gated it (mcpMergePending, |
| 1866 | + // set in addMcpApproval) so the pair is ONE row — Copilot-style — instead of "Approved …" plus a |
| 1867 | + // separate "🔌 …". An allow-listed call has no pending chip and renders its own node below; a |
| 1868 | + // skipped call never posts a node at all. |
| 1869 | + if (kind === 'mcp' && mcpMergePending){ |
| 1870 | + mcpMergePending.className = 'tl tl-tool'; |
| 1871 | + mcpMergePending.innerHTML = rowHtml; |
| 1872 | + mcpMergePending = null; |
| 1873 | + scrollIfStuck(); |
| 1874 | + return; |
| 1875 | + } |
| 1876 | + mcpMergePending = null; // any other timeline row closes a stale merge window |
| 1877 | + const d = document.createElement('div'); d.className = 'tl tl-tool'; |
| 1878 | + d.innerHTML = rowHtml; |
1858 | 1879 | groupAppend(d, st); |
1859 | 1880 | scrollIfStuck(); |
1860 | 1881 | } |
|
1894 | 1915 | // I being asked", which is exactly what it is for. On decision it collapses to a one-line verdict; |
1895 | 1916 | // the live run card follows with the output. |
1896 | 1917 | let pendingApproval = null; // { done } while a decision is awaited — Enter approves, Esc skips |
| 1918 | + // An APPROVED MCP tool call's run-node folds into the approval chip that gated it (set in |
| 1919 | + // addMcpApproval's done(), consumed in addAgentLine) so the call is ONE timeline row, not "Approved …" |
| 1920 | + // plus a separate "🔌 …". Null except in the brief window between granting approval and the node arriving. |
| 1921 | + let mcpMergePending = null; |
1897 | 1922 | // MCP tool call (S4) — its own card: server · tool · arguments, so the user sees exactly what a |
1898 | 1923 | // third-party tool is about to do. Args are shown in full (capped host-side): that IS the decision. |
1899 | 1924 | // G1 consent card: a repo-authored .levelcode/mcp.json wants to SPAWN A PROCESS. This is the one |
|
1982 | 2007 | '<div class="cmdhead"><span class="cmdverb">' + verb + '</span>' |
1983 | 2008 | + '<span class="cmdchips"><code>' + esc(m.server || '') + '</code><code>' + esc(m.tool || '') + '</code></span>' |
1984 | 2009 | + '<span class="cmdstate ' + (approved ? 'ok' : 'bad') + '">' + codicon(approved ? 'check-circle' : 'circle-slash') + '</span></div>'; |
| 2010 | + // Approved → the tool runs next and posts its own run-node; hand THIS row to it so the two merge |
| 2011 | + // into one line (addAgentLine). Skipped → nothing runs, so the chip stays as the record. |
| 2012 | + mcpMergePending = approved ? card : null; |
1985 | 2013 | forceStick(); |
1986 | 2014 | }; |
1987 | 2015 | card.querySelector('.approve').onclick = () => done(true, false); // Allow once (primary) |
|
3238 | 3266 | else if (m.type === 'autoContext'){ addAutoCtx(m.names); } |
3239 | 3267 | else if (m.type === 'mode'){ applyMode(!!m.agent); } |
3240 | 3268 | else if (m.type === 'autopilot'){ applyAutopilot(!!m.on); } |
3241 | | - else if (m.type === 'agentStart'){ closeGroup(); setStreaming(true); agentBubble = null; agentRaw = ''; turnLabeled = false; renderPlan([]); setAgentStatus('thinking…'); } |
| 3269 | + else if (m.type === 'agentStart'){ closeGroup(); setStreaming(true); agentBubble = null; agentRaw = ''; turnLabeled = false; mcpMergePending = null; renderPlan([]); setAgentStatus('thinking…'); } |
3242 | 3270 | else if (m.type === 'agentStatus'){ finishAgentBubble(); setAgentStatus(m.text || 'working…'); } |
3243 | 3271 | else if (m.type === 'agentDelta'){ clearStatus(); setWork('Responding…'); if (!agentBubble){ agentBubble = makeStream(add('assistant', '')); agentRaw = ''; } agentRaw += m.text; streamFeed(agentBubble, agentRaw); } |
3244 | 3272 | else if (m.type === 'agentTurnEnd'){ finishAgentBubble(); } |
|
0 commit comments