taskchef 7.24.0 → 7.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -60,12 +60,20 @@ function compareReferenceNumbers(left, right) {
60
60
 
61
61
  export function groupRelatedGitHubLinks(links) {
62
62
  const groups = new Map();
63
- const seen = new Set();
63
+ const seenLabels = new Set();
64
+ const seenNumbers = new Set();
65
+ const seenUrls = new Set();
64
66
  for (const link of links ?? []) {
65
- if (link.type === "repository" || seen.has(link.label)) continue;
66
- seen.add(link.label);
67
67
  const repository = repositoryIdentity(link);
68
- if (!repository) continue;
68
+ const reference = link.number ? `${repository}#${link.number}` : null;
69
+ if (link.type === "repository"
70
+ || !repository
71
+ || seenLabels.has(link.label)
72
+ || seenUrls.has(link.url)
73
+ || (reference && seenNumbers.has(reference))) continue;
74
+ seenLabels.add(link.label);
75
+ seenUrls.add(link.url);
76
+ if (reference) seenNumbers.add(reference);
69
77
  const group = groups.get(repository) ?? [];
70
78
  group.push(link);
71
79
  groups.set(repository, group);
@@ -3,6 +3,7 @@ import { mergeProjectedTurns, turnPresentation } from "../../state.js";
3
3
  import { turnUsageView } from "../presentation";
4
4
  import type { Task } from "../types";
5
5
  import { RelativeTime } from "./RelativeTime";
6
+ import { LinkedText } from "./LinkedText";
6
7
  import { ShimmerText } from "./ShimmerText";
7
8
  import { StatusBadge } from "./StatusBadge";
8
9
 
@@ -34,14 +35,16 @@ export function ActivityTimeline({ highlightTurnRef, task }: { highlightTurnRef:
34
35
  {presentation.sourceLabel && <Text c="dimmed" size="xs">{presentation.sourceLabel}</Text>}
35
36
  <Box>
36
37
  <Text c="dimmed" className="taskchef-field-label" size="xs">Request</Text>
37
- <Text className="taskchef-preserve-lines" size="sm">{turn.requestSummary ?? "Request not recorded."}</Text>
38
+ <Text className="taskchef-preserve-lines" size="sm">
39
+ <LinkedText task={task} text={turn.requestSummary ?? "Request not recorded."} />
40
+ </Text>
38
41
  </Box>
39
42
  <Box>
40
43
  <Text c="dimmed" className="taskchef-field-label" size="xs">Result</Text>
41
44
  <Text className="taskchef-preserve-lines" size="sm">
42
45
  {presentation.status === "working"
43
46
  ? <ShimmerText>{presentation.summary}</ShimmerText>
44
- : presentation.summary}
47
+ : <LinkedText task={task} text={presentation.summary} />}
45
48
  </Text>
46
49
  </Box>
47
50
  <Text
@@ -1,25 +1,46 @@
1
- import { Anchor, Group, Text } from "@mantine/core";
2
- import type { Task } from "../types";
1
+ import { Anchor, Box, Group, Text } from "@mantine/core";
2
+ import {
3
+ githubReferenceAccessibleLabel,
4
+ githubReferenceDisplayLabels,
5
+ groupRelatedGitHubLinks,
6
+ } from "../../github-links.js";
7
+ import type { GitHubLink, Task } from "../types";
3
8
 
4
9
  export function GitHubLinks({ task }: { task: Task }) {
5
10
  const links = task.relatedGitHubLinks ?? [];
6
11
  if (links.length === 0 && !task.relatedGitHubLinksTruncated) return null;
12
+ const groups = groupRelatedGitHubLinks(links) as GitHubLink[][];
13
+ const displayLabels = githubReferenceDisplayLabels(groups.flat(), { related: true }) as string[];
14
+ let linkIndex = 0;
7
15
  return (
8
- <Group aria-label={`Related GitHub links for ${task.title}`} component="nav" gap={5} wrap="wrap">
9
- {links.map((link) => (
10
- <Anchor
11
- aria-label={`${link.label}, GitHub ${link.type === "pull" ? "pull request" : link.type ?? "link"} (opens in a new tab)`}
12
- className="taskchef-github-link"
13
- href={link.url}
14
- key={link.url}
15
- rel="noopener noreferrer"
16
- size="xs"
17
- target="_blank"
18
- >
19
- {link.label}
20
- </Anchor>
16
+ <Box aria-label={`Related GitHub links for ${task.title}`} className="taskchef-github-groups" component="nav">
17
+ {groups.map((group) => (
18
+ <Group className="taskchef-github-group" gap={5} key={`${group[0].owner}/${group[0].repository}`} wrap="wrap">
19
+ {group.map((link) => {
20
+ const displayLabel = displayLabels[linkIndex];
21
+ linkIndex += 1;
22
+ const kind = link.type === "pull"
23
+ ? ", GitHub pull request"
24
+ : link.type === "issue"
25
+ ? ", GitHub issue"
26
+ : " on GitHub";
27
+ return (
28
+ <Anchor
29
+ aria-label={`${githubReferenceAccessibleLabel(link)}${kind} (opens in a new tab)`}
30
+ className="taskchef-github-link"
31
+ href={link.url}
32
+ key={link.url}
33
+ rel="noopener noreferrer"
34
+ size="xs"
35
+ target="_blank"
36
+ >
37
+ {displayLabel}
38
+ </Anchor>
39
+ );
40
+ })}
41
+ </Group>
21
42
  ))}
22
43
  {task.relatedGitHubLinksTruncated && <Text c="dimmed" size="xs">More links in task history</Text>}
23
- </Group>
44
+ </Box>
24
45
  );
25
46
  }
@@ -0,0 +1,63 @@
1
+ import { Anchor } from "@mantine/core";
2
+ import {
3
+ githubReferenceAccessibleLabel,
4
+ githubReferenceDisplayLabels,
5
+ referenceSegments,
6
+ } from "../../github-links.js";
7
+ import type { Task } from "../types";
8
+
9
+ interface ReferenceSegment {
10
+ kind: "ambiguous" | "link" | "text";
11
+ label?: string;
12
+ owner?: string;
13
+ reason?: string;
14
+ repository?: string;
15
+ text: string;
16
+ type?: "generic" | "issue" | "pull" | "repository";
17
+ url?: string;
18
+ }
19
+
20
+ export function LinkedText({ task, text }: { task: Task; text: string }) {
21
+ const segments = referenceSegments(text, {
22
+ projectRepositories: task.project.githubRepos,
23
+ taskRepository: task.relatedGitHubRepository,
24
+ }) as ReferenceSegment[];
25
+ const links = segments.filter((segment) => segment.kind === "link");
26
+ const displayLabels = githubReferenceDisplayLabels(links) as string[];
27
+ let linkIndex = 0;
28
+ return segments.map((segment, index) => {
29
+ if (segment.kind === "text") return segment.text;
30
+ if (segment.kind === "ambiguous") {
31
+ return (
32
+ <span
33
+ aria-label={segment.reason}
34
+ className="taskchef-github-reference-ambiguous"
35
+ key={`${index}:${segment.text}`}
36
+ tabIndex={0}
37
+ title={segment.reason}
38
+ >
39
+ {segment.text}
40
+ </span>
41
+ );
42
+ }
43
+ const displayLabel = displayLabels[linkIndex];
44
+ linkIndex += 1;
45
+ const kind = segment.type === "pull"
46
+ ? ", GitHub pull request"
47
+ : segment.type === "issue"
48
+ ? ", GitHub issue"
49
+ : segment.owner ? " on GitHub" : "";
50
+ return (
51
+ <Anchor
52
+ aria-label={`${githubReferenceAccessibleLabel(segment)}${kind} (opens in a new tab)`}
53
+ className="taskchef-github-link"
54
+ href={segment.url}
55
+ key={`${index}:${segment.url}`}
56
+ rel="noopener noreferrer"
57
+ target="_blank"
58
+ >
59
+ {displayLabel}
60
+ </Anchor>
61
+ );
62
+ });
63
+ }
@@ -3,6 +3,7 @@ import { IconExternalLink } from "@tabler/icons-react";
3
3
  import { latestTurnPresentation } from "../../state.js";
4
4
  import type { Task } from "../types";
5
5
  import { GitHubLinks } from "./GitHubLinks";
6
+ import { LinkedText } from "./LinkedText";
6
7
  import { RelativeTime } from "./RelativeTime";
7
8
  import { ShimmerText } from "./ShimmerText";
8
9
  import { StatusBadge } from "./StatusBadge";
@@ -34,12 +35,14 @@ export function TaskCard({
34
35
 
35
36
  <Box className="taskchef-summary-grid">
36
37
  <Text c="dimmed" className="taskchef-field-label" size="xs">Request</Text>
37
- <Text className="taskchef-preserve-lines" lineClamp={3} size="sm">{latest.requestSummary}</Text>
38
+ <Text className="taskchef-preserve-lines" lineClamp={3} size="sm">
39
+ <LinkedText task={task} text={latest.requestSummary} />
40
+ </Text>
38
41
  <Text c="dimmed" className="taskchef-field-label" size="xs">Result</Text>
39
42
  <Text className="taskchef-preserve-lines" lineClamp={3} size="sm">
40
43
  {latest.resultStatus === "working"
41
44
  ? <ShimmerText>{latest.resultSummary}</ShimmerText>
42
- : latest.resultSummary}
45
+ : <LinkedText task={task} text={latest.resultSummary} />}
43
46
  </Text>
44
47
  </Box>
45
48
 
@@ -1,7 +1,12 @@
1
1
  import { Box, Group, Paper, Text } from "@mantine/core";
2
2
  import { IconCoins } from "@tabler/icons-react";
3
3
  import type { Task } from "../types";
4
- import { usageView } from "../presentation";
4
+ import {
5
+ formatCompactTokens,
6
+ formatEstimatedCost,
7
+ formatFullTokens,
8
+ usageView,
9
+ } from "../presentation";
5
10
  import { ShimmerText } from "./ShimmerText";
6
11
 
7
12
  export function UsagePanel({ task }: { task: Task }) {
@@ -22,18 +27,45 @@ export function UsagePanel({ task }: { task: Task }) {
22
27
  const cached = usage.usage.cachedInputTokens ?? 0;
23
28
  const input = usage.usage.inputTokens ?? 0;
24
29
  const cacheRatio = input + cached > 0 ? `${Math.round((cached / (input + cached)) * 100)}%` : "—";
25
- const values = [
26
- [usage.knownSoFar ? "Tokens · known so far" : "Tokens", new Intl.NumberFormat().format(usage.usage.totalTokens)],
27
- ["Estimated cost", usage.usage.estimatedCostUsd == null ? "—" : `$${usage.usage.estimatedCostUsd.toFixed(4)}`],
28
- ["Model", Object.keys(usage.usage.models ?? {}).join(", ") || "—"],
29
- ["Cache ratio", cacheRatio],
30
- ] as const;
30
+ const values: Array<{
31
+ accessibleValue?: string;
32
+ label: string;
33
+ title?: string;
34
+ value: string;
35
+ }> = [
36
+ {
37
+ accessibleValue: `${formatFullTokens(usage.usage.totalTokens)} tokens`,
38
+ label: usage.knownSoFar ? "Tokens · known so far" : "Tokens",
39
+ title: `${formatFullTokens(usage.usage.totalTokens)} tokens`,
40
+ value: formatCompactTokens(usage.usage.totalTokens),
41
+ },
42
+ {
43
+ accessibleValue: usage.usage.estimatedCostUsd == null
44
+ ? "Estimated cost unavailable"
45
+ : `Estimated cost ${formatEstimatedCost(usage.usage.estimatedCostUsd)}`,
46
+ label: "Estimated cost",
47
+ title: usage.usage.estimatedCostUsd == null
48
+ ? undefined
49
+ : `Unrounded estimate: $${usage.usage.estimatedCostUsd}`,
50
+ value: formatEstimatedCost(usage.usage.estimatedCostUsd),
51
+ },
52
+ { label: "Model", value: Object.keys(usage.usage.models ?? {}).join(", ") || "—" },
53
+ { label: "Cache ratio", value: cacheRatio },
54
+ ];
31
55
  return (
32
56
  <Box aria-live="polite" className="taskchef-metrics">
33
- {values.map(([label, value]) => (
57
+ {values.map(({ accessibleValue, label, title, value }) => (
34
58
  <Paper className="taskchef-metric" key={label} p="sm" radius="md" withBorder>
35
59
  <Text c="dimmed" size="xs">{label}</Text>
36
- <Text className="taskchef-metric-value" fw={650} size="sm">{value}</Text>
60
+ <Text
61
+ aria-label={accessibleValue}
62
+ className="taskchef-metric-value"
63
+ fw={650}
64
+ size="sm"
65
+ title={title}
66
+ >
67
+ {value}
68
+ </Text>
37
69
  </Paper>
38
70
  ))}
39
71
  </Box>
@@ -1,10 +1,17 @@
1
1
  import { MantineProvider } from "@mantine/core";
2
- import { render, screen } from "@testing-library/react";
2
+ import { render, screen, within } from "@testing-library/react";
3
3
  import { describe, expect, test } from "vitest";
4
4
  import { fixtureTask } from "./fixtures";
5
- import { turnUsageView, usageStillCalculating, usageView } from "./presentation";
5
+ import {
6
+ formatCompactTokens,
7
+ formatEstimatedCost,
8
+ turnUsageView,
9
+ usageStillCalculating,
10
+ usageView,
11
+ } from "./presentation";
6
12
  import { ShimmerText } from "./components/ShimmerText";
7
13
  import { GitHubLinks } from "./components/GitHubLinks";
14
+ import { LinkedText } from "./components/LinkedText";
8
15
  import { UsagePanel } from "./components/UsagePanel";
9
16
 
10
17
  describe("token and working presentation", () => {
@@ -21,7 +28,7 @@ describe("token and working presentation", () => {
21
28
  status: "available",
22
29
  task: { totalTokens: 12345, estimatedCostUsd: 0.1234 },
23
30
  },
24
- })).label).toBe("12,345 tokens · Estimated cost $0.1234");
31
+ })).label).toBe("12.35K tokens · Estimated cost $0.12");
25
32
  expect(usageView(fixtureTask({ status: "completed", usage: { status: "unavailable" } })).label)
26
33
  .toBe("Token usage unavailable");
27
34
  expect(usageView(fixtureTask({
@@ -43,7 +50,7 @@ describe("token and working presentation", () => {
43
50
  },
44
51
  }));
45
52
  expect(view.kind).toBe("ready");
46
- expect(view.label).toBe("12,345 tokens · Estimated cost $0.1234 · known so far");
53
+ expect(view.label).toBe("12.35K tokens · Estimated cost $0.12 · known so far");
47
54
  if (view.kind === "ready") expect(view.knownSoFar).toBe(true);
48
55
  });
49
56
 
@@ -64,7 +71,7 @@ describe("token and working presentation", () => {
64
71
  updatedAt: task.updatedAt,
65
72
  },
66
73
  };
67
- expect(turnUsageView(task, completedTurn).label).toBe("330 tokens · Estimated cost $0.1200");
74
+ expect(turnUsageView(task, completedTurn).label).toBe("330 tokens · Estimated cost $0.12");
68
75
  expect(turnUsageView(task, { ...completedTurn, result: null }).label).toBe("Turn usage pending");
69
76
  expect(turnUsageView({
70
77
  ...task,
@@ -115,7 +122,7 @@ describe("token and working presentation", () => {
115
122
  );
116
123
 
117
124
  expect(screen.getByText("Tokens").nextSibling).toHaveTextContent("330");
118
- expect(screen.getByText("Estimated cost").nextSibling).toHaveTextContent("$0.1200");
125
+ expect(screen.getByText("Estimated cost").nextSibling).toHaveTextContent("$0.12");
119
126
  expect(screen.getByText("Model").nextSibling).toHaveTextContent("gpt-5.6-sol, gpt-5.6-luna");
120
127
  expect(screen.getByText("Cache ratio").nextSibling).toHaveTextContent("67%");
121
128
  expect(screen.queryByText(/API-equivalent/i)).not.toBeInTheDocument();
@@ -125,4 +132,110 @@ describe("token and working presentation", () => {
125
132
  render(<MantineProvider><GitHubLinks task={fixtureTask()} /></MantineProvider>);
126
133
  expect(screen.getByRole("link")).toHaveClass("taskchef-github-link");
127
134
  });
135
+
136
+ test("formats compact token boundaries without scientific notation", () => {
137
+ expect([
138
+ 0,
139
+ 999,
140
+ 1_000,
141
+ 1_100,
142
+ 12_345,
143
+ 999_999,
144
+ 1_320_000,
145
+ 1_000_000_000,
146
+ ].map((value) => formatCompactTokens(value, "en-US"))).toEqual([
147
+ "0",
148
+ "999",
149
+ "1K",
150
+ "1.1K",
151
+ "12.35K",
152
+ "1M",
153
+ "1.32M",
154
+ "1B",
155
+ ]);
156
+ });
157
+
158
+ test("formats estimates to two decimals across zero, sub-cent, missing, and large values", () => {
159
+ expect(formatEstimatedCost(0, "en-US")).toBe("$0.00");
160
+ expect(formatEstimatedCost(0.0049, "en-US")).toBe("$0.00");
161
+ expect(formatEstimatedCost(12.344, "en-US")).toBe("$12.34");
162
+ expect(formatEstimatedCost(12.345, "en-US")).toBe("$12.35");
163
+ expect(formatEstimatedCost(null, "en-US")).toBe("—");
164
+ expect(formatEstimatedCost(Number.NaN, "en-US")).toBe("—");
165
+ expect(formatEstimatedCost(1_234_567_890.125, "en-US")).toBe("$1,234,567,890.13");
166
+ });
167
+
168
+ test("exposes full token precision and the unrounded estimate on ready metric cards", () => {
169
+ render(
170
+ <MantineProvider>
171
+ <UsagePanel task={fixtureTask({
172
+ status: "completed",
173
+ usage: {
174
+ status: "available",
175
+ task: { totalTokens: 1_324_567, estimatedCostUsd: 12.3449 },
176
+ },
177
+ })} />
178
+ </MantineProvider>,
179
+ );
180
+ expect(screen.getByLabelText("1,324,567 tokens")).toHaveTextContent("1.32M");
181
+ expect(screen.getByLabelText("1,324,567 tokens")).toHaveAttribute("title", "1,324,567 tokens");
182
+ expect(screen.getByLabelText("Estimated cost $12.34")).toHaveTextContent("$12.34");
183
+ expect(screen.getByLabelText("Estimated cost $12.34")).toHaveAttribute(
184
+ "title",
185
+ "Unrounded estimate: $12.3449",
186
+ );
187
+ });
188
+
189
+ test("groups, deduplicates, and naturally sorts compact related links by repository", () => {
190
+ const task = fixtureTask({
191
+ relatedGitHubLinks: [
192
+ ["taskchef", "83", "pull"],
193
+ ["guzuoshou-workspace", "124", "issue"],
194
+ ["taskchef", "79", "issue"],
195
+ ["guzuoshou-workspace", "108", "issue"],
196
+ ["taskchef", "80", "issue"],
197
+ ["guzuoshou-workspace", "109", "pull"],
198
+ ["taskchef", "79", "pull"],
199
+ ].map(([repository, number, type]) => ({
200
+ label: `favoyang/${repository}#${number}`,
201
+ number,
202
+ owner: "favoyang",
203
+ repository,
204
+ type: type as "issue" | "pull",
205
+ url: `https://github.com/favoyang/${repository}/${type === "pull" ? "pull" : "issues"}/${number}`,
206
+ })),
207
+ });
208
+ const { container } = render(<MantineProvider><GitHubLinks task={task} /></MantineProvider>);
209
+ const groups = container.querySelectorAll(".taskchef-github-group");
210
+ expect(groups).toHaveLength(2);
211
+ expect(within(groups[0] as HTMLElement).getAllByRole("link").map((link) => link.textContent))
212
+ .toEqual(["taskchef #79", "#80", "#83"]);
213
+ expect(within(groups[1] as HTMLElement).getAllByRole("link").map((link) => link.textContent))
214
+ .toEqual(["guzuoshou-workspace #108", "#109", "#124"]);
215
+ const first = screen.getByRole("link", { name: /favoyang\/taskchef Issue #79/ });
216
+ expect(first).toHaveAttribute("href", "https://github.com/favoyang/taskchef/issues/79");
217
+ expect(first).toHaveAttribute("target", "_blank");
218
+ expect(first).toHaveAttribute("rel", "noopener noreferrer");
219
+ expect(screen.queryByText(/PR #|Issue #/)).not.toBeInTheDocument();
220
+ });
221
+
222
+ test("keeps established inline request and result link labels", () => {
223
+ const task = fixtureTask({
224
+ project: {
225
+ name: "TaskChef Preview",
226
+ path: "/tmp/taskchef-preview-project",
227
+ githubRepos: ["https://github.com/favoyang/taskchef"],
228
+ },
229
+ relatedGitHubRepository: "favoyang/taskchef",
230
+ });
231
+ const { container } = render(
232
+ <MantineProvider>
233
+ <div>
234
+ <LinkedText task={task} text="Review https://github.com/favoyang/taskchef/pull/83." />
235
+ </div>
236
+ </MantineProvider>,
237
+ );
238
+ expect(within(container).getByRole("link", { name: /favoyang\/taskchef PR #83/ }))
239
+ .toHaveTextContent("taskchef PR #83");
240
+ });
128
241
  });
@@ -1,5 +1,27 @@
1
1
  import type { Task, UsageNumbers, UsageProjection } from "./types";
2
2
 
3
+ export function formatCompactTokens(value: number, locales?: Intl.LocalesArgument) {
4
+ return new Intl.NumberFormat(locales, {
5
+ maximumFractionDigits: 2,
6
+ notation: "compact",
7
+ }).format(value);
8
+ }
9
+
10
+ export function formatFullTokens(value: number, locales?: Intl.LocalesArgument) {
11
+ return new Intl.NumberFormat(locales, { maximumFractionDigits: 2 }).format(value);
12
+ }
13
+
14
+ export function formatEstimatedCost(value: number | null | undefined, locales?: Intl.LocalesArgument) {
15
+ if (typeof value !== "number" || !Number.isFinite(value)) return "—";
16
+ return new Intl.NumberFormat(locales, {
17
+ currency: "USD",
18
+ currencyDisplay: "narrowSymbol",
19
+ maximumFractionDigits: 2,
20
+ minimumFractionDigits: 2,
21
+ style: "currency",
22
+ }).format(value);
23
+ }
24
+
3
25
  export type UsageView =
4
26
  | { kind: "pending"; label: "Token usage pending" }
5
27
  | { kind: "calculating"; label: "Calculating token usage" }
@@ -29,10 +51,10 @@ export function usageView(task: Task): UsageView {
29
51
  }
30
52
 
31
53
  function readyUsageView(usage: UsageNumbers, knownSoFar: boolean): UsageView {
32
- const tokens = new Intl.NumberFormat().format(usage.totalTokens);
54
+ const tokens = formatCompactTokens(usage.totalTokens);
33
55
  const cost = usage.estimatedCostUsd == null
34
56
  ? "Estimated cost unavailable"
35
- : `Estimated cost $${usage.estimatedCostUsd.toFixed(4)}`;
57
+ : `Estimated cost ${formatEstimatedCost(usage.estimatedCostUsd)}`;
36
58
  return {
37
59
  kind: "ready",
38
60
  knownSoFar,
@@ -49,10 +71,10 @@ export function turnUsageView(task: Task, turn: NonNullable<Task["turns"]>[numbe
49
71
  return { kind: "calculating" as const, label: "Calculating turn usage" };
50
72
  }
51
73
  if (usage?.status === "available" && typeof usage.totalTokens === "number") {
52
- const tokens = new Intl.NumberFormat().format(usage.totalTokens);
74
+ const tokens = formatFullTokens(usage.totalTokens);
53
75
  const cost = usage.estimatedCostUsd == null
54
76
  ? "Estimated cost unavailable"
55
- : `Estimated cost $${usage.estimatedCostUsd.toFixed(4)}`;
77
+ : `Estimated cost ${formatEstimatedCost(usage.estimatedCostUsd)}`;
56
78
  return { kind: "ready" as const, label: `${tokens} tokens · ${cost}` };
57
79
  }
58
80
  return {
@@ -48,6 +48,11 @@ body {
48
48
  .taskchef-preserve-lines { white-space: pre-wrap; overflow-wrap: anywhere; }
49
49
  .taskchef-github-link { color: var(--taskchef-accent); text-decoration: none; }
50
50
  .taskchef-github-link:hover { text-decoration: underline; text-underline-offset: 3px; }
51
+ .taskchef-github-link:focus-visible { outline: 2px solid var(--mantine-color-teal-6); outline-offset: 2px; border-radius: 2px; }
52
+ .taskchef-github-groups { display: grid; min-width: 0; gap: 3px; }
53
+ .taskchef-github-group { min-width: 0; }
54
+ .taskchef-github-reference-ambiguous { border-bottom: 2px dotted var(--mantine-color-yellow-7); cursor: help; }
55
+ .taskchef-github-reference-ambiguous:focus-visible { outline: 2px solid var(--mantine-color-yellow-7); outline-offset: 2px; }
51
56
  .taskchef-time { display: inline-flex; align-items: center; gap: 4px; width: fit-content; padding: 2px 3px; border-radius: 4px; color: var(--taskchef-muted); font-size: 0.7rem; line-height: 1.2; }
52
57
  .taskchef-time:hover { color: var(--taskchef-text); background: var(--taskchef-surface-muted); }
53
58
  .taskchef-time:focus-visible { outline: 2px solid var(--mantine-color-teal-6); outline-offset: 2px; }