File size: 4,945 Bytes
7c5ed63 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | import { useEffect } from 'react';
import type { Issue, IssueSeverity } from '../../lib/issues';
interface IssuesDrawerProps {
issues: Issue[];
onClose: () => void;
onJumpTo?: (lineNo: number) => void;
/** Optional predicate: "is this line currently visible under the active
* filter?". When provided, jump buttons for filtered-out lines are
* disabled and flagged. */
isLineVisible?: (lineNo: number) => boolean;
}
const SEV_COLOR: Record<IssueSeverity, string> = {
error: 'var(--color-sev-error)',
warning: 'var(--color-sev-warning)',
info: 'var(--color-sev-info)',
};
const KIND_LABEL: Record<Issue['kind'], string> = {
orphan_tool_call: 'orphan tool.call',
missing_tool_result: 'missing tool.result',
tool_error: 'tool error',
tool_truncated: 'tool output truncated',
model_filtered: 'response filtered',
model_max_tokens: 'hit max_tokens',
incomplete_step: 'incomplete step',
incomplete_compaction: 'incomplete compaction',
active_plan_mode: 'plan mode active',
rejected_approval: 'approval rejected',
wire_warning: 'wire warning',
};
export function IssuesDrawer({ issues, onClose, onJumpTo, isLineVisible }: IssuesDrawerProps) {
// ESC closes — standard drawer affordance.
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', onKey);
return () => {
window.removeEventListener('keydown', onKey);
};
}, [onClose]);
return (
<>
<button
type="button"
aria-label="close issues"
onClick={onClose}
className="absolute inset-0 z-10 bg-black/20"
/>
<aside
className="absolute right-0 top-0 bottom-0 z-20 flex w-[360px] flex-col border-l border-border bg-surface-1 shadow-[-8px_0_32px_rgba(0,0,0,0.25)]"
role="dialog"
aria-label="issues"
>
<header className="flex shrink-0 items-center justify-between border-b border-border px-3 py-2 font-mono text-[12px] text-fg-0">
<span>
Issues <span className="text-fg-3">·</span>{' '}
<span className="tabular text-fg-2">{issues.length}</span>
</span>
<button
type="button"
onClick={onClose}
className="font-mono text-[14px] text-fg-3 hover:text-fg-0"
title="close (ESC)"
>
×
</button>
</header>
<div className="min-h-0 flex-1 overflow-y-auto">
{issues.length === 0 ? (
<div className="p-6 font-mono text-[12px] text-fg-3">no issues detected</div>
) : (
<ul className="divide-y divide-border">
{issues.map((iss, i) => (
<IssueItem
key={`${iss.kind}-${iss.lineNo ?? 'w'}-${i}`}
issue={iss}
onJumpTo={onJumpTo}
onClose={onClose}
isLineVisible={isLineVisible}
/>
))}
</ul>
)}
</div>
</aside>
</>
);
}
function IssueItem({
issue,
onJumpTo,
onClose,
isLineVisible,
}: {
issue: Issue;
onJumpTo?: (lineNo: number) => void;
onClose: () => void;
isLineVisible?: (lineNo: number) => boolean;
}) {
const color = SEV_COLOR[issue.severity];
const lineNo = issue.lineNo;
const hidden = lineNo !== null && isLineVisible !== undefined && !isLineVisible(lineNo);
const canJump = lineNo !== null && onJumpTo !== undefined && !hidden;
return (
<li className="px-3 py-2 hover:bg-surface-2">
<div className="flex items-center gap-2 font-mono text-[11px]">
<span
className="inline-block h-[8px] w-[8px] shrink-0 rounded-full"
style={{ backgroundColor: color }}
aria-hidden="true"
/>
<span className="text-fg-1">{KIND_LABEL[issue.kind]}</span>
{lineNo !== null ? (
<>
<span className="text-fg-3">·</span>
<span className="tabular text-fg-3">line {lineNo}</span>
</>
) : null}
{hidden ? <span className="text-fg-3">(filtered out)</span> : null}
{lineNo !== null ? (
<button
type="button"
disabled={!canJump}
onClick={() => {
if (canJump) onJumpTo?.(lineNo);
onClose();
}}
className="ml-auto text-fg-3 hover:text-fg-0 disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:text-fg-3"
title={hidden ? 'record is filtered out' : 'scroll to + expand'}
>
jump →
</button>
) : null}
</div>
<div className="mt-1 break-words font-mono text-[12px] text-fg-0">{issue.summary}</div>
{issue.detail !== undefined ? (
<div className="mt-0.5 break-words font-mono text-[10.5px] text-fg-3">{issue.detail}</div>
) : null}
</li>
);
}
|