Skip to content

feat(desktop): TopBar connection status badge#36

Merged
hqhq1025 merged 1 commit intomainfrom
wt/ux-connection-status
Apr 18, 2026
Merged

feat(desktop): TopBar connection status badge#36
hqhq1025 merged 1 commit intomainfrom
wt/ux-connection-status

Conversation

@hqhq1025
Copy link
Copy Markdown
Collaborator

Summary

Implements §8.3 of docs/research/18-ux-master-plan.md.

  • Adds a 10px status dot to the TopBar, immediately right of the project crumb
  • Four states: green (connected), amber (untested/stale), red (error), grey (no provider)
  • Hover tooltip shows state label, last-tested timestamp, error message, and "click to re-test" hint
  • Clicking the dot triggers connection:v1:test-active IPC — reads active provider credentials from the encrypted keychain in the main process (no key passed from renderer)
  • Auto-tests on mount when status is stale (>5 min old) or untested; dot is never animated
  • New Zustand state slice: connectionStatus: { state, lastTestedAt, lastError } + setConnectionStatus + testConnection actions
  • i18n keys added under topbar.status.* in en.json and zh-CN.json
  • 13 new unit tests (6 store action tests + 7 component color/time-format tests); all 98 tests pass

Test plan

  • typecheck passes (pnpm -r typecheck)
  • lint passes with 0 errors (pnpm lint)
  • all 98 tests pass (pnpm test)
  • dot renders in TopBar next to crumb text
  • amber on app start → auto-tests → turns green or red
  • click dot → re-test fires → dot updates
  • tooltip shows correct state label and last-tested time
  • no dot rendered when no provider is configured

Add a 10px status dot to TopBar (§8.3 of docs/research/18-ux-master-plan.md):
- Green / amber / red / grey states based on last connection test result
- Hover tooltip shows state label + last-tested time + error message + re-test prompt
- Click triggers an active-provider connection test via new IPC connection:v1:test-active
- Auto-test on mount if status is stale (>5 min) or untested
- connectionStatus slice added to Zustand store with setConnectionStatus + testConnection actions
- New IPC handler reads active provider credentials from encrypted keychain (no key from renderer)
- i18n keys added under topbar.status.* in en.json and zh-CN.json
- Unit tests for store actions and component color/time helpers (98 tests total, all green)

Signed-off-by: hqhq1025 <1506751656@qq.com>
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Major] Renderer bridge failures are mislabeled as no_provider (silent misdiagnosis) — this hides IPC/preload breakage as a config issue, making real runtime failures harder to detect and violating the “no silent fallbacks” constraint. Evidence apps/desktop/src/renderer/src/store.ts:272
    Suggested fix:

    async testConnection() {
      const cfg = get().config;
      if (!window.codesign) {
        const msg = tr('errors.rendererDisconnected');
        set({
          connectionStatus: { state: 'error', lastTestedAt: Date.now(), lastError: msg },
          errorMessage: msg,
          lastError: msg,
        });
        return;
      }
      if (cfg === null || !cfg.hasKey || cfg.provider === null) {
        set({ connectionStatus: { state: 'no_provider', lastTestedAt: null, lastError: null } });
        return;
      }
      // ...
    }
  • [Major] Relative time text is hardcoded in English and bypasses i18n — Chinese locale will still show "30s ago"/"3m ago", creating mixed-language UI. Evidence apps/desktop/src/renderer/src/components/ConnectionStatusDot.tsx:7
    Suggested fix:

    function formatRelativeTime(ts: number): string {
      const diffSec = Math.round((Date.now() - ts) / 1000);
      if (diffSec < 60) return t('topbar.status.relative.secondsAgo', { count: diffSec });
      const diffMin = Math.round(diffSec / 60);
      if (diffMin < 60) return t('topbar.status.relative.minutesAgo', { count: diffMin });
      return t('topbar.status.relative.hoursAgo', { count: Math.round(diffMin / 60) });
    }
  • [Major] New UI uses raw pixel typography value text-[11px], which violates the token-only UI value constraint — this introduces a non-token size in app code. Evidence apps/desktop/src/renderer/src/components/ConnectionStatusDot.tsx:86
    Suggested fix:

    <span
      role="tooltip"
      className="... text-[var(--text-xs)] ..."
    >
  • [Minor] “Component tests” do not exercise production component logic — test file redefines helper logic locally, so regressions in ConnectionStatusDot.tsx can pass undetected. Evidence apps/desktop/src/renderer/src/components/ConnectionStatusDot.test.ts:4
    Suggested fix:

    import { render, screen } from '@testing-library/react';
    import { ConnectionStatusDot } from './ConnectionStatusDot';
    
    it('renders localized last-tested tooltip', () => {
      render(<ConnectionStatusDot />);
      expect(screen.getByRole('button')).toHaveAttribute('aria-label', expect.stringContaining('Last tested'));
    });

Summary

  • Review mode: initial
  • 4 issues found in changed lines (3 Major, 1 Minor). No new dependency/license or direct provider SDK import issues found in this diff. docs/VISION.md and docs/PRINCIPLES.md are not present in this checkout (as noted by CLAUDE.md), so those docs could not be re-validated here.

Testing

  • Not run (automation)

open-codesign Bot


async testConnection() {
const cfg = get().config;
if (!window.codesign || cfg === null || !cfg.hasKey || cfg.provider === null) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Major] !window.codesign is mapped to no_provider, which masks renderer/main bridge failures as config state. Please surface this as an explicit error state (e.g., rendererDisconnected) instead of provider-missing.


const STALE_MS = 5 * 60 * 1000;

function formatRelativeTime(ts: number): string {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Major] Relative-time text is hardcoded in English (s/m/h ago) and bypasses i18n, so zh-CN users will see mixed-language tooltip content. Please localize these units/phrases via translation keys.

</button>
<span
role="tooltip"
className="pointer-events-none absolute bottom-full mb-1.5 left-1/2 -translate-x-1/2 z-50 whitespace-nowrap max-w-xs rounded-[var(--radius-sm)] bg-[var(--color-text-primary)] px-2 py-1 text-[11px] font-medium text-[var(--color-background)] opacity-0 transition-opacity duration-150 delay-[400ms] group-hover:opacity-100 shadow-[var(--shadow-card)]"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Major] text-[11px] introduces a raw pixel typography value in app code. Per token-only UI constraints, use a tokenized text size (e.g., var(--text-*)) from packages/ui tokens.

import { describe, expect, it } from 'vitest';
import type { ConnectionState } from '../store';

// ── Pure helpers extracted for unit testing ────────────────────────────────
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] This test file redefines helper logic locally instead of importing/testing the production component logic, so regressions in ConnectionStatusDot.tsx can slip through. Please switch to component-level assertions or exported helpers from the source file.

@hqhq1025 hqhq1025 merged commit 9b4d400 into main Apr 18, 2026
6 checks passed
@hqhq1025 hqhq1025 deleted the wt/ux-connection-status branch April 18, 2026 17:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant