Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/dashboard-overview-focus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@gemstack/the-framework": minor
---

Rework the dashboard Overview around what matters at a glance (#1139).

Usage moves to the top as the first thing you see. The two cross-project queues now sit side by side: a Human Queue ("Agents awaiting your approval, review, or input") and an AI Queue ("Tasks AI will work on next"), the latter showing every queued item in full as bullet points rather than collapsing to "+N more". A new Agents view lists the sessions working now and the ones just finished in two columns — Current and Recent — each row clickable straight into its session and dated in plain language ("22s ago", "2w ago") with the exact moment on hover. Hot tickets narrows to the three lanes worth surfacing — In progress, AI Queue (tickets an open `TODO_AGENTS.md` entry links to), and High priority — in a two-column layout.

The redundant cards are gone: the KPI tiles, run outcomes, the projects table, and the "Overview" title with its tagline. Session activity is removed for now, to return later.
6 changes: 6 additions & 0 deletions .the-framework/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# The Framework: the committed project DB; session state is transient.
*
!.gitignore
!LOGS.md
!conversations/
!conversations/**
26 changes: 26 additions & 0 deletions .the-framework/LOGS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# The Framework logs

## 2026-07-25T20:45:36.937Z · prompt · Open TODO_AGENTS.md and work on the FIRST open entry only, then check it off. Do not start any other entry.

- status: stopped
- run: 2026-07-25T20-21-58-507Z
- branch: the-framework/improve-tooltip

## 2026-07-25T21:02:56.804Z · prompt · This session was interrupted when The Framework restarted, not by anyone asking you to stop. Look at what you had already done, then carry on from there.

- status: stopped
- run: 2026-07-25T20-21-58-507Z
- branch: the-framework/improve-tooltip

## 2026-07-25T21:05:44.591Z · prompt · This session was interrupted when The Framework restarted, not by anyone asking you to stop. Look at what you had already done, then carry on from there.

- status: stopped
- run: 2026-07-25T20-21-58-507Z
- branch: the-framework/improve-tooltip

## 2026-07-25T21:16:10.488Z · prompt · This session was interrupted when The Framework restarted, not by anyone asking you to stop. Look at what you had already done, then carry on from there.

- status: done
- run: 2026-07-25T20-21-58-507Z
- session: [45015d11-755b-423c-ae71-8dbfd54f7cce](https://claude.ai/code)
- branch: the-framework/improve-tooltip
40 changes: 40 additions & 0 deletions .the-framework/conversations/2026-07-25T20-21-58-507Z.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Conversation 2026-07-25T20-21-58-507Z

## 2026-07-25T20:21:59.473Z · user · dashboard

Open TODO_AGENTS.md and work on the FIRST open entry only, then check it off. Do not start any other entry.

## 2026-07-25T20:46:05.845Z · user · cli

This session was interrupted when The Framework restarted, not by anyone asking you to stop. Look at what you had already done, then carry on from there.

## 2026-07-25T21:04:00.490Z · user · cli

This session was interrupted when The Framework restarted, not by anyone asking you to stop. Look at what you had already done, then carry on from there.

## 2026-07-25T21:07:31.013Z · user · cli

This session was interrupted when The Framework restarted, not by anyone asking you to stop. Look at what you had already done, then carry on from there.

## 2026-07-25T21:16:07.419Z · agent · cli

Picked the session back up where the restart cut it off, verified the work, and finished it.

**What was already in place** (from before the interrupt) — I reviewed all of it rather than trusting it:
- `components/ui/tooltip.tsx`: `TooltipTrigger` now defaults to `delay={0}`. Base UI reads the delay off the trigger and an explicit trigger delay beats a provider's, so this one default makes every tooltip instant; the two `delay={300}` providers (`ProjectActions`, `PreviewBar`) were removed as dead config.
- ~40 native `title=` hints across 30 components converted to the custom tooltip; the popup got `role="tooltip"` so assistive tech and tests can find it.
- Tests that used to read a `title` attribute now hover, via a new `hoverTooltip()` helper in `packages/framework-dashboard/test-utils.ts`.

**What I did this turn:**
- Confirmed the exact button in the issue's "bad tooltip" screenshot — I fetched both images from the ticket; the system tooltip shown is **"Open in editor"** in `WorkspaceActions`, which is converted. That also confirms the earlier scoping call (convert interactive elements and indicators; leave `title` as an overflow fallback on truncated text and as a `Section`/`Dialog` prop) matches what the user actually complained about.
- Verified the remaining `title=` hits are all either component props, truncation fallbacks, or the unused vendored `SidebarRail`.
- Checked that dropped titles only duplicated already-visible text (`NotificationsMenu` descriptions, the onboarding button label, editor menu rows) — no information lost.
- Checked Base UI's `useDismiss({ referencePress: true })`, so a tooltip on a dropdown trigger closes on click instead of sitting over the open menu.
- `pnpm typecheck` clean; full repo `pnpm test` green — 21/21 turbo tasks, dashboard 475 tests, the-framework 1327.
- Recorded a `## Status` section on `tickets/2026-07-25_improve-tooltip.md` (OP items done, thread's design follow-ups still open — so the ticket stays open) and checked off the queue entry.
- Committed as `fae7f25 [AI] Tooltips: no delay, and the custom one everywhere`.

Only `ANALYSIS_RESULT.md` is left uncommitted — it's a session artifact and has never been committed on any branch.

```ready-for-merge
```
8 changes: 8 additions & 0 deletions ANALYSIS_RESULT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# ANALYSIS_RESULT.md

- **Ambiguous prompt**: NO — the prompt says to work the first open `TODO_AGENTS.md` entry only: the tooltip ticket ([tickets/2026-07-25_improve-tooltip.md](tickets/2026-07-25_improve-tooltip.md)), scoped to the two items in the ticket's OP (no delay + custom tooltip everywhere). The thread's design follow-ups are explicitly out of scope.
- **Scope**: small — one focused UI refactor in `packages/framework-dashboard`. Wider than the TODO's estimate (~40 native `title=` tooltips found, not ~16), but it is one mechanical pattern applied repeatedly; no plan file needed.
- **Variability**: no low-rated problems, so no choices to await:
- *No delay* (rating 9): Base UI puts `delay` on the **Trigger** (default 600 ms), and an explicit trigger `delay` beats the provider's (`delay ?? providerDelay ?? default`). Defaulting `delay={0}` in the shared `TooltipTrigger` wrapper (`components/ui/tooltip.tsx`) makes every tooltip instant with one change — including the ~40 new sites, which have no `TooltipProvider` around them. The two `delay={300}` providers become dead config and are removed. The per-site alternative (edit the two providers) would not cover the new sites, so it is not a real contender.
- *Which `title=` to convert* (rating 7): convert informative tooltips on interactive elements and indicator icons (what the ticket complains about); drop `title`s that duplicate already-visible text (nothing to show); keep component props named `title` (`Section`, `Dialog`, `ConfirmDialog`, empty states) and truncation-fallback `title`s on `truncate` spans (overflow fallback, not tooltip UI — an instant tooltip on every hover of a long path would be noise).
- *How to convert* (rating 9): the established idiom — `<Tooltip><TooltipTrigger render={<Button …/>}>…</TooltipTrigger><TooltipContent>…</TooltipContent></Tooltip>`.
2 changes: 1 addition & 1 deletion TODO_AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ The agent queue. Each unchecked entry is worked front to back, highest priority

## Priority 7

- [ ] [Improve tooltip: show it with no delay, and use it everywhere the browser's tooltip is still used](tickets/2026-07-25_improve-tooltip.md) — only the two items in the ticket's OP. (1) `TooltipProvider` is set to `delay={300}` in `packages/framework-dashboard/components/ProjectActions.tsx:11` and `PreviewBar.tsx:75`; the ticket asks for no delay. (2) Roughly 16 interactive elements still carry a native `title=` attribute (`SessionActionsMenu.tsx:133`, `AgentModelMenu.tsx:65`, `ThemeToggle.tsx:32`, `ConnectionIndicator.tsx:30`, …), which is the slow system tooltip the ticket is complaining about; wrap them in the custom `Tooltip` instead. Leave `title` props on `Section`/`Dialog`/`ConfirmDialog` alone — those are component props, not tooltips. **Out of scope**: the thread's follow-ups (auto-show the dropdown on hover, settings-row redesign, making `Autopilot`/`Open PR` read as labels) are design calls, so the ticket stays open.
- [x] [Improve tooltip: show it with no delay, and use it everywhere the browser's tooltip is still used](tickets/2026-07-25_improve-tooltip.md) — only the two items in the ticket's OP. (1) `TooltipProvider` is set to `delay={300}` in `packages/framework-dashboard/components/ProjectActions.tsx:11` and `PreviewBar.tsx:75`; the ticket asks for no delay. (2) Roughly 16 interactive elements still carry a native `title=` attribute (`SessionActionsMenu.tsx:133`, `AgentModelMenu.tsx:65`, `ThemeToggle.tsx:32`, `ConnectionIndicator.tsx:30`, …), which is the slow system tooltip the ticket is complaining about; wrap them in the custom `Tooltip` instead. Leave `title` props on `Section`/`Dialog`/`ConfirmDialog` alone — those are component props, not tooltips. **Out of scope**: the thread's follow-ups (auto-show the dropdown on hover, settings-row redesign, making `Autopilot`/`Open PR` read as labels) are design calls, so the ticket stays open.

## Priority 5

Expand Down
4 changes: 2 additions & 2 deletions packages/framework-dashboard/components/ActivityChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ function localDateKey(date: Date): string {
// quiet day still reads as an empty slot rather than a gap. Hovering a column names its day.
// The columns are plain divs (#948): they act on nothing, so as buttons a keyboard user
// tabbed through 14 focusable controls that did nothing on Enter. The read-out data rides
// each column's title and the group's accessible description instead.
// the hover read-out and the group's accessible description; no `title`, so the slow system
// tooltip never doubles the instant read-out (#1149).
export function ActivityChart({ data }: { data: ActivityDay[] }) {
const [hover, setHover] = useState<number | null>(null)
const max = Math.max(1, ...data.map(d => d.count))
Expand All @@ -31,7 +32,6 @@ export function ActivityChart({ data }: { data: ActivityDay[] }) {
{data.map((d, i) => (
<div
key={d.date}
title={`${d.date}: ${runs(d.count)}`}
onMouseEnter={() => setHover(i)}
onMouseLeave={() => setHover(null)}
className="flex h-full flex-1 items-end rounded-sm bg-muted/40"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, test, vi } from 'vitest'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { AgentModelMenu, type AgentOption } from './AgentModelMenu.js'
import { hoverTooltip } from '../test-utils.js'

afterEach(cleanup)

Expand Down Expand Up @@ -31,12 +32,13 @@ function renderMenu(over: Partial<Parameters<typeof AgentModelMenu>[0]> = {}) {
}

describe('AgentModelMenu tree (#658)', () => {
test('the trigger shows the current agent logo and model', () => {
test('the trigger shows the current agent logo and model', async () => {
renderMenu()
const trigger = screen.getByRole('button')
expect(trigger.querySelector('[data-testid="claude-logo"]')).toBeTruthy()
expect(trigger.textContent).toContain('Opus')
expect(trigger.getAttribute('title')).toContain('Claude Code')
// The logo is the only thing naming the agent on the trigger, so hovering has to spell it out.
expect((await hoverTooltip(trigger)).textContent).toContain('Claude Code')
})

test('picking a model within an agent sets both the agent and the model', () => {
Expand Down
35 changes: 21 additions & 14 deletions packages/framework-dashboard/components/AgentModelMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ReactNode } from 'react'
import { ChevronDown, Check } from 'lucide-react'
import { cn } from '../lib/utils.js'
import { buttonVariants } from './ui/button.js'
import { Tooltip, TooltipTrigger, TooltipContent } from './ui/tooltip.js'
import {
DropdownMenu,
DropdownMenuTrigger,
Expand Down Expand Up @@ -59,20 +60,26 @@ export function AgentModelMenu({
const currentModelLabel = modelLabel(current, model)
return (
<DropdownMenu>
<DropdownMenuTrigger
type="button"
disabled={busy}
title={`Agent: ${current?.label ?? ''} · Model: ${currentModelLabel}`}
className={cn(buttonVariants({ variant: 'ghost', size: 'sm' }), 'gap-1.5 px-2 font-normal')}
>
{current?.icon ? (
<span className="flex h-4 w-4 items-center justify-center">{current.icon}</span>
) : (
current?.label
)}
{currentModelLabel}
<ChevronDown className="h-3.5 w-3.5 opacity-70" />
</DropdownMenuTrigger>
<Tooltip>
<TooltipTrigger
render={
<DropdownMenuTrigger
type="button"
disabled={busy}
className={cn(buttonVariants({ variant: 'ghost', size: 'sm' }), 'gap-1.5 px-2 font-normal')}
/>
}
>
{current?.icon ? (
<span className="flex h-4 w-4 items-center justify-center">{current.icon}</span>
) : (
current?.label
)}
{currentModelLabel}
<ChevronDown className="h-3.5 w-3.5 opacity-70" />
</TooltipTrigger>
<TooltipContent>{`Agent: ${current?.label ?? ''} · Model: ${currentModelLabel}`}</TooltipContent>
</Tooltip>
<DropdownMenuContent align="end">
{agents.map(a => (
<DropdownMenuSub key={a.value}>
Expand Down
26 changes: 16 additions & 10 deletions packages/framework-dashboard/components/Composer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Preferences } from '@gemstack/the-framework'
import { presets } from '@gemstack/the-framework/client'
import { addProfile } from '../lib/profiles.js'
import { selectRemoteDevice } from '../lib/remote-target.js'
import { hoverTooltip } from '../test-utils.js'

// Preferences are the shared daemon store; stub them so the composer reads a fixed value.
const updatePreferences = vi.hoisted(() => vi.fn())
Expand Down Expand Up @@ -87,25 +88,29 @@ afterEach(cleanup)

const STUDIO = 'http://192.168.1.5:4200'

// The agent/model trigger wears the model it will run with — 'Default' with no preference stored —
// and names the agent on hover (#1149), where it used to carry a `title`.
const agentTrigger = () => screen.getByRole('button', { name: 'Default' })

describe('Composer (#721)', () => {
test('renders the full control row: agent/model, options gear, and the submit button', () => {
test('renders the full control row: agent/model, options gear, and the submit button', async () => {
renderComposer({ submitLabel: 'Start session' })
// Presets have a visible surface again (#948): the `/` menu stays the fast path, the
// button is the discoverable one.
expect(screen.getByRole('button', { name: /Presets/ })).toBeTruthy()
expect(screen.getByRole('button', { name: 'Session options' })).toBeTruthy()
expect(screen.getByTitle(/Agent: Claude Code/)).toBeTruthy() // the agent/model trigger
expect((await hoverTooltip(agentTrigger())).textContent).toContain('Agent: Claude Code')
// The submit button appears only once the prompt has text (#721).
fireEvent.change(screen.getByLabelText('prompt'), { target: { value: 'x' } })
expect(screen.getByRole('button', { name: /Start session/ })).toBeTruthy()
})

test('compact (#723) keeps the agent/model + options controls (#755)', () => {
test('compact (#723) keeps the agent/model + options controls (#755)', async () => {
const { onSubmit } = renderComposer({ compact: true, submitLabel: 'Start' })
// They used to be dropped here, which meant a navbar run silently used the stored agent,
// model and options with nothing on screen saying which.
expect(screen.queryByRole('button', { name: 'Session options' })).not.toBeNull()
expect(screen.queryByTitle(/Agent: Claude Code/)).not.toBeNull()
expect((await hoverTooltip(agentTrigger())).textContent).toContain('Agent: Claude Code')
// The editor + submit still work (so `/` `<` `@` `#` triggers remain live in the editor).
fireEvent.change(screen.getByLabelText('prompt'), { target: { value: 'quick run' } })
fireEvent.click(screen.getByRole('button', { name: 'Start' }))
Expand All @@ -116,24 +121,25 @@ describe('Composer (#721)', () => {
const { onSubmit } = renderComposer({ showAgentModel: false })
// An in-session composer: the session is bound to the agent it started with, so offering the
// select there would only ever rewrite the next session's default.
expect(screen.queryByTitle(/Agent:/)).toBeNull()
expect(screen.queryByRole('button', { name: 'Default' })).toBeNull()
expect(screen.getByRole('button', { name: 'Session options' })).toBeTruthy()
fireEvent.change(screen.getByLabelText('prompt'), { target: { value: 'follow-up' } })
fireEvent.click(screen.getByRole('button', { name: 'Send' }))
expect(onSubmit).toHaveBeenCalledWith('follow-up', 'build', { newSession: false })
})

test('option labels promise only what the code delivers (#801)', () => {
test('option labels promise only what the code delivers (#801)', async () => {
prefs = { onBeforeMergeableQuality: true }
renderComposer()
fireEvent.click(screen.getByRole('button', { name: 'Session options' }))
// Autopilot no longer claims to relax the maintenance stance: #556 took that section out of the
// prompt, leaving the countdown as the whole feature. Scoped to the menu: the same label is on
// the resolved-options strip (#842), which carries its own "where it came from" title.
// the resolved-options strip (#842), which explains where the value came from instead.
const menu = screen.getByRole('menu')
const autopilot = within(menu).getByText('Autopilot').closest('[title]')
expect(autopilot?.getAttribute('title')).not.toMatch(/maintenance/i)
expect(autopilot?.getAttribute('title')).toMatch(/countdown/i)
const autopilot = within(menu).getByText('Autopilot').closest('[role="menuitemcheckbox"]')!
const tip = await hoverTooltip(autopilot)
expect(tip.textContent).not.toMatch(/maintenance/i)
expect(tip.textContent).toMatch(/countdown/i)
})

test('Browser is disabled with a reason off Claude Code (#801)', () => {
Expand Down
Loading