mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-10-29 18:52:45 +08:00
Merge pull request #10228 from Kilo-Org/supersede-shell-command-highlighting
feat(ui): add highlighted shell command output
This commit is contained in:
@@ -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.
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c3f37193389ad7937c7b2103ad2fe3dd63123a0d4f60a0fbfd65b31d3982c092
|
||||
size 12105
|
||||
oid sha256:dbaab3a1ff10eac8f710b0ed8ddc99d96188c06a09c227e634ca712bcee12130
|
||||
size 11543
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:74a42ec77a8ac8d5a1c555b46e3d550c9acf3198f45cc87da6582c0ff924d5f9
|
||||
size 17677
|
||||
oid sha256:7cabfff215cef485ec913b9a082aaac844275e506e88ba0fab112d95ff6de329
|
||||
size 18200
|
||||
|
||||
Generated
+4
@@ -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": "كان هذا مفيدًا",
|
||||
|
||||
Generated
+4
@@ -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",
|
||||
|
||||
Generated
+4
@@ -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",
|
||||
|
||||
Generated
+5
-1
@@ -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",
|
||||
|
||||
Generated
+4
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+4
@@ -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",
|
||||
|
||||
Generated
+5
-1
@@ -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",
|
||||
|
||||
Generated
+5
-1
@@ -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": "役に立ちました",
|
||||
|
||||
Generated
+5
-1
@@ -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": "도움이 됐어요",
|
||||
|
||||
Generated
+5
-1
@@ -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",
|
||||
|
||||
Generated
+5
-1
@@ -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",
|
||||
|
||||
Generated
+4
@@ -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",
|
||||
|
||||
Generated
+5
-1
@@ -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": "Это было полезно",
|
||||
|
||||
Generated
+4
@@ -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": "สิ่งนี้มีประโยชน์",
|
||||
|
||||
Generated
+5
-1
@@ -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",
|
||||
|
||||
Generated
+5
-1
@@ -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": "Це було корисно",
|
||||
|
||||
Generated
+4
@@ -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": "这有帮助",
|
||||
|
||||
Generated
+4
@@ -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": "這有幫助",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<Tooltip value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")} placement="bottom" gutter={4}>
|
||||
<IconButton
|
||||
icon={copied() ? "check" : "copy"}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onClick={handler}
|
||||
aria-label={props.label}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
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 = `<pre data-slot="bash-pre"><code data-lang="shellscript">${escapeHtml(cmd)}</code></pre>`
|
||||
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 = `<pre data-slot="bash-pre"><code data-lang="log">${escapeHtml(out)}</code></pre>`
|
||||
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 (
|
||||
<div data-component="bash-output">
|
||||
<Show when={props.cmd}>
|
||||
<div data-slot="mcp-section-label">{i18n.t("ui.messagePart.shell.command")}</div>
|
||||
<div data-slot="bash-section">
|
||||
<div data-slot="bash-section-code" ref={cmdRef} />
|
||||
<div data-slot="bash-section-actions">
|
||||
<BashCopyButton value={() => props.cmd} label={i18n.t("ui.message.copy")} />
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.cmd && props.output}>
|
||||
<div data-slot="bash-divider" />
|
||||
</Show>
|
||||
<Show when={props.output}>
|
||||
<div data-slot="mcp-section-label">{i18n.t("ui.messagePart.shell.output")}</div>
|
||||
<div data-slot="bash-section">
|
||||
<div data-slot="bash-section-code" data-scrollable ref={outRef} />
|
||||
<div data-slot="bash-section-actions">
|
||||
<Show when={data.openContent || (props.outputPath && data.openFile)}>
|
||||
<Tooltip value={i18n.t("ui.messagePart.openInEditor")} placement="bottom" gutter={4}>
|
||||
<IconButton
|
||||
icon="square-arrow-top-right"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onClick={openInEditor}
|
||||
aria-label={i18n.t("ui.messagePart.openInEditor")}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
<BashCopyButton value={() => props.output} label={i18n.t("ui.message.copy")} />
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<BasicTool
|
||||
@@ -2068,6 +2164,7 @@ ToolRegistry.register({
|
||||
icon="console"
|
||||
animated
|
||||
defaultOpen={props.defaultOpen ?? true}
|
||||
onOpenChange={setOpen}
|
||||
allowPendingToggle
|
||||
trigger={
|
||||
<div data-slot="basic-tool-tool-info-structured">
|
||||
@@ -2080,29 +2177,7 @@ ToolRegistry.register({
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div data-component="bash-output">
|
||||
<div data-slot="bash-copy">
|
||||
<Tooltip
|
||||
value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
|
||||
placement="top"
|
||||
gutter={4}
|
||||
>
|
||||
<IconButton
|
||||
icon={copied() ? "check" : "copy"}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={handleCopy}
|
||||
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div data-slot="bash-scroll" data-scrollable>
|
||||
<pre data-slot="bash-pre">
|
||||
<code>{text()}</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<BashHighlightedOutput cmd={cmd()} output={out()} outputPath={props.metadata.outputPath} active={open()} />
|
||||
</BasicTool>
|
||||
)
|
||||
},
|
||||
|
||||
@@ -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 = `<pre data-slot="shell-expanded-pre"><code data-lang="log">${escapeHtml(code)}</code></pre>`
|
||||
void deferredHighlight(ref, undefined, signal)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
state.signal.aborted = true
|
||||
})
|
||||
|
||||
return <div ref={ref} />
|
||||
}
|
||||
|
||||
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` }}
|
||||
>
|
||||
<pre data-slot="shell-expanded-pre">
|
||||
<code>{props.out}</code>
|
||||
</pre>
|
||||
<ShellOutputHighlight code={props.out} active={props.open} />
|
||||
</div>
|
||||
</>
|
||||
</Show>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function escapeHtml(s: string): string {
|
||||
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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(`
|
||||
|
||||
@@ -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}
|
||||
</DataProvider>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface BasicToolProps {
|
||||
animated?: boolean
|
||||
allowPendingToggle?: boolean // kilocode_change
|
||||
onSubtitleClick?: () => void
|
||||
onOpenChange?: (open: boolean) => void // kilocode_change
|
||||
onTriggerClick?: JSX.EventHandlerUnion<HTMLElement, MouseEvent>
|
||||
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 = () => (
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user