Diff hunks + thread context on review comments; preview env links; hide matrix placeholder
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DgBEW6qAwgn2fcQbA2f4ZQ
This commit is contained in:
parent
df0e82e372
commit
d14b6ebed3
@ -25,6 +25,11 @@
|
|||||||
],
|
],
|
||||||
"specs": ["specs", "rspec", "rstest", "coverage", "qlty coverage", "qlty coverage diff"],
|
"specs": ["specs", "rspec", "rstest", "coverage", "qlty coverage", "qlty coverage diff"],
|
||||||
"e2e": ["Preview E2E *"]
|
"e2e": ["Preview E2E *"]
|
||||||
|
},
|
||||||
|
"ignoreChecks": ["Preview E2E ${{ matrix.folder }}"],
|
||||||
|
"preview": {
|
||||||
|
"label": "preview",
|
||||||
|
"urlTemplate": "https://pr-{number}.argocd.testmd.co.uk/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -68,6 +68,7 @@ export function categorize(rules: RepoRules | undefined, contexts: CheckContext[
|
|||||||
const buckets = new Map<CheckCategory, CheckInfo[]>(CATEGORIES.map((c) => [c, []]));
|
const buckets = new Map<CheckCategory, CheckInfo[]>(CATEGORIES.map((c) => [c, []]));
|
||||||
for (const ctx of dedupeByName(contexts)) {
|
for (const ctx of dedupeByName(contexts)) {
|
||||||
const name = ctx.__typename === 'StatusContext' ? ctx.context : ctx.name;
|
const name = ctx.__typename === 'StatusContext' ? ctx.context : ctx.name;
|
||||||
|
if (rules?.ignore.has(name)) continue;
|
||||||
buckets.get(categoryFor(rules, name))!.push({
|
buckets.get(categoryFor(rules, name))!.push({
|
||||||
name,
|
name,
|
||||||
state: checkState(ctx),
|
state: checkState(ctx),
|
||||||
|
|||||||
@ -15,6 +15,14 @@ const repoSchema = z.object({
|
|||||||
e2e: categorySchema.optional(),
|
e2e: categorySchema.optional(),
|
||||||
})
|
})
|
||||||
.default({}),
|
.default({}),
|
||||||
|
ignoreChecks: z.array(z.string()).default([]),
|
||||||
|
preview: z
|
||||||
|
.object({
|
||||||
|
label: z.string(),
|
||||||
|
/** `{number}` is replaced with the PR number. */
|
||||||
|
urlTemplate: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const muteRuleSchema = z.object({
|
const muteRuleSchema = z.object({
|
||||||
@ -47,6 +55,8 @@ interface GlobMatcher {
|
|||||||
export interface RepoRules {
|
export interface RepoRules {
|
||||||
exact: Map<string, CheckCategory>;
|
exact: Map<string, CheckCategory>;
|
||||||
globs: GlobMatcher[];
|
globs: GlobMatcher[];
|
||||||
|
ignore: Set<string>;
|
||||||
|
preview?: { label: string; urlTemplate: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppConfig extends RawConfig {
|
export interface AppConfig extends RawConfig {
|
||||||
@ -98,7 +108,12 @@ export function loadConfig(path = join(rootDir, 'config.json')): AppConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rulesByRepo.set(repo.name, { exact, globs });
|
rulesByRepo.set(repo.name, {
|
||||||
|
exact,
|
||||||
|
globs,
|
||||||
|
ignore: new Set(repo.ignoreChecks),
|
||||||
|
preview: repo.preview,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = ['is:pr', 'is:open', ...raw.repos.map((r) => `repo:${r.name}`)];
|
const parts = ['is:pr', 'is:open', ...raw.repos.map((r) => `repo:${r.name}`)];
|
||||||
|
|||||||
15
server/db.ts
15
server/db.ts
@ -55,6 +55,7 @@ export interface EventInsert {
|
|||||||
reviewState: string | null;
|
reviewState: string | null;
|
||||||
threadId: string | null;
|
threadId: string | null;
|
||||||
path: string | null;
|
path: string | null;
|
||||||
|
diffHunk: string | null;
|
||||||
read: boolean;
|
read: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,6 +77,11 @@ export class Db {
|
|||||||
this.db.exec('PRAGMA foreign_keys = ON');
|
this.db.exec('PRAGMA foreign_keys = ON');
|
||||||
this.db.exec(SCHEMA);
|
this.db.exec(SCHEMA);
|
||||||
|
|
||||||
|
const eventCols = this.db.prepare('PRAGMA table_info(events)').all() as { name: string }[];
|
||||||
|
if (!eventCols.some((c) => c.name === 'diff_hunk')) {
|
||||||
|
this.db.exec('ALTER TABLE events ADD COLUMN diff_hunk TEXT');
|
||||||
|
}
|
||||||
|
|
||||||
const clauses: string[] = [];
|
const clauses: string[] = [];
|
||||||
for (const rule of opts.muteRules ?? []) {
|
for (const rule of opts.muteRules ?? []) {
|
||||||
let clause = 'e.actor = ?';
|
let clause = 'e.actor = ?';
|
||||||
@ -155,10 +161,11 @@ export class Db {
|
|||||||
this.db
|
this.db
|
||||||
.prepare(
|
.prepare(
|
||||||
`INSERT INTO events (id, pr_id, kind, actor, created_at, updated_at, body_excerpt, url,
|
`INSERT INTO events (id, pr_id, kind, actor, created_at, updated_at, body_excerpt, url,
|
||||||
review_state, thread_id, path, read, read_at)
|
review_state, thread_id, path, diff_hunk, read, read_at)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
body_excerpt = excluded.body_excerpt, updated_at = excluded.updated_at`,
|
body_excerpt = excluded.body_excerpt, updated_at = excluded.updated_at,
|
||||||
|
diff_hunk = excluded.diff_hunk`,
|
||||||
)
|
)
|
||||||
.run(
|
.run(
|
||||||
e.id,
|
e.id,
|
||||||
@ -172,6 +179,7 @@ export class Db {
|
|||||||
e.reviewState,
|
e.reviewState,
|
||||||
e.threadId,
|
e.threadId,
|
||||||
e.path,
|
e.path,
|
||||||
|
e.diffHunk,
|
||||||
e.read ? 1 : 0,
|
e.read ? 1 : 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -229,6 +237,7 @@ export class Db {
|
|||||||
reviewState: r.review_state as string | null,
|
reviewState: r.review_state as string | null,
|
||||||
path: r.path as string | null,
|
path: r.path as string | null,
|
||||||
read: (r.read as number) === 1,
|
read: (r.read as number) === 1,
|
||||||
|
diffHunk: (r.diff_hunk as string | null) ?? null,
|
||||||
inReplyTo:
|
inReplyTo:
|
||||||
r.root_id && r.root_id !== r.id
|
r.root_id && r.root_id !== r.id
|
||||||
? { actor: r.root_actor as string, bodyExcerpt: (r.root_body as string) ?? '' }
|
? { actor: r.root_actor as string, bodyExcerpt: (r.root_body as string) ?? '' }
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export const LIST_QUERY = /* GraphQL */ `
|
|||||||
headRefName
|
headRefName
|
||||||
headRefOid
|
headRefOid
|
||||||
repository { nameWithOwner }
|
repository { nameWithOwner }
|
||||||
|
labels(first: 20) { nodes { name } }
|
||||||
commits(last: 1) { nodes { commit { statusCheckRollup { state } } } }
|
commits(last: 1) { nodes { commit { statusCheckRollup { state } } } }
|
||||||
reviewThreads(first: 50) { totalCount nodes { isResolved } }
|
reviewThreads(first: 50) { totalCount nodes { isResolved } }
|
||||||
comments(first: 1) { totalCount }
|
comments(first: 1) { totalCount }
|
||||||
@ -38,6 +39,7 @@ export interface ListPr {
|
|||||||
headRefName: string;
|
headRefName: string;
|
||||||
headRefOid: string;
|
headRefOid: string;
|
||||||
repository: { nameWithOwner: string };
|
repository: { nameWithOwner: string };
|
||||||
|
labels: { nodes: { name: string }[] };
|
||||||
commits: { nodes: { commit: { statusCheckRollup: { state: string } | null } }[] };
|
commits: { nodes: { commit: { statusCheckRollup: { state: string } | null } }[] };
|
||||||
reviewThreads: { totalCount: number; nodes: { isResolved: boolean }[] };
|
reviewThreads: { totalCount: number; nodes: { isResolved: boolean }[] };
|
||||||
comments: { totalCount: number };
|
comments: { totalCount: number };
|
||||||
@ -90,7 +92,7 @@ export const DETAIL_QUERY = /* GraphQL */ `
|
|||||||
isResolved
|
isResolved
|
||||||
path
|
path
|
||||||
comments(first: 30) {
|
comments(first: 30) {
|
||||||
nodes { id author { login } bodyText createdAt updatedAt url }
|
nodes { id author { login } bodyText createdAt updatedAt url diffHunk }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -110,6 +112,8 @@ export interface DetailComment {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
/** Present only on review-thread comments. */
|
||||||
|
diffHunk?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DetailPr {
|
export interface DetailPr {
|
||||||
|
|||||||
@ -36,6 +36,7 @@ function signature(pr: ListPr): string {
|
|||||||
pr.comments.totalCount,
|
pr.comments.totalCount,
|
||||||
pr.reviews.totalCount,
|
pr.reviews.totalCount,
|
||||||
pr.latestReviews.nodes.map((r) => `${r.author?.login}:${r.state}`),
|
pr.latestReviews.nodes.map((r) => `${r.author?.login}:${r.state}`),
|
||||||
|
pr.labels.nodes.map((l) => l.name).sort(),
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
.digest('hex');
|
.digest('hex');
|
||||||
@ -59,14 +60,20 @@ async function fetchList(config: AppConfig): Promise<ListPr[]> {
|
|||||||
|
|
||||||
function buildSnapshot(config: AppConfig, pr: ListPr, detail: DetailPr | undefined, db: Db): PrSnapshot {
|
function buildSnapshot(config: AppConfig, pr: ListPr, detail: DetailPr | undefined, db: Db): PrSnapshot {
|
||||||
const repo = pr.repository.nameWithOwner;
|
const repo = pr.repository.nameWithOwner;
|
||||||
|
const rules = config.rulesByRepo.get(repo);
|
||||||
let categories;
|
let categories;
|
||||||
if (detail) {
|
if (detail) {
|
||||||
const contexts = detail.commits.nodes[0]?.commit.statusCheckRollup?.contexts.nodes ?? [];
|
const contexts = detail.commits.nodes[0]?.commit.statusCheckRollup?.contexts.nodes ?? [];
|
||||||
categories = categorize(config.rulesByRepo.get(repo), contexts);
|
categories = categorize(rules, contexts);
|
||||||
} else {
|
} else {
|
||||||
// Detail node inaccessible this round — keep the previous check breakdown.
|
// Detail node inaccessible this round — keep the previous check breakdown.
|
||||||
categories = db.getSnapshot(pr.id)?.categories ?? categorize(config.rulesByRepo.get(repo), []);
|
categories = db.getSnapshot(pr.id)?.categories ?? categorize(rules, []);
|
||||||
}
|
}
|
||||||
|
const labels = pr.labels.nodes.map((l) => l.name);
|
||||||
|
const previewUrl =
|
||||||
|
rules?.preview && labels.includes(rules.preview.label)
|
||||||
|
? rules.preview.urlTemplate.replaceAll('{number}', String(pr.number))
|
||||||
|
: null;
|
||||||
return {
|
return {
|
||||||
id: pr.id,
|
id: pr.id,
|
||||||
repo,
|
repo,
|
||||||
@ -77,6 +84,8 @@ function buildSnapshot(config: AppConfig, pr: ListPr, detail: DetailPr | undefin
|
|||||||
isDraft: pr.isDraft,
|
isDraft: pr.isDraft,
|
||||||
branch: pr.headRefName,
|
branch: pr.headRefName,
|
||||||
updatedAt: pr.updatedAt,
|
updatedAt: pr.updatedAt,
|
||||||
|
labels,
|
||||||
|
previewUrl,
|
||||||
...deriveReviewState(pr),
|
...deriveReviewState(pr),
|
||||||
verdicts: deriveVerdicts(pr),
|
verdicts: deriveVerdicts(pr),
|
||||||
categories,
|
categories,
|
||||||
|
|||||||
@ -8,6 +8,17 @@ function excerpt(text: string): string {
|
|||||||
return clean.length > EXCERPT_LEN ? `${clean.slice(0, EXCERPT_LEN - 1)}…` : clean;
|
return clean.length > EXCERPT_LEN ? `${clean.slice(0, EXCERPT_LEN - 1)}…` : clean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HUNK_LINES = 8;
|
||||||
|
const HUNK_CHARS = 1200;
|
||||||
|
|
||||||
|
/** GitHub's diffHunk ends at the commented line — keep the trailing context. */
|
||||||
|
function trimHunk(hunk: string | undefined): string | null {
|
||||||
|
if (!hunk) return null;
|
||||||
|
const lines = hunk.split('\n');
|
||||||
|
const out = lines.slice(-HUNK_LINES).join('\n');
|
||||||
|
return out.length > HUNK_CHARS ? out.slice(-HUNK_CHARS) : out;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TimelineOpts {
|
export interface TimelineOpts {
|
||||||
user: string;
|
user: string;
|
||||||
/** First-ever sync: mark the historical backfill as already read. */
|
/** First-ever sync: mark the historical backfill as already read. */
|
||||||
@ -32,6 +43,7 @@ export function ingestTimeline(db: Db, pr: DetailPr, opts: TimelineOpts): number
|
|||||||
reviewState: null,
|
reviewState: null,
|
||||||
threadId: null,
|
threadId: null,
|
||||||
path: null,
|
path: null,
|
||||||
|
diffHunk: null,
|
||||||
read: false,
|
read: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -50,6 +62,7 @@ export function ingestTimeline(db: Db, pr: DetailPr, opts: TimelineOpts): number
|
|||||||
reviewState: null,
|
reviewState: null,
|
||||||
threadId: thread.id,
|
threadId: thread.id,
|
||||||
path: thread.path,
|
path: thread.path,
|
||||||
|
diffHunk: trimHunk(c.diffHunk),
|
||||||
read: false,
|
read: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -72,6 +85,7 @@ export function ingestTimeline(db: Db, pr: DetailPr, opts: TimelineOpts): number
|
|||||||
reviewState: r.state,
|
reviewState: r.state,
|
||||||
threadId: null,
|
threadId: null,
|
||||||
path: null,
|
path: null,
|
||||||
|
diffHunk: null,
|
||||||
read: false,
|
read: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,9 @@ export interface PrSnapshot {
|
|||||||
isDraft: boolean;
|
isDraft: boolean;
|
||||||
branch: string;
|
branch: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
labels: string[];
|
||||||
|
/** Per-PR review environment, when the repo configures one and the label matches. */
|
||||||
|
previewUrl: string | null;
|
||||||
reviewState: ReviewState;
|
reviewState: ReviewState;
|
||||||
unresolvedThreads: number;
|
unresolvedThreads: number;
|
||||||
totalThreads: number;
|
totalThreads: number;
|
||||||
@ -59,6 +62,8 @@ export interface TimelineEvent {
|
|||||||
reviewState: string | null;
|
reviewState: string | null;
|
||||||
path: string | null;
|
path: string | null;
|
||||||
read: boolean;
|
read: boolean;
|
||||||
|
/** Trailing lines of the diff hunk a review comment is anchored to. */
|
||||||
|
diffHunk: string | null;
|
||||||
/** Set when this is a reply within a review thread: the thread's root comment. */
|
/** Set when this is a reply within a review thread: the thread's root comment. */
|
||||||
inReplyTo: { actor: string; bodyExcerpt: string } | null;
|
inReplyTo: { actor: string; bodyExcerpt: string } | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,11 @@ export default function PrCard({ pr, unread }: { pr: PrSnapshot; unread: number
|
|||||||
{pr.branch}
|
{pr.branch}
|
||||||
</span>
|
</span>
|
||||||
<span>{relativeTime(pr.updatedAt)}</span>
|
<span>{relativeTime(pr.updatedAt)}</span>
|
||||||
|
{pr.previewUrl && (
|
||||||
|
<a className="preview-link" href={pr.previewUrl} target="_blank" rel="noreferrer">
|
||||||
|
preview ↗
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="chips-row">
|
<div className="chips-row">
|
||||||
<ReviewBadge pr={pr} />
|
<ReviewBadge pr={pr} />
|
||||||
|
|||||||
@ -62,14 +62,43 @@ function KindLabel({ event }: { event: TimelineEvent }) {
|
|||||||
if (event.kind === 'review_comment') {
|
if (event.kind === 'review_comment') {
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
commented{event.path ? ' on ' : ''}
|
{event.inReplyTo ? 'replied' : 'commented'}
|
||||||
{event.path && <span className="path">{event.path.split('/').pop()}</span>}
|
{event.path ? ' on ' : ''}
|
||||||
|
{event.path && (
|
||||||
|
<span className="path" title={event.path}>
|
||||||
|
{event.path.split('/').pop()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <span>commented</span>;
|
return <span>commented</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DiffHunk({ hunk }: { hunk: string }) {
|
||||||
|
return (
|
||||||
|
<pre className="tl-diff">
|
||||||
|
{hunk.split('\n').map((line, i) => (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
className={
|
||||||
|
line.startsWith('+')
|
||||||
|
? 'diff-add'
|
||||||
|
: line.startsWith('-')
|
||||||
|
? 'diff-del'
|
||||||
|
: line.startsWith('@@')
|
||||||
|
? 'diff-meta'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{line || ' '}
|
||||||
|
{'\n'}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function Timeline() {
|
export default function Timeline() {
|
||||||
const [unreadOnly, setUnreadOnly] = useState(false);
|
const [unreadOnly, setUnreadOnly] = useState(false);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@ -144,14 +173,18 @@ export default function Timeline() {
|
|||||||
readMutation.mutate({ id: e.id, read: !e.read });
|
readMutation.mutate({ id: e.id, read: !e.read });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div className="tl-content">
|
||||||
<div className="tl-kind">
|
<div className="tl-kind">
|
||||||
<KindLabel event={e} />
|
<KindLabel event={e} />
|
||||||
</div>
|
</div>
|
||||||
|
{e.diffHunk && !e.inReplyTo && <DiffHunk hunk={e.diffHunk} />}
|
||||||
{e.inReplyTo && (
|
{e.inReplyTo && (
|
||||||
<div className="tl-reply-ctx">
|
<div className="tl-thread">
|
||||||
↳ <span className="actor">{e.inReplyTo.actor}</span>:{' '}
|
{e.diffHunk && <DiffHunk hunk={e.diffHunk} />}
|
||||||
{e.inReplyTo.bodyExcerpt}
|
<div className="tl-reply-ctx">
|
||||||
|
<span className="actor">{e.inReplyTo.actor}</span>{' '}
|
||||||
|
{e.inReplyTo.bodyExcerpt}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{e.bodyExcerpt && <div className="body">{e.bodyExcerpt}</div>}
|
{e.bodyExcerpt && <div className="body">{e.bodyExcerpt}</div>}
|
||||||
|
|||||||
@ -288,6 +288,19 @@ button {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-link {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-link:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
.unread-pill {
|
.unread-pill {
|
||||||
background: var(--unread);
|
background: var(--unread);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@ -580,11 +593,20 @@ a.check-row:hover {
|
|||||||
background: var(--unread);
|
background: var(--unread);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tl-content {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-thread {
|
||||||
|
border-left: 2px solid var(--hairline);
|
||||||
|
padding-left: 8px;
|
||||||
|
margin: 3px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.tl-reply-ctx {
|
.tl-reply-ctx {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--ink-muted);
|
color: var(--ink-muted);
|
||||||
border-left: 2px solid var(--hairline);
|
|
||||||
padding-left: 7px;
|
|
||||||
margin: 2px 0;
|
margin: 2px 0;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
@ -597,6 +619,39 @@ a.check-row:hover {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tl-diff {
|
||||||
|
margin: 3px 0;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid var(--hairline);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--page);
|
||||||
|
font-family: ui-monospace, monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.5;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre;
|
||||||
|
max-height: 160px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-diff .diff-add {
|
||||||
|
color: var(--status-pass-text);
|
||||||
|
background: var(--chip-pass-bg);
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-diff .diff-del {
|
||||||
|
color: var(--status-fail-text);
|
||||||
|
background: var(--chip-fail-bg);
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tl-diff .diff-meta {
|
||||||
|
color: var(--ink-muted);
|
||||||
|
}
|
||||||
|
|
||||||
.tl-item .body {
|
.tl-item .body {
|
||||||
color: var(--ink-secondary);
|
color: var(--ink-secondary);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user