Skip to content

Commit c65cf29

Browse files
committed
feat(web): complete tooltip migration inventory
1 parent 67682d1 commit c65cf29

11 files changed

Lines changed: 127 additions & 36 deletions

File tree

packages/web/src/components/ui/MIGRATION.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
| Modal | 🟢 complete | `.modal-overlay .modal-card .modal-*` | 0 | 2026-05-09 |
1717
| ConfirmDialog | 🟡 partial | modal convenience wrapper | richer confirm/auth flows remain on raw `Modal` | 2026-05-09 |
1818
| Toast | 🟢 complete | `.toast*` | 0 | 2026-05-09 |
19-
| Tooltip | 🟡 partial | native `title` hover labels | branch picker, code-editor actions, file-tree actions, git-diff close, fullscreen, topbar actions, connection-status, git status-strip branch, session/supervisor actions, selected settings actions, and workspace/terminal file-toolbar actions covered; truncation/path and other non-action long-text titles remain deferred | 2026-05-09 |
19+
| Tooltip | 🟢 complete | native `title` hover labels | 0 | 2026-05-09 |
2020
| ProgressBar | 🟢 complete | `--progress-height` patterns | 0 | 2026-05-09 |
2121
| Notice | 🟢 complete | `.settings-page__notice*` | 0 | 2026-05-09 |
2222
| EmptyState | 🟡 partial | feature-specific empty state blocks | centered shared shells plus workspace desktop/mobile no-session or no-workspace empties, agent-panes no-workspace, topbar no-workspace, workspace route resolving/load-failed shells, worktree detail no-diff and empty-tree states, worktree manager list-empty, workspace launch directory-empty, command palette no-results, desktop branch quick-pick loading/idle/filter-empty states, shared git-panel compact worktree/change/history shells, file-tree-panel loading/search-empty/tree fallback shells, the mobile select empty shell, and the mobile supervisor enable shell covered; additional list-style and other feature-owned empty blocks remain | 2026-05-09 |
@@ -63,7 +63,7 @@
6363

6464
`Switch` now completes the bounded boolean-toggle inventory: the settings notifications toggles use the shared primitive from the public UI barrel, and no additional feature-local switch implementations remain in the selected scope.
6565

66-
`Tooltip` now covers a broader bounded action-trigger batch: branch picker, topbar/workspace fullscreen, selected code-editor actions, file-tree delete/create actions, git-diff close, connection-status hover copy, the git status-strip branch trigger, session/supervisor actions, the settings config-editor format action, workspace desktop/mobile file-toolbar actions, and terminal toolbar open/close actions all use the shared primitive from the public UI barrel. The shared primitive also preserves hover help for real disabled button triggers so these migrations do not regress existing affordances. Deferred tooltip-like families such as truncation/path and other non-action long-text titles remain intentionally outside this slice.
66+
`Tooltip` now completes the feature-layer native hover-label migration inventory: the earlier action-trigger adoptions plus the remaining file-tree path labels, git history subjects, workspace tab path labels, settings config path labels, and supervisor objective text all use the shared primitive from the public UI barrel instead of native `title` attributes. The shared primitive also preserves hover help for real disabled button triggers so these migrations do not regress existing affordances, and no feature-local native `title` hover-label callsites remain.
6767

6868
`Sheet` now completes the mobile sheet-shell migration inventory: the mobile workspace files and terminal fullscreen sheets, mobile supervisor flows, mobile select sheet presentation, mobile worktree manager surface, workspace launch modal, worktree modal, and command palette all use the shared primitive from the public UI barrel while preserving the existing `mobile-sheet*` compatibility classes and caller-owned body/content modifiers.
6969

