/** * Bash Parser view — a playground for the App-scope `IBashParserService` * (the `bashParser` domain, a thin adapter over `@moonshot-ai/tree-sitter-bash`). * * left: the bash source textarea plus the parse budget (timeoutMs / * maxNodes, empty = package default); the `examples…` dropdown * fills the textarea with curated snippets from the parser's own * differential fixtures; * right: the parse result — status badges (hasError / aborted / node * count) and the syntax tree, one row per node with its type, * UTF-16 range and (for leaves) the source text. Anonymous tokens * are dimmed; rows expand/collapse. * * Parsing is debounced off the textarea and rides the same `/api/v1/debug` * channel as every other panel (`klient.core(IBashParserService).parse`) — * the budgeted parse never throws, `{ ok: false }` means budget exhaustion. */ import { useEffect, useState } from 'react'; import { IBashParserService, type BashParseResult, type BashSyntaxNode, } from '@moonshot-ai/agent-core-v2/app/bashParser/bashParser'; import { useConnection } from '../connection'; import { Badge, errorMessage } from '../ui'; const DEFAULT_SOURCE = `if [ -f config.sh ]; then source config.sh && echo "loaded" | tee -a setup.log else echo "missing" >&2; exit 1 fi `; const PARSE_DEBOUNCE_MS = 300; /** * Quick-fill examples, adapted from the parser's own differential fixtures * (`packages/tree-sitter-bash/test/fixtures/differential/*.txt`) — each one * exercises a distinct area of the grammar. The last three probe the * non-happy paths: deep nesting (a left-associative arithmetic chain, the * case that once overflowed the DTO conversion) and the error-recovery * paths that set `hasError`. */ const EXAMPLES: readonly { readonly name: string; readonly source: string }[] = [ { name: 'deep arithmetic (1000 operands)', // A thousand left-nested binary_expression levels. Deeper chains parse // fine in-process, but past ~2500 levels the JSON RPC transport itself // cannot serialize the tree (V8 call-stack limit in JSON.stringify). source: `echo $((${'1+'.repeat(1000)}1))`, }, { name: 'pipeline & redirects', source: `git log --oneline | head -20 | tee /tmp/log.txt find . -name '*.ts' -print0 2>/dev/null | xargs -0 grep -l TODO cmd <<< "$input" >out.txt 2>&1 `, }, { name: 'case statement', source: `case $x in a) echo A ;; b|c) echo BC ;& foo*|bar) echo match ;; [a-z]) echo lower ;; *) echo other ;; esac `, }, { name: 'heredoc', source: `foo() { cat < sum + countNodes(child), 0); } export function BashParserView() { const { klient } = useConnection(); const [source, setSource] = useState(DEFAULT_SOURCE); const [timeoutMs, setTimeoutMs] = useState(''); const [maxNodes, setMaxNodes] = useState(''); const [result, setResult] = useState(null); const [error, setError] = useState(null); useEffect(() => { const handle = setTimeout(() => { klient .core(IBashParserService) .parse(source, { timeoutMs: timeoutMs === '' ? undefined : Number(timeoutMs), maxNodes: maxNodes === '' ? undefined : Number(maxNodes), }) .then(setResult, (e: unknown) => { setResult(null); setError(errorMessage(e)); }); }, PARSE_DEBOUNCE_MS); return () => { clearTimeout(handle); }; }, [klient, source, timeoutMs, maxNodes]); const nodeCount = result !== null && result.ok ? countNodes(result.root) : null; return (
bash source