Fix shell block styling in acme theme

- Add shell-block class with white bg, border, proper border-radius
- Fix info message bubble width and margin for acme theme
- Parse shell output correctly: first $ line is command, rest is output
- Style shell output with proper padding and text color
This commit is contained in:
Levi Neely 2026-05-20 17:22:32 +02:00
parent 0af153bf4f
commit 0ca3d38ec8
3 changed files with 63 additions and 9 deletions

View File

@ -240,11 +240,11 @@ export function ChatPane({ session, cfg, messages, onSend, onStop, browsePath, b
<div key={i} class={`message ${msg.role}`}> <div key={i} class={`message ${msg.role}`}>
<div class="msg-bubble"> <div class="msg-bubble">
{msg.role === 'user' ? ( {msg.role === 'user' ? (
<div class="user-text"><span class="msg-prefix">u: </span>{msg.content}</div> <div class="md" dangerouslySetInnerHTML={{ __html: linkifyPaths((marked.parse(msg.content) as string).replace(/^<p>/, '<p><span class="msg-prefix">u: </span>'), session.cwd) }} />
) : msg.role === 'tool' ? ( ) : msg.role === 'tool' ? (
<ToolCall content={msg.content} /> <ToolCall content={msg.content} />
) : msg.role === 'info' ? ( ) : msg.role === 'info' ? (
<div class="info-text">{msg.content}</div> <InfoMessage content={msg.content} />
) : ( ) : (
<div <div
class="md" class="md"
@ -349,21 +349,27 @@ function DiagramModal({ html, onClose }: { html: string; onClose: () => void })
function formatToolContent(raw: string): { name: string; detail: string; body: string; output: string } { function formatToolContent(raw: string): { name: string; detail: string; body: string; output: string } {
// Call is always on the first line; output follows after the newline. // Call is always on the first line; output follows after the newline.
const nl = raw.indexOf('\n') const nl = raw.indexOf("\n")
const callLine = nl === -1 ? raw : raw.slice(0, nl) const callLine = nl === -1 ? raw : raw.slice(0, nl)
const output = nl === -1 ? '' : raw.slice(nl + 1).trimEnd() const output = nl === -1 ? "" : raw.slice(nl + 1).trimEnd()
const m = callLine.match(/^(\w+)\((.*)\)$/) const m = callLine.match(/^(\w+)\((.*)\)$/)
if (!m) return { name: '', detail: '', body: raw, output: '' } if (!m) return { name: "", detail: "", body: raw, output: "" }
const name = m[1] const name = m[1]
const argsRaw = m[2] const argsRaw = m[2]
try { try {
const obj = JSON.parse(argsRaw) const obj = JSON.parse(argsRaw)
let detail = '' let detail = ""
if (obj.steps?.[0]?.tool) detail = obj.steps[0].tool if (name === "call_tool") {
else if (obj.steps?.[0]?.code) detail = obj.language ?? 'bash' detail = obj.calls?.[0]?.tool ?? ""
} else if (name === "pipe") {
const s = obj.stages?.[0]
detail = s?.tool ?? s?.language ?? "bash"
} else {
detail = obj.steps?.[0]?.language ?? "bash"
}
return { name, detail, body: JSON.stringify(obj, null, 2), output } return { name, detail, body: JSON.stringify(obj, null, 2), output }
} catch { } catch {
return { name, detail: '', body: argsRaw, output } return { name, detail: "", body: argsRaw, output }
} }
} }
@ -383,12 +389,37 @@ const TOOL_ICONS: Record<string, string> = {
task_check: '☑️', task_check: '☑️',
task_clear: '☑️', task_clear: '☑️',
bash: '🖥️', bash: '🖥️',
python3: '🐍',
perl: '🐪',
lua: '🌙',
awk: '🔧',
sed: '🔧',
jq: '🔧',
bc: '🔢',
} }
function toolIcon(detail: string): string { function toolIcon(detail: string): string {
return TOOL_ICONS[detail] ?? '🖥️' return TOOL_ICONS[detail] ?? '🖥️'
} }
function InfoMessage({ content }: { content: string }) {
const lines = content.split('\n')
const shellLines = lines.filter(l => l.startsWith('$ ')).map(l => l.slice(2))
if (shellLines.length === 0) return <div class="info-text">{content}</div>
const cmd = shellLines[0]
const output = shellLines.slice(1).join('\n')
return (
<div class="tool-call shell-block">
<div class="tool-header">
<span class="tool-arrow">🖥️</span>
<span class="tool-name">shell</span>
<span class="tool-detail">{cmd}</span>
</div>
<pre class="tool-output">{output || '(no output)'}</pre>
</div>
)
}
function ToolCall({ content }: { content: string }) { function ToolCall({ content }: { content: string }) {
const { name, detail, body, output } = formatToolContent(content) const { name, detail, body, output } = formatToolContent(content)
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)

View File

@ -797,6 +797,28 @@ details[open] > .tool-header::before {
[data-theme="acme"] .tool-body { [data-theme="acme"] .tool-body {
padding: 4px 10px; padding: 4px 10px;
} }
[data-theme="acme"] .message.info .msg-bubble {
width: 100%;
padding: 0;
margin: 4px 0;
}
[data-theme="acme"] .shell-block {
background: #fff;
border: 1px solid var(--border);
border-radius: var(--r-sm);
}
[data-theme="acme"] .shell-block .tool-header {
cursor: default;
}
[data-theme="acme"] .shell-block .tool-header::before {
display: none;
}
[data-theme="acme"] .shell-block .tool-output {
display: block;
color: var(--text);
font-style: normal;
padding: 8px 12px;
}
[data-theme="acme"] .tool-header { [data-theme="acme"] .tool-header {
padding: 4px 10px; padding: 4px 10px;
} }

View File

@ -3,6 +3,7 @@ import preact from '@preact/preset-vite'
export default defineConfig({ export default defineConfig({
plugins: [preact()], plugins: [preact()],
base: './',
server: { server: {
proxy: { proxy: {
'/s/': 'http://localhost:8080', '/s/': 'http://localhost:8080',