packages/web/src/features/settings/components/config-editor.test.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,19 @@ function renderConfigEditor(options?: {
6262
}
6363

6464
describe("ConfigEditor", () => {
65-
it("preserves the full config path in the header title when the visible path is truncated", async () => {
65+
it("preserves the full config path in the header tooltip when the visible path is truncated", async () => {
6666
renderConfigEditor();
6767

68-
const path = await screen.findByTitle("/home/spencer/.claude/settings.json");
68+
const path = await screen.findByText("/home/spencer/.claude/settings.json");
6969

7070
expect(path).toHaveClass("config-card-path");
7171
expect(path).toHaveTextContent("/home/spencer/.claude/settings.json");
72+
expect(path).not.toHaveAttribute("title");
73+
74+
fireEvent.mouseEnter(path);
75+
const tooltip = await screen.findByRole("tooltip");
76+
expect(tooltip).toHaveTextContent("/home/spencer/.claude/settings.json");
77+
expect(path).toHaveAttribute("aria-describedby", tooltip.getAttribute("id") ?? "");
7278

7379
await waitFor(() => {
7480
expect(screen.getByTestId("monaco-host")).toBeInTheDocument();

packages/web/src/features/settings/components/config-editor.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -264,9 +264,9 @@ export function ConfigEditor({
264264
<div className="config-card-header" onClick={handleToggle}>
265265
<div className="config-card-title">
266266
<FileJson2 size={16} />
267-
<span className="config-card-path" title={configPath}>
268-
{configPath}
269-
</span>
267+
<Tooltip content={configPath}>
268+
<span className="config-card-path">{configPath}</span>
269+
</Tooltip>
270270
</div>
271271
<div className="config-card-header-right">
272272
{isExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}

packages/web/src/features/supervisor/components/supervisor-card.test.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,29 @@ describe("SupervisorCard", () => {
128128
);
129129
});
130130

131+
it("uses the shared tooltip for the supervisor objective text", () => {
132+
const store = createStore();
133+
window.localStorage.setItem("ui.locale", JSON.stringify("en"));
134+
store.set(localeAtom, "en");
135+
store.set(wsClientAtom, { sendCommand: vi.fn() } as never);
136+
store.set(supervisorsAtom, new Map([["sess-1", createSupervisor()]]));
137+
store.set(supervisorCyclesAtom, new Map());
138+
139+
render(
140+
<Provider store={store}>
141+
<SupervisorCard sessionId="sess-1" workspaceId="ws-1" />
142+
</Provider>
143+
);
144+
145+
const objective = screen.getByText("Finish the server refactor");
146+
expect(objective).not.toHaveAttribute("title");
147+
148+
fireEvent.mouseEnter(objective);
149+
const tooltip = screen.getByRole("tooltip");
150+
expect(tooltip).toHaveTextContent("Finish the server refactor");
151+
expect(objective).toHaveAttribute("aria-describedby", tooltip.getAttribute("id") ?? "");
152+
});
153+
131154
it('shows "No guidance injected this cycle" for a completed cycle with no result and no errorReason', () => {
132155
const sendCommand = vi.fn().mockResolvedValue(undefined);
133156
const store = createStore();

packages/web/src/features/supervisor/views/shared/supervisor-card.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,9 @@ export function SupervisorCard({ sessionId, workspaceId }: SupervisorCardProps)
115115
</div>
116116

117117
<div className="supervisor-objective-row" onDoubleClick={() => openDialog("edit")}>
118-
<span className="supervisor-objective-text" title={supervisor.objective}>
119-
{supervisor.objective}
120-
</span>
118+
<Tooltip content={supervisor.objective}>
119+
<span className="supervisor-objective-text">{supervisor.objective}</span>
120+
</Tooltip>
121121
<span className="supervisor-provider-pill">{supervisor.evaluatorProviderId}</span>
122122
</div>
123123

packages/web/src/features/topbar/components/tab.test.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ describe("WorkspaceTab", () => {
7070
);
7171
}
7272

73-
it("renders the folder basename when workspace name is a full path", () => {
73+
it("renders the folder basename and uses a shared tooltip for the full path", () => {
7474
const workspace = {
7575
...createWorkspace("ws-2", "/home/spencer/workspace/coder-studio"),
7676
name: "/home/spencer/workspace/coder-studio",
@@ -80,11 +80,17 @@ describe("WorkspaceTab", () => {
8080

8181
renderWorkspaceTab(store, workspace);
8282

83-
expect(screen.getByRole("tab", { name: "coder-studio" })).toHaveAttribute(
84-
"title",
85-
"/home/spencer/workspace/coder-studio"
86-
);
83+
const tab = screen.getByRole("tab", { name: "coder-studio" });
84+
const label = screen.getByText("coder-studio");
85+
86+
expect(tab).not.toHaveAttribute("title");
87+
expect(label).not.toHaveAttribute("title");
8788
expect(screen.queryByText("/home/spencer/workspace/coder-studio")).toBeNull();
89+
90+
fireEvent.mouseEnter(label);
91+
const tooltip = screen.getByRole("tooltip");
92+
expect(tooltip).toHaveTextContent("/home/spencer/workspace/coder-studio");
93+
expect(label).toHaveAttribute("aria-describedby", tooltip.getAttribute("id") ?? "");
8894
});
8995

9096
it("sets the active workspace without navigating when a tab is clicked", () => {

packages/web/src/features/topbar/components/tab.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { useSetAtom } from "jotai";
1010
import { X } from "lucide-react";
1111
import type { FC } from "react";
1212
import { activeWorkspaceIdAtom } from "../../../atoms/workspaces";
13-
import { Badge, IconButton, Tab } from "../../../components/ui";
13+
import { Badge, IconButton, Tab, Tooltip } from "../../../components/ui";
1414
import { useTranslation } from "../../../lib/i18n";
1515
import { formatWorkspaceLabel } from "../../notifications/format";
1616
import { useWorkspaceCloseAction } from "../../workspace/actions/use-workspace-close-action";
@@ -46,14 +46,11 @@ export const WorkspaceTab: FC<WorkspaceTabProps> = ({ workspace, isActive }) =>
4646

4747
return (
4848
<div className={`topbar-tab-shell ${isActive ? "active" : ""}`} role="presentation">
49-
<Tab
50-
className="topbar-tab"
51-
onClick={handleClick}
52-
title={workspace.path || workspace.id}
53-
value={workspace.id}
54-
>
49+
<Tab className="topbar-tab" onClick={handleClick} value={workspace.id}>
5550
<span className={`topbar-dot ${workspace.isActive ? "active" : "idle"}`} />
56-
<span className="topbar-tab-name">{displayName}</span>
51+
<Tooltip content={workspace.path || workspace.id}>
52+
<span className="topbar-tab-name">{displayName}</span>
53+
</Tooltip>
5754
<Badge count={workspace.unreadCount ?? 0} max={9} />
5855
</Tab>
5956
<IconButton

packages/web/src/features/workspace/views/shared/file-tree-panel.test.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -827,7 +827,7 @@ describe("FileTreePanel", () => {
827827
expect(await screen.findByRole("dialog")).toBeInTheDocument();
828828
});
829829

830-
it("uses shared tooltips for file-tree action triggers while preserving row path titles", () => {
830+
it("uses shared tooltips for file-tree action triggers and row path labels", () => {
831831
const store = createStore();
832832
store.set(wsClientAtom, { sendCommand: vi.fn().mockResolvedValue({}) } as never);
833833
store.set(
@@ -871,11 +871,12 @@ describe("FileTreePanel", () => {
871871
expect(newFolderButton).not.toHaveAttribute("title");
872872
expect(deleteDirectoryButton).not.toHaveAttribute("title");
873873
expect(deleteFileButton).not.toHaveAttribute("title");
874-
expect(screen.getByText("src").closest(".tree-item")).toHaveAttribute("title", "src");
875-
expect(screen.getByText("app.tsx").closest(".tree-item")).toHaveAttribute(
876-
"title",
877-
"src/app.tsx"
878-
);
874+
const directoryLabel = screen.getByText("src");
875+
const fileLabel = screen.getByText("app.tsx");
876+
expect(directoryLabel).not.toHaveAttribute("title");
877+
expect(fileLabel).not.toHaveAttribute("title");
878+
expect(directoryLabel.closest(".tree-item")).not.toHaveAttribute("title");
879+
expect(fileLabel.closest(".tree-item")).not.toHaveAttribute("title");
879880

880881
fireEvent.mouseEnter(newFileButton);
881882
expect(screen.getByRole("tooltip")).toHaveTextContent("file.new_file");
@@ -891,6 +892,14 @@ describe("FileTreePanel", () => {
891892
fireEvent.mouseLeave(deleteDirectoryButton);
892893
fireEvent.mouseEnter(deleteFileButton);
893894
expect(screen.getByRole("tooltip")).toHaveTextContent("file.delete");
895+
896+
fireEvent.mouseLeave(deleteFileButton);
897+
fireEvent.mouseEnter(directoryLabel);
898+
expect(screen.getByRole("tooltip")).toHaveTextContent("src");
899+
900+
fireEvent.mouseLeave(directoryLabel);
901+
fireEvent.mouseEnter(fileLabel);
902+
expect(screen.getByRole("tooltip")).toHaveTextContent("src/app.tsx");
894903
});
895904

896905
it("keeps expanded directories populated after refreshing the file tree", async () => {

packages/web/src/features/workspace/views/shared/file-tree-panel.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,6 @@ const FileSearchResultRow: FC<FileSearchResultRowProps> = ({
252252
className={`tree-item ${selectedPath === node.path ? "selected" : ""}`}
253253
onClick={() => onSelectFile(node.path)}
254254
style={{ paddingLeft: 12 }}
255-
title={node.path}
256255
>
257256
<span className="tree-chevron" aria-hidden="true" />
258257

@@ -261,8 +260,14 @@ const FileSearchResultRow: FC<FileSearchResultRowProps> = ({
261260
</span>
262261

263262
<span className="tree-search-labels">
264-
<span className="tree-label">{node.name}</span>
265-
{dirName ? <span className="tree-search-path">{dirName}</span> : null}
263+
<Tooltip content={node.path}>
264+
<span className="tree-label">{node.name}</span>
265+
</Tooltip>
266+
{dirName ? (
267+
<Tooltip content={node.path}>
268+
<span className="tree-search-path">{dirName}</span>
269+
</Tooltip>
270+
) : null}
266271
</span>
267272

268273
<div className="tree-item-actions">
@@ -352,7 +357,6 @@ const FileTreeNode: FC<FileTreeNodeProps> = ({
352357
className={`tree-item ${selectedPath === node.path ? "selected" : ""}`}
353358
onClick={handleClick}
354359
style={{ paddingLeft }}
355-
title={node.path}
356360
>
357361
<span className="tree-chevron" aria-hidden="true">
358362
{isFolder ? isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} /> : null}
@@ -362,7 +366,9 @@ const FileTreeNode: FC<FileTreeNodeProps> = ({
362366
<Icon size={14} />
363367
</span>
364368

365-
<span className="tree-label">{node.name}</span>
369+
<Tooltip content={node.path}>
370+
<span className="tree-label">{node.name}</span>
371+
</Tooltip>
366372

367373
<div className="tree-item-actions">
368374
{isFolder ? (

packages/web/src/features/workspace/views/shared/git-panel.test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,49 @@ describe("GitPanel", () => {
675675
});
676676
});
677677

678+
it("uses the shared tooltip for long history subjects instead of native titles", async () => {
679+
const sendCommand = vi.fn().mockImplementation(async (op: string) => {
680+
if (op === "git.status") {
681+
return status;
682+
}
683+
684+
if (op === "git.branches") {
685+
return { current: "feature/ai-agent", branches: [] };
686+
}
687+
688+
if (op === "worktree.list") {
689+
return { worktrees: [] };
690+
}
691+
692+
if (op === "git.log") {
693+
return { entries: historyEntries };
694+
}
695+
696+
return {};
697+
});
698+
const store = createStore();
699+
store.set(localeAtom, "en");
700+
store.set(wsClientAtom, { sendCommand } as never);
701+
seedWorkspaceStore(store);
702+
703+
render(
704+
<Provider store={store}>
705+
<GitPanel workspaceId="ws-test" />
706+
</Provider>
707+
);
708+
709+
fireEvent.click(await screen.findByRole("button", { name: "History" }));
710+
711+
const subject = await screen.findByText("feat: refresh source control surface");
712+
expect(subject).not.toHaveAttribute("title");
713+
expect(subject.closest(".git-history-row")).not.toHaveAttribute("title");
714+
715+
fireEvent.mouseEnter(subject);
716+
const tooltip = screen.getByRole("tooltip");
717+
expect(tooltip).toHaveTextContent("feat: refresh source control surface");
718+
expect(subject).toHaveAttribute("aria-describedby", tooltip.getAttribute("id") ?? "");
719+
});
720+
678721
it("renders the shared commit textarea styling in the updated panel shell", async () => {
679722
const sendCommand = vi.fn().mockImplementation(async (op: string) => {
680723
if (op === "git.status") {

0 commit comments

Comments
 (0)