diff --git a/.changeset/shell-syntax-highlighting.md b/.changeset/shell-syntax-highlighting.md new file mode 100644 index 00000000000..038f51dd685 --- /dev/null +++ b/.changeset/shell-syntax-highlighting.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Add syntax highlighting to shell command output with Shiki, labeled "Command" and "Output" sections, per-section copy buttons, and an "Open in Editor" action that opens the full untruncated output in a VS Code editor tab. diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/message-list-tool-to-queued-user-spacing-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/message-list-tool-to-queued-user-spacing-chromium-linux.png index ff195b4d918..edc40570888 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/message-list-tool-to-queued-user-spacing-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/message-list-tool-to-queued-user-spacing-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3f37193389ad7937c7b2103ad2fe3dd63123a0d4f60a0fbfd65b31d3982c092 -size 12105 +oid sha256:dbaab3a1ff10eac8f710b0ed8ddc99d96188c06a09c227e634ca712bcee12130 +size 11543 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/components-shell/shell-execution-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/components-shell/shell-execution-chromium-linux.png index 4d612a7ad03..ddcba78b314 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/components-shell/shell-execution-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/components-shell/shell-execution-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74a42ec77a8ac8d5a1c555b46e3d550c9acf3198f45cc87da6582c0ff924d5f9 -size 17677 +oid sha256:7cabfff215cef485ec913b9a082aaac844275e506e88ba0fab112d95ff6de329 +size 18200 diff --git a/packages/kilo-i18n/src/ar.ts b/packages/kilo-i18n/src/ar.ts index 7dd1f9304fa..d7a778caa65 100644 --- a/packages/kilo-i18n/src/ar.ts +++ b/packages/kilo-i18n/src/ar.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "أوقف لقطات Kilo لهذا المشروع. ستفقد ميزة التراجع/الإعادة لتعديلات Kilo، لكن git سيستمر في تتبع كل شيء.", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "فتح في عارض الفروقات", + "ui.messagePart.shell.command": "الأمر", + "ui.messagePart.shell.output": "المخرجات", + "ui.messagePart.openInEditor": "فتح في المحرر", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "كان هذا مفيدًا", diff --git a/packages/kilo-i18n/src/br.ts b/packages/kilo-i18n/src/br.ts index e79ef39bf30..bbf53cd2ff3 100644 --- a/packages/kilo-i18n/src/br.ts +++ b/packages/kilo-i18n/src/br.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Desligue os snapshots do Kilo para este projeto. Você perde desfazer/refazer das mudanças feitas pelo Kilo, mas o git continua rastreando tudo.", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "Abrir no Visualizador de Diferenças", + "ui.messagePart.shell.command": "Comando", + "ui.messagePart.shell.output": "Saída", + "ui.messagePart.openInEditor": "Abrir no Editor", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Isso foi útil", diff --git a/packages/kilo-i18n/src/bs.ts b/packages/kilo-i18n/src/bs.ts index e23e964bbd5..2e4983260dc 100644 --- a/packages/kilo-i18n/src/bs.ts +++ b/packages/kilo-i18n/src/bs.ts @@ -96,7 +96,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Isključi Kilo snapshotove za ovaj projekat. Izgubićete poništi/vrati za izmjene koje napravi Kilo, ali git i dalje prati sve.", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "Otvori u pregledniku razlika", + "ui.messagePart.shell.command": "Naredba", + "ui.messagePart.shell.output": "Izlaz", + "ui.messagePart.openInEditor": "Otvori u editoru", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Ovo je bilo korisno", diff --git a/packages/kilo-i18n/src/da.ts b/packages/kilo-i18n/src/da.ts index 2a086970b40..e930733acbe 100644 --- a/packages/kilo-i18n/src/da.ts +++ b/packages/kilo-i18n/src/da.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Slå Kilos snapshots fra for dette projekt. Du mister fortryd/gentag for Kilo-ændringer, men git sporer stadig alt.", - "ui.messagePart.openInDiffViewer": "Åbn i diff-viser", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "Åbn i Diff-visning", + "ui.messagePart.shell.command": "Kommando", + "ui.messagePart.shell.output": "Output", + "ui.messagePart.openInEditor": "Åbn i editor", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Dette var nyttigt", diff --git a/packages/kilo-i18n/src/de.ts b/packages/kilo-i18n/src/de.ts index 437f5fc649f..ce73601069b 100644 --- a/packages/kilo-i18n/src/de.ts +++ b/packages/kilo-i18n/src/de.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Kilo-Snapshots für dieses Projekt ausschalten. Rückgängig/Wiederherstellen für Kilo-Änderungen ist nicht mehr möglich, aber git verfolgt weiterhin alles.", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "Im Diff-Viewer öffnen", + "ui.messagePart.shell.command": "Befehl", + "ui.messagePart.shell.output": "Ausgabe", + "ui.messagePart.openInEditor": "Im Editor öffnen", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Das war hilfreich", diff --git a/packages/kilo-i18n/src/en.ts b/packages/kilo-i18n/src/en.ts index bd80d92857d..7f908032995 100644 --- a/packages/kilo-i18n/src/en.ts +++ b/packages/kilo-i18n/src/en.ts @@ -99,6 +99,10 @@ export const dict = { // Edit-tool header: hover-revealed action opening the diff in a full tab. "ui.messagePart.openInDiffViewer": "Open in Diff Viewer", + // Shell-tool section labels and actions. + "ui.messagePart.shell.command": "Command", + "ui.messagePart.shell.output": "Output", + "ui.messagePart.openInEditor": "Open in Editor", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "This was helpful", diff --git a/packages/kilo-i18n/src/es.ts b/packages/kilo-i18n/src/es.ts index 8b05522cb9f..07af68a1a0f 100644 --- a/packages/kilo-i18n/src/es.ts +++ b/packages/kilo-i18n/src/es.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Apaga las instantáneas de Kilo para este proyecto. Perderás deshacer/rehacer de los cambios de Kilo, pero git seguirá rastreando todo.", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "Abrir en el visor de diferencias", + "ui.messagePart.shell.command": "Comando", + "ui.messagePart.shell.output": "Salida", + "ui.messagePart.openInEditor": "Abrir en el editor", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Esto fue útil", diff --git a/packages/kilo-i18n/src/fr.ts b/packages/kilo-i18n/src/fr.ts index b64ace8f1f9..2faf61d345b 100644 --- a/packages/kilo-i18n/src/fr.ts +++ b/packages/kilo-i18n/src/fr.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Désactivez les instantanés Kilo pour ce projet. Vous perdez l'annulation/restauration des modifications faites par Kilo, mais git continue de tout suivre.", - "ui.messagePart.openInDiffViewer": "Ouvrir dans la visionneuse de différences", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "Ouvrir dans le visualiseur de différences", + "ui.messagePart.shell.command": "Commande", + "ui.messagePart.shell.output": "Sortie", + "ui.messagePart.openInEditor": "Ouvrir dans l'éditeur", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "C'était utile", diff --git a/packages/kilo-i18n/src/ja.ts b/packages/kilo-i18n/src/ja.ts index 9d5214c7a48..f30eae15557 100644 --- a/packages/kilo-i18n/src/ja.ts +++ b/packages/kilo-i18n/src/ja.ts @@ -89,7 +89,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "このプロジェクトでは Kilo のスナップショットを無効にします。Kilo による変更の取り消し/やり直しはできなくなりますが、git は引き続きすべてを追跡します。", - "ui.messagePart.openInDiffViewer": "差分ビューアで開く", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "差分ビューアーで開く", + "ui.messagePart.shell.command": "コマンド", + "ui.messagePart.shell.output": "出力", + "ui.messagePart.openInEditor": "エディタで開く", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "役に立ちました", diff --git a/packages/kilo-i18n/src/ko.ts b/packages/kilo-i18n/src/ko.ts index 6308cf9fc68..8a9692001fe 100644 --- a/packages/kilo-i18n/src/ko.ts +++ b/packages/kilo-i18n/src/ko.ts @@ -89,7 +89,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "이 프로젝트의 Kilo 스냅샷을 끕니다. Kilo 변경에 대한 실행 취소/다시 실행은 사용할 수 없지만 git은 여전히 모든 것을 추적합니다.", - "ui.messagePart.openInDiffViewer": "Diff 뷰어에서 열기", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "차이점 뷰어에서 열기", + "ui.messagePart.shell.command": "명령어", + "ui.messagePart.shell.output": "출력", + "ui.messagePart.openInEditor": "편집기에서 열기", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "도움이 됐어요", diff --git a/packages/kilo-i18n/src/nl.ts b/packages/kilo-i18n/src/nl.ts index 4fb55eddca5..142c0958286 100644 --- a/packages/kilo-i18n/src/nl.ts +++ b/packages/kilo-i18n/src/nl.ts @@ -93,7 +93,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Zet Kilo-snapshots uit voor dit project. Je verliest ongedaan maken/opnieuw doen van Kilo-wijzigingen, maar git blijft alles volgen.", - "ui.messagePart.openInDiffViewer": "Openen in diff-viewer", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "Openen in Diff-weergave", + "ui.messagePart.shell.command": "Opdracht", + "ui.messagePart.shell.output": "Uitvoer", + "ui.messagePart.openInEditor": "Openen in editor", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Dit was nuttig", diff --git a/packages/kilo-i18n/src/no.ts b/packages/kilo-i18n/src/no.ts index 915801390af..e7f223b923b 100644 --- a/packages/kilo-i18n/src/no.ts +++ b/packages/kilo-i18n/src/no.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Slå av Kilos snapshots for dette prosjektet. Du mister angre/gjør om for Kilo-endringer, men git fortsetter å spore alt.", - "ui.messagePart.openInDiffViewer": "Åpne i diff-viser", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "Åpne i diff-visning", + "ui.messagePart.shell.command": "Kommando", + "ui.messagePart.shell.output": "Utdata", + "ui.messagePart.openInEditor": "Åpne i editor", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Dette var nyttig", diff --git a/packages/kilo-i18n/src/pl.ts b/packages/kilo-i18n/src/pl.ts index 67b83e46c29..8cfd61e3c2b 100644 --- a/packages/kilo-i18n/src/pl.ts +++ b/packages/kilo-i18n/src/pl.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Wyłącz snapshoty Kilo dla tego projektu. Stracisz cofanie/przywracanie zmian Kilo, ale git nadal śledzi wszystko.", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "Otwórz w podglądzie różnic", + "ui.messagePart.shell.command": "Polecenie", + "ui.messagePart.shell.output": "Wyjście", + "ui.messagePart.openInEditor": "Otwórz w edytorze", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "To było pomocne", diff --git a/packages/kilo-i18n/src/ru.ts b/packages/kilo-i18n/src/ru.ts index d9cff803db5..2a923436c22 100644 --- a/packages/kilo-i18n/src/ru.ts +++ b/packages/kilo-i18n/src/ru.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Выключите снимки Kilo для этого проекта. Вы потеряете отмену/повтор изменений Kilo, но git по-прежнему отслеживает всё.", - "ui.messagePart.openInDiffViewer": "Открыть в просмотрщике различий", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "Открыть в просмотре различий", + "ui.messagePart.shell.command": "Команда", + "ui.messagePart.shell.output": "Вывод", + "ui.messagePart.openInEditor": "Открыть в редакторе", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Это было полезно", diff --git a/packages/kilo-i18n/src/th.ts b/packages/kilo-i18n/src/th.ts index be90eddc10a..ae0c5a4d88c 100644 --- a/packages/kilo-i18n/src/th.ts +++ b/packages/kilo-i18n/src/th.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "ปิดสแน็ปช็อตของ Kilo สำหรับโปรเจกต์นี้ คุณจะสูญเสียการยกเลิก/ทำซ้ำสำหรับการเปลี่ยนแปลงของ Kilo แต่ git ยังคงติดตามทุกอย่าง", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "เปิดในตัวดูความแตกต่าง", + "ui.messagePart.shell.command": "คำสั่ง", + "ui.messagePart.shell.output": "ผลลัพธ์", + "ui.messagePart.openInEditor": "เปิดในตัวแก้ไข", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "สิ่งนี้มีประโยชน์", diff --git a/packages/kilo-i18n/src/tr.ts b/packages/kilo-i18n/src/tr.ts index 0ebf2a73f24..6297ed3295a 100644 --- a/packages/kilo-i18n/src/tr.ts +++ b/packages/kilo-i18n/src/tr.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Bu proje için Kilo anlık görüntülerini kapat. Kilo değişiklikleri için geri alma/yeniden yapmayı kaybedersiniz, ancak git her şeyi izlemeye devam eder.", - "ui.messagePart.openInDiffViewer": "Fark görüntüleyicide aç", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "Fark Görüntüleyicide Aç", + "ui.messagePart.shell.command": "Komut", + "ui.messagePart.shell.output": "Çıktı", + "ui.messagePart.openInEditor": "Düzenleyicide Aç", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Bu yardımcı oldu", diff --git a/packages/kilo-i18n/src/uk.ts b/packages/kilo-i18n/src/uk.ts index 3e325a33f7d..afc11ae14e1 100644 --- a/packages/kilo-i18n/src/uk.ts +++ b/packages/kilo-i18n/src/uk.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Вимкніть знімки Kilo для цього проєкту. Ви втратите скасування/повторення для змін Kilo, але git продовжить відстежувати все.", - "ui.messagePart.openInDiffViewer": "Відкрити у переглядачі відмінностей", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "Відкрити в переглядачі відмінностей", + "ui.messagePart.shell.command": "Команда", + "ui.messagePart.shell.output": "Вивід", + "ui.messagePart.openInEditor": "Відкрити в редакторі", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Це було корисно", diff --git a/packages/kilo-i18n/src/zh.ts b/packages/kilo-i18n/src/zh.ts index 29a57c5290d..37954ff59d2 100644 --- a/packages/kilo-i18n/src/zh.ts +++ b/packages/kilo-i18n/src/zh.ts @@ -86,7 +86,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "关闭本项目的 Kilo 快照。你将失去对 Kilo 更改的撤销/重做,但 git 仍会追踪所有内容。", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "在差异查看器中打开", + "ui.messagePart.shell.command": "命令", + "ui.messagePart.shell.output": "输出", + "ui.messagePart.openInEditor": "在编辑器中打开", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "这有帮助", diff --git a/packages/kilo-i18n/src/zht.ts b/packages/kilo-i18n/src/zht.ts index 08582812567..5561173b89d 100644 --- a/packages/kilo-i18n/src/zht.ts +++ b/packages/kilo-i18n/src/zht.ts @@ -86,7 +86,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "關閉本專案的 Kilo 快照。你將失去對 Kilo 變更的撤銷/重做,但 git 仍會追蹤所有內容。", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "在差異檢視器中開啟", + "ui.messagePart.shell.command": "命令", + "ui.messagePart.shell.output": "輸出", + "ui.messagePart.openInEditor": "在編輯器中開啟", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "這有幫助", diff --git a/packages/kilo-ui/src/components/message-part.css b/packages/kilo-ui/src/components/message-part.css index bb6f99c515a..5494f7c0c71 100644 --- a/packages/kilo-ui/src/components/message-part.css +++ b/packages/kilo-ui/src/components/message-part.css @@ -195,11 +195,69 @@ html[data-theme="kilo-vscode"] [data-component="bash-output"] { color: var(--vscode-terminal-foreground, var(--vscode-editor-foreground)); } - [data-slot="bash-copy"] [data-component="icon-button"][data-variant="secondary"] { - background: var(--vscode-terminal-background, var(--vscode-panel-background)); + [data-slot="bash-divider"] { + background: var(--vscode-panel-border, var(--border-weak-base)); } } +/* Bash tool: section layout (command + output) */ +[data-component="bash-output"] [data-slot="bash-section"] { + position: relative; +} + +[data-component="bash-output"] [data-slot="bash-section-code"] { + overflow-y: auto; + overflow-x: hidden; + max-height: 240px; + scrollbar-width: none; + &::-webkit-scrollbar { + display: none; + } +} + +[data-component="bash-output"] [data-slot="bash-section-actions"] { + position: absolute; + top: 4px; + right: 4px; + display: flex; + align-items: center; + gap: 2px; + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease; +} + +[data-component="bash-output"] [data-slot="bash-section"]:hover [data-slot="bash-section-actions"], +[data-component="bash-output"] [data-slot="bash-section"]:focus-within [data-slot="bash-section-actions"] { + opacity: 1; + pointer-events: auto; +} + +/* Edge-to-edge divider (matches MCP tool divider) */ +[data-component="bash-output"] [data-slot="bash-divider"] { + height: 1px; + background: var(--border-weak-base); +} + +/* Shiki syntax-highlighted shell output */ +[data-component="bash-output"] .shiki, +[data-component="shell-expanded-output"] .shiki { + margin: 0; + padding: 12px; + background: transparent !important; +} + +[data-component="bash-output"] .shiki code, +[data-component="shell-expanded-output"] .shiki code { + font-family: var(--font-family-mono); + font-feature-settings: var(--font-family-mono--font-feature-settings); + font-size: var(--font-size-small); + line-height: var(--line-height-large); + white-space: pre-wrap; + overflow-wrap: anywhere; + background: transparent; +} + /* Tool error inline card (upstream removed these styles in v1.2.25) */ [data-component="tool-error"] { display: flex; diff --git a/packages/kilo-ui/src/components/message-part.tsx b/packages/kilo-ui/src/components/message-part.tsx index 5df2fd231a1..8e1e72648a0 100644 --- a/packages/kilo-ui/src/components/message-part.tsx +++ b/packages/kilo-ui/src/components/message-part.tsx @@ -54,6 +54,8 @@ import { ContextToolGroupHeader, ContextToolExpandedList, ContextToolRollingResu import { ShellRollingResults } from "./shell-rolling-results" import { extractFilePathFromHref } from "../file-path" import { normalize } from "./session-diff" +import { deferredHighlight } from "../context/marked" +import { escapeHtml } from "../util/escape-html" // Windows CLI tools (e.g. winget) use \r to overwrite progress bars in-place. // Without this, every progress frame renders as a separate visual line. @@ -2028,6 +2030,112 @@ ToolRegistry.register({ }, }) +function BashCopyButton(props: { value: () => string; label: string }) { + const i18n = useI18n() + const [copied, setCopied] = createSignal(false) + const handler = async () => { + const text = props.value() + if (!text) return + await navigator.clipboard.writeText(text) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + return ( + + + + ) +} + +function BashHighlightedOutput(props: { cmd: string; output: string; outputPath?: string; active?: boolean }) { + const data = useData() + const i18n = useI18n() + const cmdState = { signal: { aborted: false } } + const outState = { signal: { aborted: false } } + let cmdRef: HTMLDivElement | undefined + let outRef: HTMLDivElement | undefined + + createEffect(() => { + cmdState.signal.aborted = true + if (!props.active) return + const cmd = props.cmd + if (!cmdRef || !cmd) return + const signal = { aborted: false } + cmdState.signal = signal + cmdRef.innerHTML = `
${escapeHtml(cmd)}
` + void deferredHighlight(cmdRef, undefined, signal) + }) + + createEffect(() => { + outState.signal.aborted = true + if (!props.active) return + const out = props.output + if (!outRef || !out) return + const signal = { aborted: false } + outState.signal = signal + outRef.innerHTML = `
${escapeHtml(out)}
` + void deferredHighlight(outRef, undefined, signal) + }) + + onCleanup(() => { + cmdState.signal.aborted = true + outState.signal.aborted = true + }) + + const openInEditor = () => { + // When output was truncated, open the full output file on disk + if (props.outputPath && data.openFile) { + data.openFile(props.outputPath) + return + } + if (!data.openContent) return + data.openContent(props.output, "log") + } + + return ( +
+ +
{i18n.t("ui.messagePart.shell.command")}
+
+
+
+ props.cmd} label={i18n.t("ui.message.copy")} /> +
+
+ + +
+ + +
{i18n.t("ui.messagePart.shell.output")}
+
+
+
+ + + + + + props.output} label={i18n.t("ui.message.copy")} /> +
+
+ +
+ ) +} + ToolRegistry.register({ name: "bash", render(props) { @@ -2035,6 +2143,7 @@ ToolRegistry.register({ const pending = () => busy(props.status) const reveal = useToolReveal(pending, () => props.reveal !== false) const subtitle = () => props.input.description ?? props.metadata.description + const [open, setOpen] = createSignal(props.defaultOpen ?? true) // also apply processCarriageReturns for Windows CLI tools const cmd = createMemo(() => { @@ -2048,19 +2157,6 @@ ToolRegistry.register({ return "" }) const out = createMemo(() => processCarriageReturns(stripAnsi(rawOutput()))) - const text = createMemo(() => `$ ${cmd()}${out() ? "\n\n" + out() : ""}`) - - const hasOutput = createMemo(() => out().length > 0) - const [copied, setCopied] = createSignal(false) - - const handleCopy = async () => { - const command = cmd() - if (!command) return - const content = out() ? `${command}\n\n${out()}` : command - await navigator.clipboard.writeText(content) - setCopied(true) - setTimeout(() => setCopied(false), 2000) - } return ( @@ -2080,29 +2177,7 @@ ToolRegistry.register({
} > -
-
- - e.preventDefault()} - onClick={handleCopy} - aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")} - /> - -
-
-
-              {text()}
-            
-
-
+ ) }, diff --git a/packages/kilo-ui/src/components/shell-rolling-results.tsx b/packages/kilo-ui/src/components/shell-rolling-results.tsx index e5310b6ce08..573e2e44bb0 100644 --- a/packages/kilo-ui/src/components/shell-rolling-results.tsx +++ b/packages/kilo-ui/src/components/shell-rolling-results.tsx @@ -3,6 +3,8 @@ import stripAnsi from "strip-ansi" import type { ToolPart } from "@kilocode/sdk/v2" import { useReducedMotion } from "../hooks/use-reduced-motion" import { useI18n } from "../context/i18n" +import { deferredHighlight } from "../context/marked" +import { escapeHtml } from "../util/escape-html" import { RollingResults } from "./rolling-results" import { Icon } from "./icon" import { IconButton } from "./icon-button" @@ -57,6 +59,28 @@ function ShellRollingCommand(props: { text: string; animate?: boolean }) { ) } +function ShellOutputHighlight(props: { code: string; active?: boolean }) { + const state = { signal: { aborted: false } } + let ref: HTMLDivElement | undefined + + createEffect(() => { + state.signal.aborted = true + if (!props.active) return + const code = props.code + if (!ref || !code) return + const signal = { aborted: false } + state.signal = signal + ref.innerHTML = `
${escapeHtml(code)}
` + void deferredHighlight(ref, undefined, signal) + }) + + onCleanup(() => { + state.signal.aborted = true + }) + + return
+} + function ShellExpanded(props: { cmd: string; out: string; open: boolean }) { const i18n = useI18n() const rows = 10 @@ -156,9 +180,7 @@ function ShellExpanded(props: { cmd: string; out: string; open: boolean }) { onScroll={updateMask} style={{ "max-height": `${cap()}px` }} > -
-                  {props.out}
-                
+
diff --git a/packages/kilo-ui/src/util/escape-html.ts b/packages/kilo-ui/src/util/escape-html.ts new file mode 100644 index 00000000000..243d85be4c6 --- /dev/null +++ b/packages/kilo-ui/src/util/escape-html.ts @@ -0,0 +1,3 @@ +export function escapeHtml(s: string): string { + return s.replace(/&/g, "&").replace(//g, ">") +} diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 96932f34d35..0aa9c66aba8 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -607,6 +607,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper await routeSuggestionWebviewMessage(this.questionCtx, message) if (await ModelState.handleMessage(message.type, message, this.client, (msg) => this.postMessage(msg))) return if (await routeAutocompleteMessage(message, (msg) => this.postMessage(msg))) return + if (this.handleEditorOpenMessage(message)) return if ( await handleSidebarWorktreeMessage(message, { post: (msg) => this.postMessage(msg), @@ -771,11 +772,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper break case "saveImage": return saveImage(this.getWorkspaceDirectory(this.currentSession?.id), message) - case "openFile": - if (message.filePath) { - this.handleOpenFile(message.filePath, message.line, message.column) - } - break case "requestProviders": this.fetchAndSendProviders().catch((e) => console.error("[Kilo New] fetchAndSendProviders failed:", e)) break @@ -2897,6 +2893,35 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper .then(open, (err) => console.error("[Kilo New] KiloProvider: Failed to preview image:", err)) } + private handleEditorOpenMessage(message: { + type?: string + filePath?: string + line?: number + column?: number + content?: string + language?: string + }): boolean { + if (message.type === "openFile") { + if (message.filePath) this.handleOpenFile(message.filePath, message.line, message.column) + return true + } + if (message.type === "openContent") { + if (message.content) this.handleOpenContent(message.content, message.language) + return true + } + return false + } + + /** + * Handle openContent request - open arbitrary text in an untitled VS Code editor tab. + */ + private handleOpenContent(content: string, language?: string): void { + vscode.workspace.openTextDocument({ content, language: language || "log" }).then( + (doc) => vscode.window.showTextDocument(doc, { preview: true }), + (err) => console.error("[Kilo New] KiloProvider: Failed to open content:", err), + ) + } + /** * Handle openFile request from the webview — open a file in the VS Code editor. * Resolves relative paths against the current session's directory (which may be diff --git a/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts b/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts index 727c6a9b2e7..1397f191010 100644 --- a/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts @@ -128,6 +128,13 @@ describe("DataProvider contract (runtime)", () => { expect(src).toContain("OpenDiffFn") expect(src).toMatch(/openDiff:\s*props\.onOpenDiff/) }) + + it("DataProvider accepts onOpenContent prop and exports OpenContentFn (source)", () => { + const src = fs.readFileSync(DATA_CONTEXT_FILE, "utf-8") + expect(src).toContain("onOpenContent") + expect(src).toContain("OpenContentFn") + expect(src).toMatch(/openContent:\s*props\.onOpenContent/) + }) }) describe("Edit tool diff-first click contract (source)", () => { @@ -164,6 +171,53 @@ describe("Write and apply_patch patch rendering contracts (source)", () => { }) }) +describe("Bash tool syntax highlighting and section labels (source)", () => { + const src = fs.readFileSync(KILO_MESSAGE_PART_FILE, "utf-8") + const block = + src.match(/ToolRegistry\.register\(\{\s*name:\s*"bash"[\s\S]*?(?=ToolRegistry\.register\(|$)/)?.[0] ?? "" + + it("bash tool renders BashHighlightedOutput", () => { + expect(block).toContain("BashHighlightedOutput") + }) + + it("BashHighlightedOutput uses shellscript grammar for commands without $ prefix", () => { + // The command should be highlighted as shellscript, but the $ prompt must + // NOT be inside the highlighted code (it breaks Shiki's parse context) + expect(src).toMatch(/data-lang="shellscript">\$\{escapeHtml\(cmd\)\}/) + expect(src).not.toMatch(/data-lang="shellscript">\$\s/) + }) + + it("BashHighlightedOutput uses log grammar for output", () => { + expect(src).toMatch(/data-lang="log"/) + }) + + it("BashHighlightedOutput renders section labels matching MCP tool pattern", () => { + // Must use the same data-slot as MCP tools for consistent styling + expect(src).toMatch(/data-slot="mcp-section-label".*shell\.command/) + expect(src).toMatch(/data-slot="mcp-section-label".*shell\.output/) + }) + + it("BashHighlightedOutput has edge-to-edge divider between sections", () => { + expect(src).toContain('data-slot="bash-divider"') + }) + + it("BashHighlightedOutput supports openContent for opening output in editor", () => { + expect(src).toContain("data.openContent") + expect(src).toContain("openInEditor") + }) + + it("BashHighlightedOutput opens full output file when truncated", () => { + // When the CLI truncates output, metadata.outputPath holds the full file. + // openInEditor should prefer openFile(outputPath) over openContent. + expect(src).toContain("props.outputPath") + expect(src).toMatch(/props\.outputPath.*data\.openFile/) + }) + + it("bash tool passes outputPath from metadata to BashHighlightedOutput", () => { + expect(block).toContain("props.metadata.outputPath") + }) +}) + describe("BasicTool export contract (runtime)", () => { it("BasicTool and GenericTool are exported from basic-tool", () => { const result = check(` diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index 70e596921ad..9843ad7d7d9 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -144,6 +144,10 @@ export const DataBridge: Component<{ children: any }> = (props) => { vscode.postMessage({ type: "openExternal", url }) } + const openContent = (content: string, language?: string) => { + vscode.postMessage({ type: "openContent", content, language }) + } + const directory = () => { const dir = server.workspaceDirectory() if (!dir) return "" @@ -161,6 +165,7 @@ export const DataBridge: Component<{ children: any }> = (props) => { onOpenFile={open} onOpenDiff={openDiff} onOpenUrl={openUrl} + onOpenContent={openContent} > {props.children} diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts index 517c2a79593..51c37652c15 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts @@ -131,6 +131,12 @@ export interface OpenFileRequest { column?: number } +export interface OpenContentRequest { + type: "openContent" + content: string + language?: string +} + export interface CancelLoginRequest { type: "cancelLogin" } @@ -1198,6 +1204,7 @@ export type WebviewMessage = | ToggleSectionCollapsedRequest | MoveToSectionRequest | MoveSectionRequest + | OpenContentRequest | AgentManagerTerminalCreateRequest | AgentManagerTerminalCloseRequest | AgentManagerTerminalResizeRequest diff --git a/packages/ui/src/components/basic-tool.tsx b/packages/ui/src/components/basic-tool.tsx index e1527387f9c..fc09a91294e 100644 --- a/packages/ui/src/components/basic-tool.tsx +++ b/packages/ui/src/components/basic-tool.tsx @@ -35,6 +35,7 @@ export interface BasicToolProps { animated?: boolean allowPendingToggle?: boolean // kilocode_change onSubtitleClick?: () => void + onOpenChange?: (open: boolean) => void // kilocode_change onTriggerClick?: JSX.EventHandlerUnion triggerHref?: string clickable?: boolean @@ -124,6 +125,7 @@ export function BasicTool(props: BasicToolProps) { if (props.hideDetails) return // kilocode_change if (props.locked && !value) return setState("open", value) + props.onOpenChange?.(value) // kilocode_change } const trigger = () => ( diff --git a/packages/ui/src/context/data.tsx b/packages/ui/src/context/data.tsx index f6c46c0f1ba..c5d913c2ee6 100644 --- a/packages/ui/src/context/data.tsx +++ b/packages/ui/src/context/data.tsx @@ -43,6 +43,8 @@ export type OpenDiffFn = (diff: { }) => void export type OpenUrlFn = (url: string) => void + +export type OpenContentFn = (content: string, language?: string) => void // kilocode_change // kilocode_change end export const { use: useData, provider: DataProvider } = createSimpleContext({ @@ -55,6 +57,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ onOpenFile?: OpenFileFn // kilocode_change onOpenDiff?: OpenDiffFn // kilocode_change onOpenUrl?: OpenUrlFn // kilocode_change + onOpenContent?: OpenContentFn // kilocode_change }) => { return { get store() { @@ -68,6 +71,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ openFile: props.onOpenFile, // kilocode_change openDiff: props.onOpenDiff, // kilocode_change openUrl: props.onOpenUrl, // kilocode_change + openContent: props.onOpenContent, // kilocode_change } }, })