opentui(v6): suppress redundant JSON/diff-echo output under rendered diffs
A patch tool's result is a JSON record whose payload IS the diff. In a verbose
session the gateway redacts + TAIL-caps result_text (_cap_tui_verbose_text),
so the echo arrived under the native diff in two broken shapes: truncated
mid-JSON (unparseable, so the old JSON.parse check failed open), or — for tall
edits — capped PAST the JSON head, which the store's normalizeOutput then
un-escapes into plain lines that duplicate the diff. North star: no raw JSON
in the transcript, ever.
Three layers:
- gateway: when diff_unified ships, result_text drops the in-JSON diff echo
(_result_sans_diff_echo) — small, parseable, carries only the non-diff
signal (success/files_modified/warnings/lsp_diagnostics).
- fileTool diffOutputPlan: anything starting with '{' under a rendered diff is
suppressed regardless of parseability; parseable JSON with real non-diff
signal (error/warning/lsp_diagnostics) renders JUST those as labeled notes;
a non-JSON fragment whose lines echo the rendered diff is suppressed too
(guards older emitters). Plain-text results (lint tails) still render.
This commit is contained in:
@@ -241,6 +241,40 @@ def test_tool_complete_emits_full_unified_diff(monkeypatch):
|
|||||||
assert "inline_diff" in payload
|
assert "inline_diff" in payload
|
||||||
|
|
||||||
|
|
||||||
|
def test_verbose_result_text_drops_diff_echo_when_diff_unified_ships(monkeypatch):
|
||||||
|
# A tall edit's result JSON embeds the WHOLE diff; tail-capping that echo
|
||||||
|
# yields an unparseable JSON-looking fragment the TUI can't suppress
|
||||||
|
# reliably. When diff_unified ships, result_text must carry only the
|
||||||
|
# non-diff signal — small, parseable, never the diff echo.
|
||||||
|
events: list[tuple[str, str, dict]] = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
server, "_emit", lambda event_type, sid, payload: events.append((event_type, sid, payload))
|
||||||
|
)
|
||||||
|
monkeypatch.setitem(
|
||||||
|
server._sessions,
|
||||||
|
"diff-echo-test",
|
||||||
|
{"tool_progress_mode": "verbose", "tool_started_at": {}, "edit_snapshots": {}},
|
||||||
|
)
|
||||||
|
|
||||||
|
lines = "\n".join(f"+def fn_{i}() -> int: return {i}" for i in range(60))
|
||||||
|
diff = f"--- a/x.py\n+++ b/x.py\n@@ -1,0 +1,60 @@\n{lines}\n"
|
||||||
|
result = json.dumps(
|
||||||
|
{"success": True, "diff": diff, "files_modified": ["x.py"], "_warning": "stale read"}
|
||||||
|
)
|
||||||
|
server._on_tool_complete("diff-echo-test", "tool-1", "patch", {"mode": "patch"}, result)
|
||||||
|
|
||||||
|
payload = events[0][2]
|
||||||
|
assert payload["diff_unified"] == diff
|
||||||
|
text = payload["result_text"]
|
||||||
|
assert "[showing verbose tail" not in text # small enough to dodge the cap
|
||||||
|
parsed = json.loads(text) # parseable …
|
||||||
|
assert "diff" not in parsed # … with the echo gone
|
||||||
|
assert parsed["_warning"] == "stale read" # non-diff signal survives
|
||||||
|
# without diff_unified (non-edit tools) the result_text is untouched
|
||||||
|
assert server._result_sans_diff_echo("plain text result") == "plain text result"
|
||||||
|
assert server._result_sans_diff_echo('{"output": "x"}') == '{"output": "x"}'
|
||||||
|
|
||||||
|
|
||||||
def test_cap_diff_unified_truncates_at_line_boundary():
|
def test_cap_diff_unified_truncates_at_line_boundary():
|
||||||
line = "+" + "x" * 63 # 64 bytes per line incl. newline
|
line = "+" + "x" * 63 # 64 bytes per line incl. newline
|
||||||
diff = "\n".join([line] * 100)
|
diff = "\n".join([line] * 100)
|
||||||
|
|||||||
+34
-4
@@ -1972,6 +1972,31 @@ def _cap_diff_unified(diff: str, max_bytes: int = _DIFF_UNIFIED_MAX_BYTES) -> st
|
|||||||
return f"{head}\n# … diff truncated ({omitted} more bytes)"
|
return f"{head}\n# … diff truncated ({omitted} more bytes)"
|
||||||
|
|
||||||
|
|
||||||
|
def _result_sans_diff_echo(result: str) -> str:
|
||||||
|
"""The file-edit result JSON minus its `diff` echo.
|
||||||
|
|
||||||
|
Used for verbose `result_text` when the FULL diff already ships as
|
||||||
|
`diff_unified`: a multi-KB diff echo inside the result JSON gets
|
||||||
|
tail-capped by `_cap_tui_verbose_text` into an unparseable JSON-looking
|
||||||
|
fragment that a client can neither render nor reliably suppress. The
|
||||||
|
native renderer shows the real diff, so result_text should carry only the
|
||||||
|
non-diff signal (success/files_modified/warnings/lsp_diagnostics).
|
||||||
|
Returns `result` unchanged when it isn't a JSON object with a `diff` key.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = json.loads(result)
|
||||||
|
except Exception:
|
||||||
|
return result
|
||||||
|
if not isinstance(data, dict) or "diff" not in data:
|
||||||
|
return result
|
||||||
|
try:
|
||||||
|
return json.dumps(
|
||||||
|
{k: v for k, v in data.items() if k != "diff"}, ensure_ascii=False
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _redact_tui_verbose_text(text: str) -> str:
|
def _redact_tui_verbose_text(text: str) -> str:
|
||||||
try:
|
try:
|
||||||
from agent.redact import redact_sensitive_text
|
from agent.redact import redact_sensitive_text
|
||||||
@@ -2093,10 +2118,6 @@ def _on_tool_complete(sid: str, tool_call_id: str, name: str, args: dict, result
|
|||||||
summary = _tool_summary(name, result, duration_s)
|
summary = _tool_summary(name, result, duration_s)
|
||||||
if summary:
|
if summary:
|
||||||
payload["summary"] = summary
|
payload["summary"] = summary
|
||||||
if _session_verbose(sid):
|
|
||||||
result_text = _tool_result_text(result)
|
|
||||||
if result_text:
|
|
||||||
payload["result_text"] = result_text
|
|
||||||
if name == "todo":
|
if name == "todo":
|
||||||
try:
|
try:
|
||||||
data = json.loads(result)
|
data = json.loads(result)
|
||||||
@@ -2134,6 +2155,15 @@ def _on_tool_complete(sid: str, tool_call_id: str, name: str, args: dict, result
|
|||||||
payload["diff_unified"] = _cap_diff_unified(diff_unified)
|
payload["diff_unified"] = _cap_diff_unified(diff_unified)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
if _session_verbose(sid):
|
||||||
|
# Computed AFTER diff_unified: when the full diff ships natively, the
|
||||||
|
# result_text drops the in-JSON diff echo (it would tail-cap into
|
||||||
|
# unparseable JSON-looking noise under the client's rendered diff).
|
||||||
|
result_text = _tool_result_text(
|
||||||
|
_result_sans_diff_echo(result) if payload.get("diff_unified") else result
|
||||||
|
)
|
||||||
|
if result_text:
|
||||||
|
payload["result_text"] = result_text
|
||||||
if _tool_progress_enabled(sid) or payload.get("inline_diff") or payload.get("diff_unified"):
|
if _tool_progress_enabled(sid) or payload.get("inline_diff") or payload.get("diff_unified"):
|
||||||
_emit("tool.complete", sid, payload)
|
_emit("tool.complete", sid, payload)
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { createSessionStore, type ToolPartState } from '../logic/store.ts'
|
|||||||
import { App } from '../view/App.tsx'
|
import { App } from '../view/App.tsx'
|
||||||
import { ThemeProvider } from '../view/theme.tsx'
|
import { ThemeProvider } from '../view/theme.tsx'
|
||||||
import { BashToolBody, commandOf } from '../view/tools/bashTool.tsx'
|
import { BashToolBody, commandOf } from '../view/tools/bashTool.tsx'
|
||||||
|
import { diffOutputPlan, FileToolBody } from '../view/tools/fileTool.tsx'
|
||||||
import { renderProbe, type RenderProbe } from './lib/render.ts'
|
import { renderProbe, type RenderProbe } from './lib/render.ts'
|
||||||
|
|
||||||
type Store = ReturnType<typeof createSessionStore>
|
type Store = ReturnType<typeof createSessionStore>
|
||||||
@@ -320,6 +321,103 @@ describe('file tool renderer — relative path + diff stats (Epic 2.3)', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('file tool — output suppression under a rendered diff (no raw JSON, ever)', () => {
|
||||||
|
// A file-edit result is a JSON record whose payload IS the diff. In a verbose
|
||||||
|
// session the gateway REDACTS + CAPS result_text, so it can arrive truncated
|
||||||
|
// mid-JSON (unparseable) — that JSON-looking blob must never render below the
|
||||||
|
// native diff. Plain-text results (lint tails etc.) must still render.
|
||||||
|
const DIFF = ['--- a/x.py', '+++ b/x.py', '@@ -1,2 +1,2 @@', ' ctx', '-old', '+new'].join('\n')
|
||||||
|
|
||||||
|
const part = (resultText: string): ToolPartState => ({
|
||||||
|
type: 'tool',
|
||||||
|
id: 'fp1',
|
||||||
|
name: 'patch',
|
||||||
|
state: 'complete',
|
||||||
|
args: { path: '/p/x.py' },
|
||||||
|
resultText,
|
||||||
|
diffUnified: DIFF,
|
||||||
|
diffStats: { added: 1, removed: 1 }
|
||||||
|
})
|
||||||
|
|
||||||
|
test('diffOutputPlan: truncated/unparseable JSON is suppressed; plain text renders; JSON warnings surface', () => {
|
||||||
|
// gateway-capped mid-JSON (unparseable, still contains "diff") → suppress
|
||||||
|
const capped = '{"success": true, "diff": "--- a/x.py\\n+++ b/x.py\\n@@ -1,2 +1'
|
||||||
|
expect(diffOutputPlan(part(capped))).toEqual({ kind: 'suppress' })
|
||||||
|
// intact JSON echo of the diff → suppress
|
||||||
|
expect(diffOutputPlan(part(JSON.stringify({ success: true, diff: DIFF })))).toEqual({ kind: 'suppress' })
|
||||||
|
// plain text (lint tail) → full output block
|
||||||
|
expect(diffOutputPlan(part('warning: trailing whitespace on line 3'))).toEqual({ kind: 'output' })
|
||||||
|
// parseable JSON carrying real non-diff signal → just the notes
|
||||||
|
expect(diffOutputPlan(part(JSON.stringify({ success: true, diff: DIFF, warning: 'mode fallback' })))).toEqual({
|
||||||
|
kind: 'notes',
|
||||||
|
notes: [['warning', 'mode fallback']]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('diffOutputPlan: tail-capped echo that LOST the JSON head (normalized to diff lines) is suppressed', () => {
|
||||||
|
// A long file-edit JSON tail-capped past its `{"success"…` head: the store
|
||||||
|
// un-escapes the literal \n so it arrives as plain lines that ARE diff
|
||||||
|
// lines (first/last cut mid-line) — live bug shape from the v6 smoke.
|
||||||
|
const tallDiff = [
|
||||||
|
'--- a/x.py',
|
||||||
|
'+++ b/x.py',
|
||||||
|
'@@ -1,1 +1,9 @@',
|
||||||
|
' ctx',
|
||||||
|
...Array.from({ length: 8 }, (_, i) => `+def fn_${i}() -> int: return ${i}`)
|
||||||
|
].join('\n')
|
||||||
|
const echoTail = [
|
||||||
|
'n 1', // cut mid-line
|
||||||
|
...Array.from({ length: 6 }, (_, i) => `+def fn_${i + 2}() -> int: return ${i + 2}`),
|
||||||
|
'", "files_modified": ["/p/x.py' // cut mid-JSON
|
||||||
|
].join('\n')
|
||||||
|
expect(diffOutputPlan({ ...part(echoTail), diffUnified: tallDiff })).toEqual({ kind: 'suppress' })
|
||||||
|
// …but a genuine plain-text tail sharing no lines with the diff still renders
|
||||||
|
const lintTail = ['x.py:3: W291 trailing whitespace', 'x.py:9: E302 expected 2 blank lines', '2 warnings'].join(
|
||||||
|
'\n'
|
||||||
|
)
|
||||||
|
expect(diffOutputPlan({ ...part(lintTail), diffUnified: tallDiff })).toEqual({ kind: 'output' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('TRUNCATED JSON result under a rendered diff → NO output block in the frame', async () => {
|
||||||
|
const capped = '{"success": true, "diff": "--- a/x.py\\n+++ b/x.py\\n@@ -1,2 +1'
|
||||||
|
const probe = await renderProbe(
|
||||||
|
() => (
|
||||||
|
<ThemeProvider>
|
||||||
|
<FileToolBody part={part(capped)} width={70} />
|
||||||
|
</ThemeProvider>
|
||||||
|
),
|
||||||
|
{ width: 80, height: 16 }
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
// wait for the native <diff> to paint (Tree-sitter settles async)
|
||||||
|
const frame = await probe.waitForFrame(f => f.includes('new'))
|
||||||
|
expect(frame).not.toContain('output') // no output section label
|
||||||
|
expect(frame).not.toContain('{"') // and never raw JSON
|
||||||
|
expect(frame).not.toContain('success')
|
||||||
|
} finally {
|
||||||
|
probe.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('plain-text result under a rendered diff → output block still shown', async () => {
|
||||||
|
const probe = await renderProbe(
|
||||||
|
() => (
|
||||||
|
<ThemeProvider>
|
||||||
|
<FileToolBody part={part('warning: trailing whitespace on line 3')} width={70} />
|
||||||
|
</ThemeProvider>
|
||||||
|
),
|
||||||
|
{ width: 80, height: 16 }
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
const frame = await probe.waitForFrame(f => f.includes('trailing whitespace'))
|
||||||
|
expect(frame).toContain('output') // labeled output section
|
||||||
|
expect(frame).toContain('warning: trailing whitespace on line 3')
|
||||||
|
} finally {
|
||||||
|
probe.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('redaction precedence — gateway args_text wins over raw args (security)', () => {
|
describe('redaction precedence — gateway args_text wins over raw args (security)', () => {
|
||||||
// The gateway redacts verbose `args_text` (server.py _tool_args_text) but
|
// The gateway redacts verbose `args_text` (server.py _tool_args_text) but
|
||||||
// sends the raw `args` dict on tool.complete UNREDACTED. structuredArgs must
|
// sends the raw `args` dict on tool.complete UNREDACTED. structuredArgs must
|
||||||
|
|||||||
@@ -37,22 +37,81 @@ export function filePathOf(part: ToolPartState): string {
|
|||||||
return part.argsPreview ?? ''
|
return part.argsPreview ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Non-diff result keys worth surfacing under a rendered diff (file_operations.py
|
||||||
|
* Write/PatchResult.to_dict + the file_tools.py `_warning` injection). */
|
||||||
|
const INTERESTING_KEYS = ['error', 'warning', '_warning', 'warnings', 'lsp_diagnostics'] as const
|
||||||
|
|
||||||
|
/** What to render below an ALREADY-RENDERED diff for the settled output. */
|
||||||
|
export type DiffOutputPlan =
|
||||||
|
| { kind: 'suppress' } // JSON echo of the diff (or empty) — the diff tells the story
|
||||||
|
| { kind: 'notes'; notes: Array<[string, string]> } // interesting non-diff keys only
|
||||||
|
| { kind: 'output' } // plain text (lint tails, etc.) — full output block
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the settled output adds nothing over the rendered diff: file-edit
|
* True when a NON-JSON-looking result is really an echo FRAGMENT of the
|
||||||
* results are JSON records whose payload IS the diff (`patch` returns
|
* already-rendered diff. A gateway TAIL-cap (`_cap_tui_verbose_text`) on a
|
||||||
* `{success, diff, …}`) — re-printing that below the native diff is noise.
|
* file-edit JSON result can cut off the `{"success"…` head entirely, and the
|
||||||
* Plain-text results (lint warnings, errors, capped tails) still show.
|
* store's `normalizeOutput` then turns the surviving literal `\n` escapes into
|
||||||
|
* real lines — so the fragment arrives looking like plain text whose lines ARE
|
||||||
|
* lines of the diff. Suppress when most inner lines (first/last are typically
|
||||||
|
* cut mid-line) appear verbatim in the rendered diff. Genuine plain-text
|
||||||
|
* results (lint tails etc.) share no lines with the diff and still render.
|
||||||
|
* (Current gateways strip the diff echo from result_text at the source —
|
||||||
|
* server.py `_result_sans_diff_echo` — this guards older/other emitters.)
|
||||||
*/
|
*/
|
||||||
function outputRedundantWithDiff(part: ToolPartState): boolean {
|
function isDiffEchoFragment(r: string, diff: string): boolean {
|
||||||
|
const lines = r
|
||||||
|
.split('\n')
|
||||||
|
.map(l => l.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
if (lines.length < 3) return false
|
||||||
|
const inner = lines.slice(1, -1)
|
||||||
|
const hits = inner.filter(l => diff.includes(l)).length
|
||||||
|
return hits >= Math.ceil(inner.length / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decide the output section under a rendered diff. File-edit results are JSON
|
||||||
|
* records whose payload IS the diff (`patch` returns `{success, diff, …}`) —
|
||||||
|
* re-printing that below the native diff is noise. Worse, a verbose session's
|
||||||
|
* `result_text` may arrive gateway-CAPPED mid-JSON (unparseable) — a
|
||||||
|
* JSON-LOOKING blob under a rendered diff is never useful content, so anything
|
||||||
|
* starting with `{` is suppressed regardless of parseability, and a non-JSON
|
||||||
|
* fragment whose lines echo the diff (a tail-cap that lost the JSON head) is
|
||||||
|
* suppressed too. The exception: parseable JSON carrying real non-diff signal
|
||||||
|
* (error/warning strings, lsp_diagnostics) renders JUST those as labeled
|
||||||
|
* lines. Plain-text results still render in full.
|
||||||
|
*/
|
||||||
|
export function diffOutputPlan(part: ToolPartState): DiffOutputPlan {
|
||||||
const r = (part.resultText ?? '').trim()
|
const r = (part.resultText ?? '').trim()
|
||||||
if (!r) return true
|
if (!r) return { kind: 'suppress' }
|
||||||
if (!r.startsWith('{')) return false
|
if (!r.startsWith('{')) {
|
||||||
try {
|
if (part.diffUnified && isDiffEchoFragment(r, part.diffUnified)) return { kind: 'suppress' }
|
||||||
const o: unknown = JSON.parse(r)
|
return { kind: 'output' }
|
||||||
return Boolean(o && typeof o === 'object' && typeof (o as Record<string, unknown>)['diff'] === 'string')
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
let parsed: unknown
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(r)
|
||||||
|
} catch {
|
||||||
|
return { kind: 'suppress' } // capped/garbled JSON — the diff already tells the story
|
||||||
|
}
|
||||||
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return { kind: 'suppress' }
|
||||||
|
const o = parsed as Record<string, unknown>
|
||||||
|
const notes: Array<[string, string]> = []
|
||||||
|
for (const key of INTERESTING_KEYS) {
|
||||||
|
const v = o[key]
|
||||||
|
if (typeof v === 'string' && v.trim()) notes.push([key, v.trim()])
|
||||||
|
else if (Array.isArray(v)) {
|
||||||
|
const items = v.filter((x): x is string => typeof x === 'string' && Boolean(x.trim()))
|
||||||
|
if (items.length > 0) notes.push([key, items.join('\n')])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return notes.length > 0 ? { kind: 'notes', notes } : { kind: 'suppress' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The notes of a plan, as a `<Show when>`-friendly truthy value (else undefined). */
|
||||||
|
function notesOf(plan: DiffOutputPlan): Array<[string, string]> | undefined {
|
||||||
|
return plan.kind === 'notes' ? plan.notes : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
/** One file's diff: an optional path label (chrome) + the native `<diff>`. */
|
/** One file's diff: an optional path label (chrome) + the native `<diff>`. */
|
||||||
@@ -86,14 +145,40 @@ function FileDiff(props: { file: DiffFileSection; label: boolean; cwd?: string |
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Labeled non-diff notes (warnings/errors) surfaced from a JSON result. */
|
||||||
|
function DiffNotes(props: { notes: Array<[string, string]> }) {
|
||||||
|
const theme = useTheme()
|
||||||
|
return (
|
||||||
|
<For each={props.notes}>
|
||||||
|
{([key, value]) => (
|
||||||
|
<>
|
||||||
|
{/* section label — chrome, not content */}
|
||||||
|
<text selectable={false}>
|
||||||
|
<span style={{ fg: key === 'error' ? theme().color.error : theme().color.label }}>{key}</span>
|
||||||
|
</text>
|
||||||
|
<For each={value.split('\n')}>
|
||||||
|
{line => (
|
||||||
|
<text selectionBg={theme().color.selectionBg}>
|
||||||
|
<span style={{ fg: theme().color.muted }}>{line}</span>
|
||||||
|
</text>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** Expanded body: per-file native diffs (+ non-redundant output), else default. */
|
/** Expanded body: per-file native diffs (+ non-redundant output), else default. */
|
||||||
export function FileToolBody(props: ToolBodyProps) {
|
export function FileToolBody(props: ToolBodyProps) {
|
||||||
const files = createMemo(() => (props.part.diffUnified ? splitUnifiedDiff(props.part.diffUnified) : []))
|
const files = createMemo(() => (props.part.diffUnified ? splitUnifiedDiff(props.part.diffUnified) : []))
|
||||||
|
const plan = createMemo(() => diffOutputPlan(props.part))
|
||||||
return (
|
return (
|
||||||
<Show when={files().length > 0} fallback={<DefaultToolBody part={props.part} width={props.width} />}>
|
<Show when={files().length > 0} fallback={<DefaultToolBody part={props.part} width={props.width} />}>
|
||||||
<box style={{ flexDirection: 'column', flexGrow: 1, minWidth: 0 }}>
|
<box style={{ flexDirection: 'column', flexGrow: 1, minWidth: 0 }}>
|
||||||
<For each={files()}>{file => <FileDiff file={file} label={files().length > 1} cwd={props.cwd} />}</For>
|
<For each={files()}>{file => <FileDiff file={file} label={files().length > 1} cwd={props.cwd} />}</For>
|
||||||
<Show when={!outputRedundantWithDiff(props.part)}>
|
<Show when={notesOf(plan())}>{notes => <DiffNotes notes={notes()} />}</Show>
|
||||||
|
<Show when={plan().kind === 'output'}>
|
||||||
<ToolOutputBlock part={props.part} width={props.width} label />
|
<ToolOutputBlock part={props.part} width={props.width} label />
|
||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
|
|||||||
Reference in New Issue
Block a user