mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
feat(web-shell): reveal full tool detail and auto-collapse finished tools (#5088)
* feat(web-shell): reveal full tool detail and auto-collapse finished tools Long tool descriptions were hard-capped at 120 characters and finished tools (shell/edit/write) stayed expanded indefinitely, so commands were unreadable and the transcript filled with stale output. - Lift the 120-char description cap so the full command/path reaches the DOM; collapsed rows ellipsise via CSS (adapts to width) and a click reflows the full text into a wrapped block below the header. - Add a leading disclosure chevron; any row with detail output or a long description is now expandable. - Auto-collapse a tool to its one-line summary once it completes successfully. Running tools stay expanded (live output) and failures stay expanded (error visible); agents keep their own manual expand state. * fix(web-shell): preserve manual expand on completion; correct auto-expand comment Review feedback on #5088: - shouldAutoExpand: rewrite the comment to match the code. Only the verbose kinds (shell/edit/write/ask) auto-expand and stay expanded on failure; other kinds are collapsed by default (their summary line shows the outcome and they stay click-to-expand). Force-expanding every failed tool was rejected because tools without an expanded-detail renderer would then hide the summary line and show an empty body — i.e. hide the error. - Auto-collapse-on-completion no longer overrides an explicit user toggle: a userToggledRef latch (set on header click, reset on tool-identity change) guards the collapse effect, so a row the user expanded/collapsed keeps its state when the tool finishes. * test(web-shell): assert tool-detail relocation via DOM, not textContent Review feedback (#5088): the expand test asserted container.textContent contains the command before and after the click, which passes regardless of whether the description is relocated from the header span to the wrapped block (textContent concatenates the whole subtree). Assert the DOM move instead — the command is in a leaf <span> while collapsed and in none while expanded — so a regression dropping the relocation now fails the test.
This commit is contained in:
parent
e8342715e5
commit
800507598c
5 changed files with 381 additions and 13 deletions
|
|
@ -27,23 +27,26 @@ afterEach(() => {
|
|||
}
|
||||
});
|
||||
|
||||
function makeShellTool(output: string): ACPToolCall {
|
||||
function makeShellTool(
|
||||
output: string,
|
||||
status: ACPToolCall['status'] = 'completed',
|
||||
): ACPToolCall {
|
||||
return {
|
||||
callId: 'call-shell-1',
|
||||
toolName: 'Shell',
|
||||
status: 'completed',
|
||||
status,
|
||||
rawOutput: { output },
|
||||
};
|
||||
}
|
||||
|
||||
function renderShellTool(output: string): HTMLElement {
|
||||
function renderTool(tool: ACPToolCall): HTMLElement {
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
act(() => {
|
||||
root.render(
|
||||
<I18nProvider language="en">
|
||||
<ToolGroup tools={[makeShellTool(output)]} />
|
||||
<ToolGroup tools={[tool]} />
|
||||
</I18nProvider>,
|
||||
);
|
||||
});
|
||||
|
|
@ -51,6 +54,44 @@ function renderShellTool(output: string): HTMLElement {
|
|||
return container;
|
||||
}
|
||||
|
||||
function renderShellTool(output: string): HTMLElement {
|
||||
const container = renderTool(makeShellTool(output));
|
||||
// Completed tools collapse to a one-line summary by default; open the row so
|
||||
// the assertions below can inspect the bash-output view.
|
||||
const chevron = [...container.querySelectorAll('span')].find(
|
||||
(s) => s.textContent === '▸',
|
||||
);
|
||||
if (chevron?.parentElement) click(chevron.parentElement);
|
||||
return container;
|
||||
}
|
||||
|
||||
function makeShellCommandTool(command: string): ACPToolCall {
|
||||
return {
|
||||
callId: 'call-shell-cmd',
|
||||
toolName: 'run_shell_command',
|
||||
status: 'completed',
|
||||
args: { command },
|
||||
rawOutput: { output: 'done' },
|
||||
};
|
||||
}
|
||||
|
||||
function makeAgentTool(status: ACPToolCall['status']): ACPToolCall {
|
||||
return {
|
||||
callId: 'agent-1',
|
||||
toolName: 'task',
|
||||
status,
|
||||
args: { description: 'agentDescMarker' },
|
||||
subTools: [
|
||||
{
|
||||
callId: 'agent-1-sub-1',
|
||||
toolName: 'Read',
|
||||
status: 'completed',
|
||||
args: { file_path: '/ws/SubToolMarker.ts' },
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function getExpandButton(container: HTMLElement): HTMLButtonElement {
|
||||
const button = container.querySelector('button');
|
||||
expect(button).not.toBeNull();
|
||||
|
|
@ -124,3 +165,185 @@ describe('shell tool output expand toggle', () => {
|
|||
expect(button.getAttribute('aria-expanded')).toBe('false');
|
||||
});
|
||||
});
|
||||
|
||||
describe('tool description expand toggle', () => {
|
||||
it('relocates the full command from the header into a wrapped block on expand', () => {
|
||||
const command = `npm run build && npm run test && npm run lint -- ${'x'.repeat(
|
||||
80,
|
||||
)}`;
|
||||
const container = renderTool(makeShellCommandTool(command));
|
||||
|
||||
// textContent alone can't prove relocation (it concatenates the whole
|
||||
// subtree, so the command is present in either state). Assert the DOM move
|
||||
// instead: collapsed, the full command lives in the header's single-line
|
||||
// arg <span> (CSS-ellipsised); expanded, it is no longer in any leaf <span>
|
||||
// but reflowed into the wrapped block below.
|
||||
const commandInLeafSpan = () =>
|
||||
[...container.querySelectorAll('span')].some(
|
||||
(s) => s.textContent === command,
|
||||
);
|
||||
|
||||
expect(container.textContent).toContain('▸');
|
||||
expect(commandInLeafSpan()).toBe(true);
|
||||
|
||||
const chevron = [...container.querySelectorAll('span')].find(
|
||||
(s) => s.textContent === '▸',
|
||||
);
|
||||
click(chevron!.parentElement!);
|
||||
|
||||
expect(container.textContent).toContain('▾');
|
||||
expect(commandInLeafSpan()).toBe(false);
|
||||
expect(container.textContent).toContain(command); // still present, in the block
|
||||
});
|
||||
|
||||
it('keeps the result summary when expanding a long-description tool with no detail view', () => {
|
||||
// glob with a long pattern: descExpandable but no kind-specific renderer.
|
||||
const pattern = `**/${'x'.repeat(80)}/*.ts`;
|
||||
const container = renderTool({
|
||||
callId: 'call-glob',
|
||||
toolName: 'glob',
|
||||
status: 'completed',
|
||||
args: { pattern },
|
||||
rawOutput: 'a.ts\nb.ts\nc.ts',
|
||||
});
|
||||
|
||||
const chevron = [...container.querySelectorAll('span')].find(
|
||||
(s) => s.textContent === '▸',
|
||||
);
|
||||
expect(chevron).toBeTruthy(); // long pattern → expandable
|
||||
expect(container.textContent).toContain('matching file'); // summary, collapsed
|
||||
|
||||
click(chevron!.parentElement!);
|
||||
|
||||
// Expanded: the summary must NOT be lost (no detail view replaces it), and
|
||||
// the full pattern is reflowed into the block.
|
||||
expect(container.textContent).toContain('matching file');
|
||||
expect(container.textContent).toContain(pattern);
|
||||
});
|
||||
});
|
||||
|
||||
describe('auto-collapse on finish', () => {
|
||||
it('collapses a completed tool to its summary by default', () => {
|
||||
const container = renderTool(makeShellTool('a\nb\nc\nd'));
|
||||
// The expanded bash <pre> is not rendered until the user opens the row.
|
||||
expect(container.querySelector('pre')).toBeNull();
|
||||
expect(container.textContent).toContain('▸');
|
||||
});
|
||||
|
||||
it('keeps a running tool expanded so streaming output stays visible', () => {
|
||||
const container = renderTool(
|
||||
makeShellTool('streaming output', 'in_progress'),
|
||||
);
|
||||
expect(container.querySelector('pre')?.textContent).toContain('streaming');
|
||||
});
|
||||
|
||||
it('keeps a failed tool expanded so the error stays visible', () => {
|
||||
const container = renderTool(
|
||||
makeShellTool('error: boom\n at step 1', 'failed'),
|
||||
);
|
||||
expect(container.querySelector('pre')?.textContent).toContain('boom');
|
||||
});
|
||||
|
||||
it('auto-collapses a running tool when it transitions to completed', () => {
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
mounted.push({ root, container });
|
||||
|
||||
const output = 'line1\nline2\nline3\nline4';
|
||||
act(() => {
|
||||
root.render(
|
||||
<I18nProvider language="en">
|
||||
<ToolGroup tools={[makeShellTool(output, 'in_progress')]} />
|
||||
</I18nProvider>,
|
||||
);
|
||||
});
|
||||
// Running → expanded: the bash output is visible.
|
||||
expect(container.querySelector('pre')).not.toBeNull();
|
||||
|
||||
// Same callId, now finished → the row collapses on its own.
|
||||
act(() => {
|
||||
root.render(
|
||||
<I18nProvider language="en">
|
||||
<ToolGroup tools={[makeShellTool(output, 'completed')]} />
|
||||
</I18nProvider>,
|
||||
);
|
||||
});
|
||||
expect(container.querySelector('pre')).toBeNull();
|
||||
});
|
||||
|
||||
it('does not collapse an agent the user expanded when it completes', () => {
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
mounted.push({ root, container });
|
||||
|
||||
const render = (status: ACPToolCall['status']) =>
|
||||
act(() => {
|
||||
root.render(
|
||||
<I18nProvider language="en">
|
||||
<ToolGroup tools={[makeAgentTool(status)]} />
|
||||
</I18nProvider>,
|
||||
);
|
||||
});
|
||||
|
||||
render('in_progress');
|
||||
// Agents start collapsed: the sub-tool panel is hidden.
|
||||
expect(container.textContent).not.toContain('SubToolMarker');
|
||||
|
||||
// Expand by clicking the agent's summary row.
|
||||
const summaryLabel = [...container.querySelectorAll('span')].find(
|
||||
(s) => s.textContent === 'task:',
|
||||
);
|
||||
expect(summaryLabel).toBeTruthy();
|
||||
click(summaryLabel!.parentElement!);
|
||||
expect(container.textContent).toContain('SubToolMarker');
|
||||
|
||||
// Completion must NOT yank the panel shut: the collapse-on-finish effect
|
||||
// is scoped to non-agent tools.
|
||||
render('completed');
|
||||
expect(container.textContent).toContain('SubToolMarker');
|
||||
});
|
||||
|
||||
it('keeps a tool the user manually expanded open when it completes', () => {
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
mounted.push({ root, container });
|
||||
|
||||
// Long command → the row is expandable/clickable even while running.
|
||||
const command = `echo ${'z'.repeat(80)}`;
|
||||
const renderStatus = (status: ACPToolCall['status']) =>
|
||||
act(() => {
|
||||
root.render(
|
||||
<I18nProvider language="en">
|
||||
<ToolGroup
|
||||
tools={[
|
||||
{
|
||||
callId: 'call-usertoggle',
|
||||
toolName: 'run_shell_command',
|
||||
status,
|
||||
args: { command },
|
||||
rawOutput: { output: 'done' },
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</I18nProvider>,
|
||||
);
|
||||
});
|
||||
|
||||
renderStatus('in_progress');
|
||||
const chevron = () =>
|
||||
[...container.querySelectorAll('span')].find(
|
||||
(s) => s.textContent === '▾' || s.textContent === '▸',
|
||||
);
|
||||
// Toggle twice → marks the row user-controlled, ending in the expanded state.
|
||||
click(chevron()!.parentElement!);
|
||||
click(chevron()!.parentElement!);
|
||||
expect(container.textContent).toContain('▾');
|
||||
|
||||
// Completion must NOT override the user's explicit expand.
|
||||
renderStatus('completed');
|
||||
expect(container.textContent).toContain('▾');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { memo, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { memo, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { DaemonSettingDescriptor } from '@qwen-code/webui/daemon-react-sdk';
|
||||
import type {
|
||||
ACPToolCall,
|
||||
|
|
@ -81,6 +81,27 @@ function hasExpandableContent(tool: ACPToolCall): boolean {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Tools whose expanded row renders a kind-specific detail view (shell output /
|
||||
// diff / file content / Q&A). Must stay in sync with the renderers in
|
||||
// ToolLine's lineDetail block below. Tools NOT in this set have nothing extra
|
||||
// to show when expanded, so they keep their one-line result summary instead of
|
||||
// hiding it behind an empty detail area.
|
||||
function hasDetailView(tool: ACPToolCall): boolean {
|
||||
const name = tool.toolName.toLowerCase();
|
||||
return (
|
||||
isShellToolName(name) ||
|
||||
name === 'write_file' ||
|
||||
name === 'writefile' ||
|
||||
name === 'edit' ||
|
||||
name === 'write' ||
|
||||
name === 'editfile' ||
|
||||
name === 'read' ||
|
||||
name === 'read_file' ||
|
||||
name === 'readfile' ||
|
||||
isAskUserQuestionToolName(tool.toolName)
|
||||
);
|
||||
}
|
||||
|
||||
function hasDiffContent(tool: ACPToolCall): boolean {
|
||||
if (tool.content?.some((b) => b.type === 'diff')) return true;
|
||||
if (tool.rawOutput && typeof tool.rawOutput === 'object') {
|
||||
|
|
@ -154,6 +175,10 @@ function buildUnifiedDiff(oldText: string, newText: string): string {
|
|||
const MAX_BASH_LINE_CHARS = 150;
|
||||
const MAX_READ_LINES = 25;
|
||||
|
||||
// A description longer than this is likely ellipsised on a normal-width row, so
|
||||
// the row becomes expandable to re-flow the full text into a wrapped block.
|
||||
const DESCRIPTION_EXPAND_THRESHOLD = 60;
|
||||
|
||||
export function resolveShellOutputMaxLines(
|
||||
settings: readonly DaemonSettingDescriptor[],
|
||||
): number {
|
||||
|
|
@ -411,6 +436,14 @@ function getAgentDisplayInfo(
|
|||
}
|
||||
|
||||
function shouldAutoExpand(tool: ACPToolCall): boolean {
|
||||
// Only the verbose tool kinds below (shell/edit/write/ask) auto-expand, and
|
||||
// only while pending/in-progress or after failing: a successful completion
|
||||
// collapses them to a one-line summary so the transcript stays scannable
|
||||
// (click to reopen), while a failure of those kinds stays expanded so its
|
||||
// error output is visible without a click. Every other tool kind is collapsed
|
||||
// by default regardless of status — its summary line already shows the
|
||||
// outcome and it stays click-to-expand.
|
||||
if (tool.status === 'completed') return false;
|
||||
const name = tool.toolName.toLowerCase();
|
||||
if (isAskUserQuestionToolName(tool.toolName)) return true;
|
||||
if (name === 'write_file' || name === 'writefile') return true;
|
||||
|
|
@ -465,6 +498,27 @@ function ToolHeaderExtra({ info }: { info: ToolHeaderExtraRenderInfo }) {
|
|||
);
|
||||
}
|
||||
|
||||
function isDescriptionExpandable(description: string): boolean {
|
||||
return (
|
||||
description.length > DESCRIPTION_EXPAND_THRESHOLD ||
|
||||
description.includes('\n')
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleChevron({
|
||||
expandable,
|
||||
expanded,
|
||||
}: {
|
||||
expandable: boolean;
|
||||
expanded: boolean;
|
||||
}) {
|
||||
return (
|
||||
<span className={styles.lineToggle} aria-hidden="true">
|
||||
{expandable ? (expanded ? '▾' : '▸') : ''}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function getActiveTool(tools: ACPToolCall[]): ACPToolCall {
|
||||
return (
|
||||
tools.find((t) => t.status === 'in_progress') ?? tools[tools.length - 1]
|
||||
|
|
@ -586,11 +640,16 @@ export const ToolLine = memo(function ToolLine({
|
|||
const [expanded, setExpanded] = useState(
|
||||
() => !compactMode && shouldAutoExpand(tool),
|
||||
);
|
||||
// Set once the user explicitly toggles this row, so auto-collapse-on-
|
||||
// completion never silently overrides their choice.
|
||||
const userToggledRef = useRef(false);
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
setExpanded(compactMode ? false : shouldAutoExpand(tool));
|
||||
// A new tool identity (or compact-mode toggle) resets the manual latch.
|
||||
userToggledRef.current = false;
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[compactMode, tool.callId, tool.toolName],
|
||||
|
|
@ -611,6 +670,17 @@ export const ToolLine = memo(function ToolLine({
|
|||
return () => clearInterval(id);
|
||||
}, [isRunningAgent]);
|
||||
|
||||
// Collapse a regular tool to its one-line summary once it completes
|
||||
// successfully — unless the user explicitly toggled this row, in which case
|
||||
// their choice wins. Agents are excluded (they keep whatever expand state the
|
||||
// user chose, driven from their own panel) and failures stay open so the
|
||||
// error output remains visible.
|
||||
useEffect(() => {
|
||||
if (!isAgent && tool.status === 'completed' && !userToggledRef.current) {
|
||||
setExpanded(false);
|
||||
}
|
||||
}, [isAgent, tool.status]);
|
||||
|
||||
if (isAgent) {
|
||||
if (hasApproval && onConfirm) {
|
||||
return (
|
||||
|
|
@ -712,10 +782,20 @@ export const ToolLine = memo(function ToolLine({
|
|||
isShellToolName(tool.toolName) || isWebFetchToolName(tool.toolName)
|
||||
? ''
|
||||
: formatElapsed(tool.startTime, tool.endTime);
|
||||
const expandable = hasExpandableContent(tool);
|
||||
|
||||
const name = tool.toolName.toLowerCase();
|
||||
const isTodo = name === 'todowrite';
|
||||
// A row expands when it has detail output (bash/diff/read content) or when
|
||||
// its description is long enough to be ellipsised. When a long description is
|
||||
// expanded we move it out of the header into a wrapped block below, so the
|
||||
// header drops its single-line copy.
|
||||
const descExpandable = !isTodo && isDescriptionExpandable(description);
|
||||
const expandable = !isTodo && (hasExpandableContent(tool) || descExpandable);
|
||||
const relocateDescription = expanded && descExpandable;
|
||||
// Whether the expanded row renders a kind-specific detail view. When it does
|
||||
// not (e.g. grep/glob/web_fetch with a long description), keep the result
|
||||
// summary visible instead of replacing it with an empty detail area.
|
||||
const detailView = hasDetailView(tool);
|
||||
|
||||
if (hasApproval && onConfirm) {
|
||||
return (
|
||||
|
|
@ -729,8 +809,16 @@ export const ToolLine = memo(function ToolLine({
|
|||
<div className={styles.line}>
|
||||
<div
|
||||
className={`${styles.lineMain} ${expandable ? styles.lineExpandable : ''}`}
|
||||
onClick={expandable ? () => setExpanded(!expanded) : undefined}
|
||||
onClick={
|
||||
expandable
|
||||
? () => {
|
||||
userToggledRef.current = true;
|
||||
setExpanded((value) => !value);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<ToggleChevron expandable={expandable} expanded={expanded} />
|
||||
<StatusIcon status={tool.status} />
|
||||
<span className={styles.lineName}>{displayName}</span>
|
||||
<ToolHeaderExtra
|
||||
|
|
@ -738,17 +826,20 @@ export const ToolLine = memo(function ToolLine({
|
|||
kind: getToolHeaderKind(tool),
|
||||
tool,
|
||||
displayName,
|
||||
description,
|
||||
description: relocateDescription ? '' : description,
|
||||
elapsed,
|
||||
workspaceCwd,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{isTodo && <TodoWriteContent tool={tool} />}
|
||||
{!isTodo && !expanded && result && (
|
||||
{relocateDescription && (
|
||||
<div className={styles.lineFullArg}>{description}</div>
|
||||
)}
|
||||
{!isTodo && result && (!expanded || !detailView) && (
|
||||
<div className={styles.lineOutput}>{result}</div>
|
||||
)}
|
||||
{!isTodo && expanded && (
|
||||
{!isTodo && expanded && detailView && (
|
||||
<div className={styles.lineDetail}>
|
||||
{isShellToolName(name) && (
|
||||
<ExpandedBashOutput tool={tool} maxLines={shellOutputMaxLines} />
|
||||
|
|
|
|||
|
|
@ -170,4 +170,24 @@ describe('toolFormatting', () => {
|
|||
),
|
||||
).toBe('3 line(s)');
|
||||
});
|
||||
|
||||
it('keeps long shell commands in full instead of capping at one line', () => {
|
||||
const command = `echo ${'a'.repeat(200)}`;
|
||||
expect(
|
||||
getToolDescription(
|
||||
tool({ toolName: 'run_shell_command', args: { command } }),
|
||||
),
|
||||
).toBe(command);
|
||||
});
|
||||
|
||||
it('still bounds a pathologically long description', () => {
|
||||
const result = getToolDescription(
|
||||
tool({
|
||||
toolName: 'run_shell_command',
|
||||
args: { command: 'x'.repeat(5000) },
|
||||
}),
|
||||
);
|
||||
expect(result.length).toBeLessThan(5000);
|
||||
expect(result.endsWith('...')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -49,14 +49,21 @@ export function truncateText(text: string, max: number): string {
|
|||
return text.slice(0, max) + '...';
|
||||
}
|
||||
|
||||
// The tool-header description is shown single-line (CSS-ellipsised) when the
|
||||
// row is collapsed and fully wrapped when it is expanded, so we keep the whole
|
||||
// string rather than hard-capping it at a line's worth of characters. A
|
||||
// generous ceiling still guards against a pathological multi-megabyte command
|
||||
// bloating the DOM.
|
||||
const MAX_DESCRIPTION_LENGTH = 2000;
|
||||
|
||||
export function getToolDescription(
|
||||
tool: ACPToolCall,
|
||||
workspaceCwd?: string,
|
||||
): string {
|
||||
const fromTitle = getDescriptionFromTitle(tool, workspaceCwd);
|
||||
if (fromTitle) return truncateText(fromTitle, 120);
|
||||
if (fromTitle) return truncateText(fromTitle, MAX_DESCRIPTION_LENGTH);
|
||||
const fromArgs = getDescriptionFromArgs(tool, workspaceCwd);
|
||||
if (fromArgs) return truncateText(fromArgs, 120);
|
||||
if (fromArgs) return truncateText(fromArgs, MAX_DESCRIPTION_LENGTH);
|
||||
return '';
|
||||
}
|
||||
|
||||
|
|
@ -177,7 +184,7 @@ function getDescriptionFromArgs(
|
|||
if (args.description) {
|
||||
description += ` (${String(args.description).replace(/\n/g, ' ')})`;
|
||||
}
|
||||
return truncateText(description, 120);
|
||||
return truncateText(description, MAX_DESCRIPTION_LENGTH);
|
||||
}
|
||||
if (name === 'grep_search' || name === 'grep' || name === 'search') {
|
||||
const pattern = args.pattern ?? args.query;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,18 @@
|
|||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Leading disclosure column. Rendered (blank) for every tool row so names stay
|
||||
aligned; shows ▸/▾ only when the row can expand. */
|
||||
.lineToggle {
|
||||
width: 12px;
|
||||
flex-shrink: 0;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
color: var(--text-dimmed);
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 13px;
|
||||
flex-shrink: 0;
|
||||
|
|
@ -110,6 +122,21 @@
|
|||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Full, wrapped tool argument shown below the header when the row is expanded
|
||||
(the header drops its single-line ellipsised copy). Aligns with the expanded
|
||||
output block below it. */
|
||||
.lineFullArg {
|
||||
margin-left: 16px;
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--text-secondary);
|
||||
font-family: var(--font-mono);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.lineDetail {
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue