diff --git a/README.md b/README.md index 29ad719..9f4d0bc 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ **The best [DeepSeek Harness plugin](https://www.deepseek.com/harness/) for Agent's context insights and management.** [`dsh-context`](https://www.npmjs.com/package/dsh-context) provides full context lifecycle management features. -- **Context Dashboard** — the cross-session overview above Settings on the sidebar foot: KPI band with a 7-day token/cost usage chart, activity heatmap, aggregate composition ring, and filterable session cards that jump straight into any session. +- **Context Insights page** — the cross-session overview as a first-level sidebar panel (beside Plugins and Automation tasks): KPI band with a 7-day token/cost usage chart, activity heatmap, aggregate composition ring, and filterable session cards that jump straight into any session. - **Context tab** — an UI context dashboard for DeepSeek Harness's context stats, composition, trend, events, and messages. - **Context panel** — the same dashboard as a right-sidebar tab (dsh 0.1.5-rc.1+): pick **Context** on the sidebar's guide page and the panel opens beside the chat. - **`/context` command** — the slash command shows the context model for current context composition and recent context evolution. @@ -44,16 +44,16 @@ Four surfaces, one story — what your agent is carrying, how it got there, and | Where | What you get | | --- | --- | -| **Context Dashboard** | Every session at a glance: usage, cost, cache hit, daily activity, and per-session context profiles — filtered by range, day, group, or search, one click to jump in. | +| **Context Insights page** | Every session at a glance: usage, cost, cache hit, daily activity, and per-session context profiles — filtered by range, day, group, or search, one click to jump in. | | **Context tab** | The full dashboard: stats, composition, per-request trend, events, file activity, and the agent network — in every session. | | **`/context` command** | A centered modal with the same composition and context browser, without leaving the chat. | | **Preferences card** | Per-user defaults: view placement, trend granularity & mode, File Activity sort, and more. | -## 🗂️ The Context Dashboard +## 🗂️ The Context Insights page -Click **Context Dashboard / 上下文仪表盘** at the bottom-left of the sidebar, right above **Settings**: +Click **Context Insights / 上下文洞察** in the sidebar's panel list — the same row level as **Plugins** and **Automation tasks** — and the page opens as a full center-column panel: -![Context Dashboard](https://raw.githubusercontent.com/bowenliang123/dsh-context/main/docs/context-dashboard.png) +![The Context Insights page](https://raw.githubusercontent.com/bowenliang123/dsh-context/main/docs/context-dashboard.png) | Section | The question it answers | | --- | --- | diff --git a/docs/compatibility.md b/docs/compatibility.md index 75b99b2..b0b3e88 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -81,7 +81,8 @@ The browser half rides generation-specific seats, each reached through an option | Seam | V3 (`0.1.5-rc.1`) | V4 (`0.1.7-rc.2`, `0.2.0-rc.2`) | | --- | --- | --- | | Conversation panel root | keyed `main` panel's `main.conversation` (the plugin's `conversation.view` seat hangs under it) | same as V3 | -| Right Sidebar tab + guide entry | `sidebarRightTabs` registry, `sidebar.right.pane.tab`(+`.title`) seats, guide capsule with `order`/`title`/`description`/`icon` | same as V3 (`0.1.7-rc.2`+ adds an optional `commandId` to guide entries and `bindCommands` to tab actions — additive, unused by the plugin) | +| Insight page (panel + entry) | the layout's keyed `main` slot under the `dsh-context` panel key, with the sidebar entry on the `sidebar.panellist` list under the same id — the shell owns the entry row and panel switching; both spellings probed per baseline (the `insightPage` probe) | same as V3 | +| Right Sidebar tab + guide entry | `sidebarRightTabs` registry, `sidebar.right.pane.tab`(`.title`) seats, guide capsule with `order`/`title`/`description`/`icon` | same as V3 (`0.1.7-rc.2`+ adds an optional `commandId` to guide entries and `bindCommands` to tab actions — additive, unused by the plugin) | | Preferences card seat | `settings.plugin.item` over the `settingsScope` transport | `plugins.bundle.config` over the `configForms` transport | | Session jump | `uiWorkspace.openSession` — the sidebar row click's own verb; the retired `sessions.open(id)` spelling is still served as a fallback | `uiWorkspace.openSession(target: SessionTarget)` — same verb, parameter re-spelled by the 0.1.6 selection refactor; the sessions service carries no selection verb any more | @@ -89,9 +90,9 @@ The guide capsule carries `order`, `title`, `description`, and `icon`; `0.1.5-rc The File Activity card's file-name affordance is an optional-column story: on a line with the right Sidebar it opens the file's preview tab through `ctx.sidebarRight.openResource` (the built-in Files sidebar's own path), building the `dsh-resource://file/…` address with the harness's browser-safe `fileAddressFor` (inlined by the client bundle, exactly as the Sidebar's own file types inline it). Where the preview face is absent (or refuses the address), the name keeps its original system-open behavior; the `sidebar.nav` probe pins the navigation face's spelling. -The session jump — a Context Dashboard session card or an Agent network node bringing its session forward — rides the harness's own selection verb (`openSessionVia` in `src/client/services.ts`): `uiWorkspace.openSession`, the verb the built-in sidebar's row click uses on every supported line, with the sessions service's retired `open(id)` spelling kept as the degradation path for a composition without the workspace module. Both faces are re-proved per call and a deployment serving neither (or a hostile face) swallows silently — the panel still closes, the jump just does not land. Issue #90 was exactly the retired-spelling trap: the plugin called `sessions.open` alone, which the 0.1.6 selection refactor removed from the service (the click then did nothing on `0.1.7-rc.2`), so the jump now follows the view owner first and the per-baseline `sessionNav` probe pins both spellings presence-as-declared. +The session jump — a Context Insights session card or an Agent network node bringing its session forward — rides the harness's own selection verb (`openSessionVia` in `src/client/services.ts`): `uiWorkspace.openSession`, the verb the built-in sidebar's row click uses on every supported line, with the sessions service's retired `open(id)` spelling kept as the degradation path for a composition without the workspace module. Both faces are re-proved per call and a deployment serving neither (or a hostile face) swallows silently — the verb's own navigation returns the center column to the conversation (`selectPanel(null)`), so a dead jump simply stays on the page. Issue #90 was exactly the retired-spelling trap: the plugin called `sessions.open` alone, which the 0.1.6 selection refactor removed from the service (the click then did nothing on `0.1.7-rc.2`), so the jump now follows the view owner first and the per-baseline `sessionNav` probe pins both spellings presence-as-declared. -The Context Dashboard's DeepSeek balance capsule is an all-optional stack, so it arms nowhere it cannot: the host reaches the `settings` and `credentials` services only inside a deferred `ctx.inject` (and re-reads them per request, so a key added while the host runs is picked up on the next open), mounts `/api/dsh-context/balance` through the same Connection exact-route registry as the detail and backfill routes, and the outbound `/user/balance` read carries the key only inside the host process. The provider's settings row is read through whichever face the running line serves: the `get(ns)` registered-section read through V3 (the `llm-deepseek` namespace), the Config-form `describe()` projection on V4+ — there the row is the entry whose served value declares the top-level `apiKeyEnv` credential ref, preferred under the settings ids the generations have served it as (`llm-deepseek` on product profiles, `llm-deepseek-api-key` elsewhere). The capsule appears only on a live, well-formed answer; a deployment whose DeepSeek API-key provider is not composed (no settings row on either face), whose credentials service cannot resolve the key, whose connection service lacks the exact-route registry, or whose platform read fails (network, non-2xx, malformed payload, a third-party endpoint that does not serve the balance path) answers a typed `null` and the browser renders nothing — no pill, no spinner, no error state. The `balance` probe pins the settings face and the provider row's sources per baseline. +The Context Insights page's DeepSeek balance capsule is an all-optional stack, so it arms nowhere it cannot: the host reaches the `settings` and `credentials` services only inside a deferred `ctx.inject` (and re-reads them per request, so a key added while the host runs is picked up on the next open), mounts `/api/dsh-context/balance` through the same Connection exact-route registry as the detail and backfill routes, and the outbound `/user/balance` read carries the key only inside the host process. The provider's settings row is read through whichever face the running line serves: the `get(ns)` registered-section read through V3 (the `llm-deepseek` namespace), the Config-form `describe()` projection on V4+ — there the row is the entry whose served value declares the top-level `apiKeyEnv` credential ref, preferred under the settings ids the generations have served it as (`llm-deepseek` on product profiles, `llm-deepseek-api-key` elsewhere). The capsule appears only on a live, well-formed answer; a deployment whose DeepSeek API-key provider is not composed (no settings row on either face), whose credentials service cannot resolve the key, whose connection service lacks the exact-route registry, or whose platform read fails (network, non-2xx, malformed payload, a third-party endpoint that does not serve the balance path) answers a typed `null` and the browser renders nothing — no pill, no spinner, no error state. The `balance` probe pins the settings face and the provider row's sources per baseline. ## What each check means @@ -112,19 +113,19 @@ The skill-injection category (issue #66) bumps the timeline projection's `stateV The Agent network card covers the orphaned relatives without waiting for a visit: a session listed without a `contextTimeline` row (composition absent, occupancy only) fetches its slim head from the plugin's `/api/dsh-context/detail` fetch route (mounted through Connection's exact-route registry, behind the same authenticated `/api` fence), which folds the durable log on demand — the node's ring renders the full composition the first time the card opens. The same backfill serves sessions whose cache predates the plugin's installation entirely. Where the route is unavailable (the baseline gate, a connection service without the exact-route registry), the card degrades to the pressure-only occupancy ring as before. -The session cards' last-user-message preview (`lastUser` on the `contextTimeline` head) bumps the timeline projection's `stateVersion` (19 → 20): a stale row can never gain the field while its session is idle (it only lands when a user message folds), so on upgrade every cached timeline row is invalidated and refolded from the durable log. To keep the bump from orphaning idle sessions' rows, the warm-up (`src/host/backfill.ts`) probes BOTH dashboard rows — `contextActivity` AND `contextTimeline` — and cold-refolds every session whose either row is missing or version-stale, so previews (and every other head figure) are exact for the whole session list the first time the dashboard opens after the upgrade. A deployment without the cold-path services — or whose client cannot mount the warm-up's trigger route — degrades to the per-session refold on the session's next activity, the same as pre-warm-up upgrades. +The session cards' last-user-message preview (`lastUser` on the `contextTimeline` head) bumps the timeline projection's `stateVersion` (19 → 20): a stale row can never gain the field while its session is idle (it only lands when a user message folds), so on upgrade every cached timeline row is invalidated and refolded from the durable log. To keep the bump from orphaning idle sessions' rows, the warm-up (`src/host/backfill.ts`) probes BOTH insight rows — `contextActivity` AND `contextTimeline` — and cold-refolds every session whose either row is missing or version-stale, so previews (and every other head figure) are exact for the whole session list the first time the page opens after the upgrade. A deployment without the cold-path services — or whose client cannot mount the warm-up's trigger route — degrades to the per-session refold on the session's next activity, the same as pre-warm-up upgrades. -The Context Dashboard (the sidebar-foot panel) reads its `contextActivity` projection — a per-day ledger of billed tokens and completed requests, `stateVersion` 1 — off every session-list row's projection column. A new key is purely additive: no existing cached row is invalidated, but sessions folded before the unit existed (and sessions that predate the plugin entirely, whose `contextTimeline`/`contextHeaders` rows are missing too) have nothing to serve until they next go live. The plugin therefore runs a one-pass warm-up on demand (`src/host/backfill.ts`): the dashboard is the rows' only reader, so its panel POSTs the plugin's `/api/dsh-context/backfill` fetch route (the same authenticated `/api` fence as the detail route) the first time it opens, and only then does the host walk the corpus — for every stored session missing the row it cold-reads the durable log once through the projection cache's own cold-read ladder (`coldSnapshot`), which seeds each unit from its cached rows, folds the remainder, and writes the refreshed checkpoint back. The pass runs once per host process (later opens and later POSTs are no-ops); live sessions are skipped (they fold for themselves); a legacy log the running harness refuses to migrate is permanent — its per-session detail logs at debug and the pass ends with one summary info line, so an unmigratable corpus costs one line per dashboard-open process, not one warn per session per boot; and a deployment whose client never opens the dashboard never cold-reads a thing. A deployment without the sessionQuery/sessionProjectionCache/sessionPersistence/sessions services — or whose connection service lacks the exact-route registry — simply never arms the pass (such a client could not open the dashboard to read the rows anyway). +The Context Insights page (the sidebar panel list's first-level `main` panel) reads its `contextActivity` projection — a per-day ledger of billed tokens and completed requests, `stateVersion` 1 — off every session-list row's projection column. A new key is purely additive: no existing cached row is invalidated, but sessions folded before the unit existed (and sessions that predate the plugin entirely, whose `contextTimeline`/`contextHeaders` rows are missing too) have nothing to serve until they next go live. The plugin therefore runs a one-pass warm-up on demand (`src/host/backfill.ts`): the page is the rows' only reader, so it POSTs the plugin's `/api/dsh-context/backfill` fetch route (the same authenticated `/api` fence as the detail route) the first time it mounts, and only then does the host walk the corpus — for every stored session missing the row it cold-reads the durable log once through the projection cache's own cold-read ladder (`coldSnapshot`), which seeds each unit from its cached rows, folds the remainder, and writes the refreshed checkpoint back. The pass runs once per host process (later opens and later POSTs are no-ops); live sessions are skipped (they fold for themselves); a legacy log the running harness refuses to migrate is permanent — its per-session detail logs at debug and the pass ends with one summary info line, so an unmigratable corpus costs one line per page-open process, not one warn per session per boot; and a deployment whose client never opens the page never cold-reads a thing. A deployment without the sessionQuery/sessionProjectionCache/sessionPersistence/sessions services — or whose connection service lacks the exact-route registry — simply never arms the pass (such a client could not open the page to read the rows anyway). -The dashboard's per-day usage chart (the first row's right half) extends the `contextActivity` ledger with per-day pricing records — each day's billed buckets keyed provider → model → pricing period, the same `SessionCostUsage` raw material the timeline's session-cost totals carry, booked at each turn's initiation instant and priced client-side by the same book and estimator as the KPI band. Booking fees per (provider, model, period) changes the fold's state shape, so the unit's `stateVersion` bumps (1 → 2): on upgrade every cached `contextActivity` row reads version-stale, and the dashboard's existing warm-up (above) cold-refolds the corpus from the durable logs, rebuilding every day's tokens AND fees exactly the first time the panel opens. On the wire the field is additive-optional: a day folded before it existed (or whose settlements carried no model header or no usage) serves without it, and the chart degrades to tokens-only bars for those days — the same sanitizer contract every other projection value rides. +The page's per-day usage chart (the first row's right half) extends the `contextActivity` ledger with per-day pricing records — each day's billed buckets keyed provider → model → pricing period, the same `SessionCostUsage` raw material the timeline's session-cost totals carry, booked at each turn's initiation instant and priced client-side by the same book and estimator as the KPI band. Booking fees per (provider, model, period) changes the fold's state shape, so the unit's `stateVersion` bumps (1 → 2): on upgrade every cached `contextActivity` row reads version-stale, and the page's existing warm-up (above) cold-refolds the corpus from the durable logs, rebuilding every day's tokens AND fees exactly the first time the panel opens. On the wire the field is additive-optional: a day folded before it existed (or whose settlements carried no model header or no usage) serves without it, and the chart degrades to tokens-only bars for those days — the same sanitizer contract every other projection value rides. -The timing card's session-time strip (the card's bottom band: every completed step's TTFT wait, decode blocks in stream order, tool-run windows, and in-step residue packed gapless in occurrence order — each slice's width is its share of the session's cumulative active time, with ticks at zero, the quartiles, and the full span) bumps the timeline projection's `stateVersion` (22 → 23): the strip's painted spans are a per-step collection the fold flushes at `step/end`, which later events cannot backfill for steps already folded (the timing totals' own v11 precedent), so on upgrade every cached timeline row is invalidated and refolded from the durable log, which rebuilds the spans for sessions started under older plugin builds; the v20 warm-up rebuilds idle sessions' rows the first time the dashboard opens. On the wire the collection is additive-optional — it rides the detail channel (and the inline value on channel-less hosts), never the slim head — so a payload folded before it existed serves without it, and the timing card shows no strip then. The spans are bounded to the newest 2,000 records (a plain count cap), so an outsize session's strip paints its recent window; untrusted spans are re-proved per item at the client's render boundary, the same sanitizer contract every collection rides. +The timing card's session-time strip (the card's bottom band: every completed step's TTFT wait, decode blocks in stream order, tool-run windows, and in-step residue packed gapless in occurrence order — each slice's width is its share of the session's cumulative active time, with ticks at zero, the quartiles, and the full span) bumps the timeline projection's `stateVersion` (22 → 23): the strip's painted spans are a per-step collection the fold flushes at `step/end`, which later events cannot backfill for steps already folded (the timing totals' own v11 precedent), so on upgrade every cached timeline row is invalidated and refolded from the durable log, which rebuilds the spans for sessions started under older plugin builds; the v20 warm-up rebuilds idle sessions' rows the first time the page opens. On the wire the collection is additive-optional — it rides the detail channel (and the inline value on channel-less hosts), never the slim head — so a payload folded before it existed serves without it, and the timing card shows no strip then. The spans are bounded to the newest 2,000 records (a plain count cap), so an outsize session's strip paints its recent window; untrusted spans are re-proved per item at the client's render boundary, the same sanitizer contract every collection rides. -The timing totals' wait/generation boundary moving from the first token to the first observable instant (above — an earlier `block-start` marker wins when the token packing could not stamp it, the same boundary the strip's spans already paint) bumps the timeline projection's `stateVersion` (23 → 24): rows folded under 23 charged the marker-tiled decode window to BOTH the wait and the generation split, so their legend rows sum past 100%, and the mis-split totals cannot be repaired by later events, so on upgrade every cached timeline row is invalidated and refolded from the durable log; the v20 warm-up rebuilds idle sessions' rows the first time the dashboard opens. The wire shape is unchanged — the same `ttftMs`/`genMs` fields carry the corrected sums — and the TPS chip keeps the harness-parity first-token anchor, so only the card's wait/generation shares move. +The timing totals' wait/generation boundary moving from the first token to the first observable instant (above — an earlier `block-start` marker wins when the token packing could not stamp it, the same boundary the strip's spans already paint) bumps the timeline projection's `stateVersion` (23 → 24): rows folded under 23 charged the marker-tiled decode window to BOTH the wait and the generation split, so their legend rows sum past 100%, and the mis-split totals cannot be repaired by later events, so on upgrade every cached timeline row is invalidated and refolded from the durable log; the v20 warm-up rebuilds idle sessions' rows the first time the page opens. The wire shape is unchanged — the same `ttftMs`/`genMs` fields carry the corrected sums — and the TPS chip keeps the harness-parity first-token anchor, so only the card's wait/generation shares move. -Accounting for `developer/message` tool-registry changes bumps the timeline projection's `stateVersion` (24 → 25): earlier rows omitted these model-visible messages from their surface, request composition, and archives. On upgrade the derived rows refold from the durable log; the existing dashboard warm-up also rebuilds idle sessions. The wire shape stays unchanged. +Accounting for `developer/message` tool-registry changes bumps the timeline projection's `stateVersion` (24 → 25): earlier rows omitted these model-visible messages from their surface, request composition, and archives. On upgrade the derived rows refold from the durable log; the existing page warm-up also rebuilds idle sessions. The wire shape stays unchanged. -The trend chart's duration overlay (the card's Duration toggle: each request's step active time drawn as a curve over the bars, read off the right-hand quartile axis) bumps the timeline projection's `stateVersion` (25 → 26): the per-request `activeMs` is stamped at `step/end` fold time — the whole step window (the model call plus its tool runs) minus the step's user waits, those being approval decision intervals (`approval/asked` → `approval/decided`, a crash-dangling pair closing at the step's own end) and `ask_user_question` answer windows (the tool's body is the wait itself, in both the blocking and the timed schema's foreground phase) — so requests already folded can never gain it later (the v9 `cacheRead` precedent), and on upgrade every cached timeline row is invalidated and refolded from the durable log, while the v20 warm-up rebuilds idle sessions' rows the first time the dashboard opens. On the wire the field is additive-optional: a request whose step is still open (the live tail) or that folded without one (an unpaired/hostile log) serves without it, and the overlay's curve simply breaks at that bar instead of faking a point — the same sanitizer contract every collection rides. +The trend chart's duration overlay (the card's Duration toggle: each request's step active time drawn as a curve over the bars, read off the right-hand quartile axis) bumps the timeline projection's `stateVersion` (25 → 26): the per-request `activeMs` is stamped at `step/end` fold time — the whole step window (the model call plus its tool runs) minus the step's user waits, those being approval decision intervals (`approval/asked` → `approval/decided`, a crash-dangling pair closing at the step's own end) and `ask_user_question` answer windows (the tool's body is the wait itself, in both the blocking and the timed schema's foreground phase) — so requests already folded can never gain it later (the v9 `cacheRead` precedent), and on upgrade every cached timeline row is invalidated and refolded from the durable log, while the v20 warm-up rebuilds idle sessions' rows the first time the page opens. On the wire the field is additive-optional: a request whose step is still open (the live tail) or that folded without one (an unpaired/hostile log) serves without it, and the overlay's curve simply breaks at that bar instead of faking a point — the same sanitizer contract every collection rides. To force a full refold of an existing session sooner, delete its cached projection row — it is a derived cache and is rebuilt from the durable log: diff --git a/docs/context-dashboard.png b/docs/context-dashboard.png index 59db9cc..2268b2d 100644 Binary files a/docs/context-dashboard.png and b/docs/context-dashboard.png differ diff --git a/src/client/components/balanceCapsule.tsx b/src/client/components/balanceCapsule.tsx index 8f113d4..9f16aed 100644 --- a/src/client/components/balanceCapsule.tsx +++ b/src/client/components/balanceCapsule.tsx @@ -1,20 +1,20 @@ /** * BalanceCapsule — the DeepSeek open-platform balance pill in the Context - * Dashboard's header (the plugin's one account-level figure, beside the + * Insights page's heading (the plugin's one account-level figure, beside the * session-level ones). Clicking it opens the platform's usage console in a - * new tab. Mounted only by the dashboard header; it paints the figure the + * new tab. Mounted only by the page heading; it paints the figure the * previous open remembered while client/balance.ts revalidates it in the * background, and renders NOTHING when nothing is remembered and the route * answers nothing — the pill exists only when there is a figure to show. The - * non-zero parts of the breakdown (topped-up / granted) ride the harness - * Tooltip primitive — immediate on hover, where a native `title` lags — in + * non-zero parts of the breakdown (topped-up / granted) ride the page's + * portaled hover tip (hoverTip.tsx, off the pill's `data-lc-tip`) — + * immediate on hover, where a native `title` lags — in * the entry's own currency (the pill itself carries the total); the entry * follows the active locale (zh → CNY) with the account's first currency as * the fallback. */ import { useState, type ReactElement } from 'react' -import { Tooltip } from '@deepseek-ai/dsh-client-ui-primitives' import { balanceEntryOf, readPlatformBalance } from '../balance' import type { PlatformBalance } from '../../shared/types' import type { ClientCtx } from '../services' @@ -48,10 +48,11 @@ export function makeBalanceCapsule(ctx: ClientCtx, kit: ViewKit): () => ReactEle ...(entry.toppedUp > 0 ? [t('balance.tip.toppedUp') + ': ' + money(entry.toppedUp)] : []), ...(entry.granted > 0 ? [t('balance.tip.granted') + ': ' + money(entry.granted)] : []), ] - const pill = ( + return ( 0 ? tipLines.join('\n') : undefined} + {...(tipLines.length > 0 ? { 'data-lc-tip': tipLines.join('\n'), 'data-lc-tip-side': 'bottom' } : {})} href={USAGE_URL} target="_blank" rel="noreferrer" @@ -60,8 +61,5 @@ export function makeBalanceCapsule(ctx: ClientCtx, kit: ViewKit): () => ReactEle {money(entry.total)} ) - return tipLines.length > 0 - ? {pill} - : pill } } diff --git a/src/client/components/heatmap.tsx b/src/client/components/heatmap.tsx index a455f15..17f1f1f 100644 --- a/src/client/components/heatmap.tsx +++ b/src/client/components/heatmap.tsx @@ -8,16 +8,16 @@ * can spread its steps thinly across many days while another day stacks a * run, so each metric scales on its OWN maximum — in four steps; a day with * data is a button whose click - * pins the session list to that day (click again to release). Cells tip - * through the harness's own Tooltip primitive (instant on hover; native - * `title` lags a second behind), the bubble carrying the date, the day's - * active sessions, and its steps. The grid is computed from the injected + * pins the session list to that day (click again to release). Cells carry + * their tip (the date, the day's active sessions, and its steps) in + * `data-lc-tip`, served by the page's portaled hover tip (hoverTip.tsx) — + * instant on hover, where a native `title` lags a second behind. The grid is + * computed from the injected * `today` key, so * the layout is deterministic in tests and follows the browser's local * calendar at runtime. */ -import { Tooltip } from '@deepseek-ai/dsh-client-ui-primitives' import type { ReactElement } from 'react' import { dayKeyOf, shiftDayKey, sundayOfWeek } from '../../shared/days' import { type DayTotals } from '../overview' @@ -142,24 +142,20 @@ export function makeHeatmap(kit: ViewKit): (props: HeatmapProps) => ReactElement if (cell.future) return