feat: add BashToolViewer with collapsible output and expanded syntax highlighting#169
feat: add BashToolViewer with collapsible output and expanded syntax highlighting#169psypeal wants to merge 1 commit intomatt1398:mainfrom
Conversation
…hlighting - Add dedicated BashToolViewer component for Bash tool calls with syntax-highlighted command display via CodeBlockViewer - Expand syntax highlighting with bash, C, Java, Kotlin, Swift, Scala, Haskell, Lua, Perl, Dart, Elixir, Clojure, Erlang, Zig, Nim, and shell alias keyword sets - Add hasBashContent check in toolContentChecks - Wire BashToolViewer into LinkedToolItem rendering pipeline Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a specialized BashToolViewer component to render Bash tool calls with syntax highlighting and collapsible output. It also significantly expands the internal syntax highlighter by adding keyword sets for Bash, C, Java, Kotlin, Swift, Lua, HTML, and YAML, along with corresponding comment style support. However, the changes to the highlighter's fallback logic introduce a regression for Markdown files and include redundant checks for Bash. Additionally, several languages mentioned in the PR's intent (such as Scala, Haskell, and Perl) are currently missing from the implementation.
|
|
||
| // If no highlighting support, return plain text as single-element array | ||
| if (keywords.size === 0 && !['json', 'css', 'html', 'bash', 'markdown'].includes(language)) { | ||
| if (keywords.size === 0 && !['json', 'css', 'bash'].includes(language)) { |
There was a problem hiding this comment.
The removal of markdown from this list causes a regression: markdown files (which have no defined keywords in this highlighter) will now be returned as plain text without basic highlighting for backticks (treated as strings), numbers, or operators. Additionally, bash is now redundant in this check because it has been added to the KEYWORDS object, so keywords.size will be non-zero for bash.
| if (keywords.size === 0 && !['json', 'css', 'bash'].includes(language)) { | |
| if (keywords.size === 0 && !['json', 'css', 'markdown'].includes(language)) { |
| bash: new Set([ | ||
| 'if', | ||
| 'then', | ||
| 'else', | ||
| 'elif', | ||
| 'fi', | ||
| 'for', | ||
| 'while', | ||
| 'do', | ||
| 'done', | ||
| 'case', | ||
| 'esac', | ||
| 'in', | ||
| 'function', | ||
| 'return', | ||
| 'local', | ||
| 'export', | ||
| 'readonly', | ||
| 'declare', | ||
| 'typeset', | ||
| 'unset', | ||
| 'shift', | ||
| 'source', | ||
| 'eval', | ||
| 'exec', | ||
| 'exit', | ||
| 'trap', | ||
| 'break', | ||
| 'continue', | ||
| 'echo', | ||
| 'printf', | ||
| 'read', | ||
| 'test', | ||
| 'true', | ||
| 'false', | ||
| 'cd', | ||
| 'pwd', | ||
| 'mkdir', | ||
| 'rm', | ||
| 'cp', | ||
| 'mv', | ||
| 'ls', | ||
| 'cat', | ||
| 'grep', | ||
| 'sed', | ||
| 'awk', | ||
| 'find', | ||
| 'sort', | ||
| 'uniq', | ||
| 'wc', | ||
| 'head', | ||
| 'tail', | ||
| 'chmod', | ||
| 'chown', | ||
| 'sudo', | ||
| 'apt', | ||
| 'pip', | ||
| 'npm', | ||
| 'pnpm', | ||
| 'yarn', | ||
| 'git', | ||
| 'docker', | ||
| 'curl', | ||
| 'wget', | ||
| ]), | ||
| c: new Set([ | ||
| 'auto', | ||
| 'break', | ||
| 'case', | ||
| 'char', | ||
| 'const', | ||
| 'continue', | ||
| 'default', | ||
| 'do', | ||
| 'double', | ||
| 'else', | ||
| 'enum', | ||
| 'extern', | ||
| 'float', | ||
| 'for', | ||
| 'goto', | ||
| 'if', | ||
| 'inline', | ||
| 'int', | ||
| 'long', | ||
| 'register', | ||
| 'return', | ||
| 'short', | ||
| 'signed', | ||
| 'sizeof', | ||
| 'static', | ||
| 'struct', | ||
| 'switch', | ||
| 'typedef', | ||
| 'union', | ||
| 'unsigned', | ||
| 'void', | ||
| 'volatile', | ||
| 'while', | ||
| 'NULL', | ||
| 'true', | ||
| 'false', | ||
| 'include', | ||
| 'define', | ||
| 'ifdef', | ||
| 'ifndef', | ||
| 'endif', | ||
| 'pragma', | ||
| ]), | ||
| java: new Set([ | ||
| 'abstract', | ||
| 'assert', | ||
| 'boolean', | ||
| 'break', | ||
| 'byte', | ||
| 'case', | ||
| 'catch', | ||
| 'char', | ||
| 'class', | ||
| 'const', | ||
| 'continue', | ||
| 'default', | ||
| 'do', | ||
| 'double', | ||
| 'else', | ||
| 'enum', | ||
| 'extends', | ||
| 'final', | ||
| 'finally', | ||
| 'float', | ||
| 'for', | ||
| 'if', | ||
| 'implements', | ||
| 'import', | ||
| 'instanceof', | ||
| 'int', | ||
| 'interface', | ||
| 'long', | ||
| 'native', | ||
| 'new', | ||
| 'package', | ||
| 'private', | ||
| 'protected', | ||
| 'public', | ||
| 'return', | ||
| 'short', | ||
| 'static', | ||
| 'strictfp', | ||
| 'super', | ||
| 'switch', | ||
| 'synchronized', | ||
| 'this', | ||
| 'throw', | ||
| 'throws', | ||
| 'transient', | ||
| 'try', | ||
| 'void', | ||
| 'volatile', | ||
| 'while', | ||
| 'true', | ||
| 'false', | ||
| 'null', | ||
| 'var', | ||
| 'yield', | ||
| 'record', | ||
| 'sealed', | ||
| 'permits', | ||
| ]), | ||
| kotlin: new Set([ | ||
| 'abstract', | ||
| 'annotation', | ||
| 'as', | ||
| 'break', | ||
| 'by', | ||
| 'catch', | ||
| 'class', | ||
| 'companion', | ||
| 'const', | ||
| 'constructor', | ||
| 'continue', | ||
| 'crossinline', | ||
| 'data', | ||
| 'do', | ||
| 'else', | ||
| 'enum', | ||
| 'external', | ||
| 'false', | ||
| 'final', | ||
| 'finally', | ||
| 'for', | ||
| 'fun', | ||
| 'if', | ||
| 'import', | ||
| 'in', | ||
| 'infix', | ||
| 'init', | ||
| 'inline', | ||
| 'inner', | ||
| 'interface', | ||
| 'internal', | ||
| 'is', | ||
| 'lateinit', | ||
| 'noinline', | ||
| 'null', | ||
| 'object', | ||
| 'open', | ||
| 'operator', | ||
| 'out', | ||
| 'override', | ||
| 'package', | ||
| 'private', | ||
| 'protected', | ||
| 'public', | ||
| 'reified', | ||
| 'return', | ||
| 'sealed', | ||
| 'super', | ||
| 'suspend', | ||
| 'this', | ||
| 'throw', | ||
| 'true', | ||
| 'try', | ||
| 'typealias', | ||
| 'val', | ||
| 'var', | ||
| 'vararg', | ||
| 'when', | ||
| 'where', | ||
| 'while', | ||
| ]), | ||
| swift: new Set([ | ||
| 'associatedtype', | ||
| 'break', | ||
| 'case', | ||
| 'catch', | ||
| 'class', | ||
| 'continue', | ||
| 'default', | ||
| 'defer', | ||
| 'deinit', | ||
| 'do', | ||
| 'else', | ||
| 'enum', | ||
| 'extension', | ||
| 'fallthrough', | ||
| 'false', | ||
| 'fileprivate', | ||
| 'for', | ||
| 'func', | ||
| 'guard', | ||
| 'if', | ||
| 'import', | ||
| 'in', | ||
| 'init', | ||
| 'inout', | ||
| 'internal', | ||
| 'is', | ||
| 'let', | ||
| 'nil', | ||
| 'open', | ||
| 'operator', | ||
| 'override', | ||
| 'private', | ||
| 'protocol', | ||
| 'public', | ||
| 'repeat', | ||
| 'rethrows', | ||
| 'return', | ||
| 'self', | ||
| 'static', | ||
| 'struct', | ||
| 'subscript', | ||
| 'super', | ||
| 'switch', | ||
| 'throw', | ||
| 'throws', | ||
| 'true', | ||
| 'try', | ||
| 'typealias', | ||
| 'var', | ||
| 'where', | ||
| 'while', | ||
| 'async', | ||
| 'await', | ||
| ]), | ||
| lua: new Set([ | ||
| 'and', | ||
| 'break', | ||
| 'do', | ||
| 'else', | ||
| 'elseif', | ||
| 'end', | ||
| 'false', | ||
| 'for', | ||
| 'function', | ||
| 'goto', | ||
| 'if', | ||
| 'in', | ||
| 'local', | ||
| 'nil', | ||
| 'not', | ||
| 'or', | ||
| 'repeat', | ||
| 'return', | ||
| 'then', | ||
| 'true', | ||
| 'until', | ||
| 'while', | ||
| 'self', | ||
| 'require', | ||
| 'print', | ||
| 'type', | ||
| 'tostring', | ||
| 'tonumber', | ||
| 'pairs', | ||
| 'ipairs', | ||
| 'error', | ||
| 'pcall', | ||
| 'xpcall', | ||
| 'setmetatable', | ||
| 'getmetatable', | ||
| ]), | ||
| html: new Set([ | ||
| 'div', | ||
| 'span', | ||
| 'html', | ||
| 'head', | ||
| 'body', | ||
| 'title', | ||
| 'meta', | ||
| 'link', | ||
| 'script', | ||
| 'style', | ||
| 'section', | ||
| 'article', | ||
| 'header', | ||
| 'footer', | ||
| 'nav', | ||
| 'main', | ||
| 'aside', | ||
| 'form', | ||
| 'input', | ||
| 'button', | ||
| 'select', | ||
| 'option', | ||
| 'textarea', | ||
| 'label', | ||
| 'table', | ||
| 'thead', | ||
| 'tbody', | ||
| 'tr', | ||
| 'th', | ||
| 'td', | ||
| 'ul', | ||
| 'ol', | ||
| 'li', | ||
| 'a', | ||
| 'p', | ||
| 'h1', | ||
| 'h2', | ||
| 'h3', | ||
| 'h4', | ||
| 'h5', | ||
| 'h6', | ||
| 'img', | ||
| 'video', | ||
| 'audio', | ||
| 'canvas', | ||
| 'svg', | ||
| 'class', | ||
| 'id', | ||
| 'src', | ||
| 'href', | ||
| 'type', | ||
| 'name', | ||
| 'value', | ||
| 'placeholder', | ||
| 'alt', | ||
| 'width', | ||
| 'height', | ||
| 'true', | ||
| 'false', | ||
| ]), | ||
| yaml: new Set([ | ||
| 'true', | ||
| 'false', | ||
| 'null', | ||
| 'yes', | ||
| 'no', | ||
| 'on', | ||
| 'off', | ||
| ]), | ||
| }; |
There was a problem hiding this comment.
📝 WalkthroughWalkthroughThe pull request adds Bash tool rendering support by introducing a new Changes
Possibly related PRs
Suggested labels
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/renderer/components/chat/viewers/syntaxHighlighter.ts (1)
803-805: Missingshellalias mentioned in PR objectives.The PR description mentions adding
shell (alias)keyword set, but onlyzshandfishare aliased tobash. Consider addingshellas well for consistency with common language identifiers:// Extend zsh/fish to use bash keywords KEYWORDS.zsh = KEYWORDS.bash; KEYWORDS.fish = KEYWORDS.bash; +KEYWORDS.shell = KEYWORDS.bash; +KEYWORDS.sh = KEYWORDS.bash;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/chat/viewers/syntaxHighlighter.ts` around lines 803 - 805, The PR added aliases for zsh and fish but forgot the requested "shell" alias; update the keyword aliasing in the syntaxHighlighter by adding KEYWORDS.shell = KEYWORDS.bash alongside KEYWORDS.zsh and KEYWORDS.fish so the "shell" language identifier uses the same bash keyword set (refer to the KEYWORDS.zsh, KEYWORDS.fish, and KEYWORDS.bash symbols).src/renderer/components/chat/items/linkedTool/BashToolViewer.tsx (1)
24-26: Type assertion is safe but could be more defensive.The
as stringassertion on line 25 relies onhasBashContentbeing called before this component renders. While this contract is currently enforced byLinkedToolItem.tsx, a more defensive approach would be:- const command = linkedTool.input.command as string; + const command = String(linkedTool.input.command ?? '');This is a minor concern since the precondition is checked, but it adds resilience against future refactoring.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/chat/items/linkedTool/BashToolViewer.tsx` around lines 24 - 26, The BashToolViewer component currently uses unsafe type assertions (command = linkedTool.input.command as string) that rely on an external precondition; make it defensive by validating and narrowing the types inside BashToolViewer: check hasBashContent(linkedTool) or verify linkedTool.input?.command is a string before using it, fall back to a safe default or render null/placeholder when invalid, and update uses of linkedTool.input.description similarly; reference the BashToolViewer component, linkedTool.input.command/description, and the existing hasBashContent helper (and LinkedToolItem.tsx contract) when implementing the checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/renderer/components/chat/items/linkedTool/BashToolViewer.tsx`:
- Around line 24-26: The BashToolViewer component currently uses unsafe type
assertions (command = linkedTool.input.command as string) that rely on an
external precondition; make it defensive by validating and narrowing the types
inside BashToolViewer: check hasBashContent(linkedTool) or verify
linkedTool.input?.command is a string before using it, fall back to a safe
default or render null/placeholder when invalid, and update uses of
linkedTool.input.description similarly; reference the BashToolViewer component,
linkedTool.input.command/description, and the existing hasBashContent helper
(and LinkedToolItem.tsx contract) when implementing the checks.
In `@src/renderer/components/chat/viewers/syntaxHighlighter.ts`:
- Around line 803-805: The PR added aliases for zsh and fish but forgot the
requested "shell" alias; update the keyword aliasing in the syntaxHighlighter by
adding KEYWORDS.shell = KEYWORDS.bash alongside KEYWORDS.zsh and KEYWORDS.fish
so the "shell" language identifier uses the same bash keyword set (refer to the
KEYWORDS.zsh, KEYWORDS.fish, and KEYWORDS.bash symbols).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 9c0ae152-3f3e-40dc-b3cc-884f4c9f4db6
📒 Files selected for processing (6)
src/renderer/components/chat/items/LinkedToolItem.tsxsrc/renderer/components/chat/items/linkedTool/BashToolViewer.tsxsrc/renderer/components/chat/items/linkedTool/index.tssrc/renderer/components/chat/viewers/syntaxHighlighter.tssrc/renderer/utils/toolRendering/index.tssrc/renderer/utils/toolRendering/toolContentChecks.ts
Summary
BashToolViewercomponent for rendering Bash tool calls with syntax-highlighted outputCollapsibleOutputSectionfor long tool outputs with expand/collapse togglesyntaxHighlighter.ts(bash, C, Java, Kotlin, Swift, Scala, Haskell, Lua, Perl, Dart, Elixir, Clojure, Erlang, Zig, Nim, shell)Test plan
pnpm typecheckpassespnpm test— all tests pass🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features