From ba6f4a229a39cb1a462dda42fa095424cfe4882f Mon Sep 17 00:00:00 2001 From: Adam Setch Date: Sun, 25 Oct 2026 18:25:08 -0400 Subject: [PATCH] docs: complete TSDoc accuracy and guardrails --- src/main/lifecycle/first-run.ts | 5 ++- src/main/lifecycle/startup.ts | 6 +-- src/preload/utils.ts | 11 +++++ .../LoginWithPersonalAccessTokenForm.tsx | 4 +- src/renderer/stores/types.ts | 8 ++-- src/renderer/types.ts | 45 +++++++++---------- src/renderer/utils/api/errors.ts | 11 +++-- src/renderer/utils/api/queryKeys.ts | 6 ++- .../utils/forges/github/handlers/utils.ts | 14 +++--- .../utils/notifications/formatters.ts | 3 +- src/renderer/utils/notifications/group.ts | 13 +++--- .../utils/notifications/notifications.ts | 7 ++- src/renderer/utils/system/keyboardShortcut.ts | 6 ++- src/renderer/utils/system/native.ts | 7 +-- src/renderer/utils/ui/zoom.ts | 8 ++-- src/shared/logger.ts | 3 ++ vite.config.mts | 41 ++++++++++++++++- 17 files changed, 134 insertions(+), 64 deletions(-) diff --git a/src/main/lifecycle/first-run.ts b/src/main/lifecycle/first-run.ts index 585a86f0f..bb72a1716 100644 --- a/src/main/lifecycle/first-run.ts +++ b/src/main/lifecycle/first-run.ts @@ -52,9 +52,10 @@ const getConfigPath = () => { }; /** - * Determine if this is the first run of the application by checking for the existence of a specific file. + * Determine whether this is the first run by checking for the marker file. When + * it is the first run, create the marker so later launches skip this check. * - * @returns true if this is the first run, false otherwise + * @returns `true` if this is the first run, `false` otherwise. */ function checkAndMarkFirstRun(): boolean { const configPath = getConfigPath(); diff --git a/src/main/lifecycle/startup.ts b/src/main/lifecycle/startup.ts index e80a38f71..f82b957a5 100644 --- a/src/main/lifecycle/startup.ts +++ b/src/main/lifecycle/startup.ts @@ -35,8 +35,8 @@ export function initializeAppLifecycle( } /** - * Handle a gitify:// protocol URL by forwarding it to the renderer process - * as an AUTH_CALLBACK event. + * Handle a custom-protocol URL by forwarding it to the renderer process as an + * AUTH_CALLBACK event. * * @param mb - The menubar instance to forward the event through. * @param url - The protocol URL to handle. @@ -53,7 +53,7 @@ export function handleProtocolURL(mb: Menubar, url: string, protocol: string): v * Enforce a single application instance. If a second instance is launched, * any protocol URL in the command line is forwarded to the existing instance. * - * @param mb - The menubar instance to show when a second instance is detected. + * @param mb - The menubar instance used to forward a detected protocol URL. * @param protocol - The custom protocol string to extract from command line args. */ function preventSecondInstance(mb: Menubar, protocol: string): void { diff --git a/src/preload/utils.ts b/src/preload/utils.ts index 8418a5f73..2a85a3cc7 100644 --- a/src/preload/utils.ts +++ b/src/preload/utils.ts @@ -5,6 +5,10 @@ import type { EventArgs, EventRequest, EventResponse, EventType } from '../share /** * Send a fire-and-forget IPC message from the renderer to the main process. * Variadic so events without a payload can be called as `sendMainEvent(event)`. + * + * @param event - The IPC channel/event name to send. + * @param args - Payload arguments for the event; omitted for events with no + * request payload. */ export function sendMainEvent(event: E, ...args: EventArgs): void { ipcRenderer.send(event, ...args); @@ -13,6 +17,11 @@ export function sendMainEvent(event: E, ...args: EventArgs< /** * Send an IPC message from the renderer to the main process and await a response. * The resolved value type is enforced by the event's contract. + * + * @param event - The IPC channel/event name to invoke. + * @param args - Payload arguments for the event; omitted for events with no + * request payload. + * @returns The response value defined by the event's contract. */ export async function invokeMainEvent( event: E, @@ -30,6 +39,8 @@ export async function invokeMainEvent( /** * Register a listener for an IPC event sent from the main process to the renderer. * + * @param event - The IPC channel/event name to listen for. + * @param listener - Callback invoked with the event and its typed request payload. * @returns A function that removes the listener. Callers that register * listeners from React effects must return it as the effect cleanup, * otherwise every remount leaks a permanent ipcRenderer listener. diff --git a/src/renderer/components/login/LoginWithPersonalAccessTokenForm.tsx b/src/renderer/components/login/LoginWithPersonalAccessTokenForm.tsx index fd2b04a25..34ac8cca5 100644 --- a/src/renderer/components/login/LoginWithPersonalAccessTokenForm.tsx +++ b/src/renderer/components/login/LoginWithPersonalAccessTokenForm.tsx @@ -58,8 +58,8 @@ export interface LoginWithPersonalAccessTokenFormProps { /** * Shared personal-access-token login form. * - * Forge-specific route components (`routes/github`, `routes/gitea`) supply - * their own copy via props so this form stays free of forge conditionals. + * Forge-specific route components under `routes/` supply their own copy via + * props so this form stays free of forge conditionals. */ export function LoginWithPersonalAccessTokenForm({ forge, diff --git a/src/renderer/stores/types.ts b/src/renderer/stores/types.ts index c7ca4575f..6008bc89d 100644 --- a/src/renderer/stores/types.ts +++ b/src/renderer/stores/types.ts @@ -233,7 +233,8 @@ export interface NotificationActionFailure { */ export interface NotificationActionFailuresState { /** - * Map of notification ID to the details of its most recent failed action attempt. + * Map of notification failure key (`:`) to the + * details of its most recent failed action attempt. */ failures: Record; } @@ -258,8 +259,9 @@ export interface NotificationActionFailuresActions { clearFailures: (notificationKeys: string[]) => void; /** - * Clears any recorded failures for notification IDs not present in `notificationIds` - * (e.g. once a notification no longer appears in the notifications list). + * Clears any recorded failures for notification keys not present in + * `notificationKeys` (e.g. once a notification no longer appears in the + * notifications list). */ pruneFailures: (notificationKeys: string[]) => void; diff --git a/src/renderer/types.ts b/src/renderer/types.ts index e1559ab9b..087391ed7 100644 --- a/src/renderer/types.ts +++ b/src/renderer/types.ts @@ -305,23 +305,20 @@ export interface Chevron { export type FilterStateType = 'open' | 'closed' | 'merged' | 'draft' | 'other'; -/** - * - * Gitify Notification Types - * - * These types represent the clean, UI-focused notification structure - * used throughout the application. - * - * Raw GitHub API responses are transformed into these types at the API boundary. - * - **/ +// --------------------------------------------------------------------------- +// Gitify notification types +// +// These types represent the clean, UI-focused notification structure used +// throughout the application. Raw forge responses are transformed into these +// types at the API boundary. +// --------------------------------------------------------------------------- /** * Complete notification type for UI consumption. * Contains only fields actually used by components. */ export interface GitifyNotification { - /** Unique notification ID from GitHub */ + /** Unique notification ID from the account's forge */ id: string; /** Whether the notification is unread */ unread: boolean; @@ -343,9 +340,11 @@ export interface GitifyNotification { /** * A notification that has been transformed from a forge response but not yet - * formatted for display. The orchestrator pipeline (transform → filter → - * enrich → filter) operates on this type; `formatNotification` is the only - * place that widens it to `GitifyNotification` by populating `display`. + * formatted for display. The orchestrator pipeline (transform → base filter → + * enrich → format → order) operates on this type; `formatNotification` is the + * only place that widens it to `GitifyNotification` by populating `display`. + * Detailed filtering runs later on the formatted notification in the query + * selection layer. */ export type RawGitifyNotification = Omit; @@ -562,16 +561,14 @@ export type GitifyCheckSuiteStatus = | 'TIMED_OUT' | 'WAITING'; -/** - * Gitify Type Enhancements - * - * These types represent the clean, UI-focused notification structure - * used throughout the application. - * - * Raw GitHub API responses are transformed into these types at the API boundary. - **/ - -// Stronger typings for string literal attributes +// --------------------------------------------------------------------------- +// Gitify type enhancements +// +// Stronger typings for the string literal attributes used by the notification +// types above, so filters, display logic, and reason labels share one set of +// named values. +// --------------------------------------------------------------------------- + export type Reason = | 'agent_session_finished' | 'approval_requested' diff --git a/src/renderer/utils/api/errors.ts b/src/renderer/utils/api/errors.ts index f45047d1b..ecf939b99 100644 --- a/src/renderer/utils/api/errors.ts +++ b/src/renderer/utils/api/errors.ts @@ -72,11 +72,14 @@ export function determineFailureType( } /** - * Handle GraphQL response errors. - * Logs and throws if `errors` array is present and non-empty. + * Log a GraphQL response error and throw. * - * @param context The context of the GraphQL request - * @param payload The GraphQL response payload + * The caller must pass a GraphQL error payload with a non-empty `errors` array; + * this function does not guard against a missing array. + * + * @param context - The context of the GraphQL request. + * @param payload - The GraphQL error response. + * @throws An `Error` containing the joined GraphQL error messages. */ export function handleGraphQLResponseError( context: string, diff --git a/src/renderer/utils/api/queryKeys.ts b/src/renderer/utils/api/queryKeys.ts index 856546332..a0b7e765f 100644 --- a/src/renderer/utils/api/queryKeys.ts +++ b/src/renderer/utils/api/queryKeys.ts @@ -25,8 +25,10 @@ export const notificationsKeys = { * accounts always resolves to a distinct cache entry. * * @param accountUUIDs - The UUIDs of all accounts being fetched - * @param fetchReadNotifications - Whether to fetch only unread notifications - * @param participating - Whether to fetch only participating notifications + * @param fetchReadNotifications - Whether to also fetch read notifications + * (maps to GitHub's `all` parameter) + * @param participating - Whether to restrict results to participating + * notifications (maps to GitHub's `participating` parameter) */ list: (accountUUIDs: AccountUUID[], fetchReadNotifications: boolean, participating: boolean) => [...notificationsKeys.all, accountUUIDs, fetchReadNotifications, participating] as const, diff --git a/src/renderer/utils/forges/github/handlers/utils.ts b/src/renderer/utils/forges/github/handlers/utils.ts index 53bbd7279..c6a9c9526 100644 --- a/src/renderer/utils/forges/github/handlers/utils.ts +++ b/src/renderer/utils/forges/github/handlers/utils.ts @@ -11,9 +11,10 @@ import type { type AuthorInput = AuthorFieldsFragment | GitifyNotificationUser | null | undefined; /** - * Construct the notification subject user based on an order prioritized list of users - * @param users array of users in order or priority - * @returns the subject user + * Construct the notification subject user from users in priority order. + * + * @param users - Users in priority order. + * @returns The first user with a value, or `undefined` when none is present. */ export function getNotificationAuthor(users: AuthorInput[]): GitifyNotificationUser | undefined { for (const user of users) { @@ -32,15 +33,16 @@ export function getNotificationAuthor(users: AuthorInput[]): GitifyNotificationU } /** - * Construct a GitHub Actions URL for a repository with optional workflow filters. + * Construct a GitHub Actions URL for a repository with workflow filters. * * Appends the provided filter strings as a `+`-joined `query` search parameter. * Note: `%2B` in the URL is un-encoded back to `+` because the GitHub Actions * UI does not handle encoded plus signs correctly. * * @param repositoryURL - The base HTML URL of the repository. - * @param filters - Optional workflow filter strings to append as a query. - * @returns The GitHub Actions URL, with filters applied if provided. + * @param filters - Workflow filter strings to append as a query; pass an empty + * array for none. + * @returns The GitHub Actions URL, with filters applied. */ export function actionsURL(repositoryURL: string, filters: string[]): Link { const url = new URL(repositoryURL); diff --git a/src/renderer/utils/notifications/formatters.ts b/src/renderer/utils/notifications/formatters.ts index fdcc1b182..e90a1ab14 100644 --- a/src/renderer/utils/notifications/formatters.ts +++ b/src/renderer/utils/notifications/formatters.ts @@ -26,7 +26,8 @@ export function formatForDisplay(text: string[]): string { } /** - * Formats a string to proper case (capitalize the first letter of each word). + * Formats a string to proper case: capitalize the first letter of each word and + * lower-case the remaining characters. * * @param text - The string to format. * @returns The proper-cased string. diff --git a/src/renderer/utils/notifications/group.ts b/src/renderer/utils/notifications/group.ts index 9f5ebeb45..5205a9396 100644 --- a/src/renderer/utils/notifications/group.ts +++ b/src/renderer/utils/notifications/group.ts @@ -3,9 +3,8 @@ import useSettingsStore from '../../stores/useSettingsStore'; import type { GitifyNotification } from '../../types'; /** - * Returns true when settings say to group by date. + * Returns true when the settings store says to group by date. * - * @param settings - The application settings. * @returns `true` when `groupBy` is `"DATE"`. */ export function isGroupByDate() { @@ -13,9 +12,8 @@ export function isGroupByDate() { } /** - * Returns true when settings say to group by repository. + * Returns true when the settings store says to group by repository. * - * @param settings - The application settings. * @returns `true` when `groupBy` is `"REPOSITORY"`. */ export function isGroupByRepository() { @@ -53,13 +51,16 @@ export function groupNotificationsByRepository( } return repoGroups; -} /** +} + +/** * Returns a flattened, ordered notifications list according to: * - repository-first-seen order (when grouped by repository) * - natural notifications order otherwise * + * Uses the settings store's `groupBy` value to choose the strategy. + * * @param notifications - The notifications to flatten and order. - * @param settings - Application settings controlling the groupBy strategy. * @returns A flat, ordered array of notifications. */ export function getFlattenedNotificationsByRepo( diff --git a/src/renderer/utils/notifications/notifications.ts b/src/renderer/utils/notifications/notifications.ts index 28352f4c0..2e57de8c8 100644 --- a/src/renderer/utils/notifications/notifications.ts +++ b/src/renderer/utils/notifications/notifications.ts @@ -56,10 +56,12 @@ function getNotifications(accounts: Account[]) { * - Transform * - Base filtering * - Enrichment - * - Detailed filtering * - Formatting * - Ordering * + * Detailed filtering runs later in the notifications query's `select` step, so + * the cached data stays unfiltered and filter changes apply without a refetch. + * * @returns A promise that resolves to an array of account notifications. */ export async function getAllNotifications(): Promise { @@ -264,7 +266,8 @@ export function clearEnrichmentCache(): void { * Assign an order property to each notification to stabilize how they are displayed * during notification interaction events (mark as read, mark as done, etc.) * - * @param accountNotifications + * @param accountNotifications - The account notifications whose notifications + * should be ordered. */ export function stabilizeNotificationsOrder(accountNotifications: AccountNotifications[]) { let orderIndex = 0; diff --git a/src/renderer/utils/system/keyboardShortcut.ts b/src/renderer/utils/system/keyboardShortcut.ts index 3a8cafa7e..fd482a8c8 100644 --- a/src/renderer/utils/system/keyboardShortcut.ts +++ b/src/renderer/utils/system/keyboardShortcut.ts @@ -40,7 +40,11 @@ const MODIFIER_KEYS = new Set(['Control', 'Shift', 'Alt', 'Meta', 'Dead']); /** * Convert a keydown event to an Electron accelerator string, or `null` if invalid. - * Requires Command (macOS) or Control (Windows/Linux) plus a non-modifier key. + * Requires a primary modifier (Command or Control) plus a non-modifier key. The + * result always uses the platform-neutral `CommandOrControl` accelerator. + * + * @param event - The keydown event to convert. + * @returns The accelerator string, or `null` when the event is not a valid shortcut. */ export function keyboardEventToAccelerator(event: KeyboardEvent): string | null { if (event.repeat) { diff --git a/src/renderer/utils/system/native.ts b/src/renderer/utils/system/native.ts index 88421068b..872f138e3 100644 --- a/src/renderer/utils/system/native.ts +++ b/src/renderer/utils/system/native.ts @@ -7,9 +7,10 @@ import { generateNotificationWebUrl } from '../notifications/url'; /** * Raises a native OS notification. * - * For a single notification, the message is used as the title (non-Windows) and the - * formatted footer text is used as the body. For multiple notifications, a generic count - * summary is shown instead. + * For a single notification, the repository full name is the title, the subject + * title is the body, and the generated web URL is the click target. For + * multiple notifications, the app name is the title and the body summarizes the + * notification count. * * @param notifications - The notifications to surface as a native OS notification. */ diff --git a/src/renderer/utils/ui/zoom.ts b/src/renderer/utils/ui/zoom.ts index b0daca959..d4f94a3ce 100644 --- a/src/renderer/utils/ui/zoom.ts +++ b/src/renderer/utils/ui/zoom.ts @@ -12,8 +12,8 @@ const ZOOM_STEP = 10 as Percentage; * Zoom percentage to level. 100% is the recommended zoom level (0). * If somehow the percentage is not set, it will return 0, the default zoom level. * - * @param percentage - Zoom percentage (0–150). - * @returns Electron zoom level (-2 to 0.5). + * @param percentage - Zoom percentage (0–120). + * @returns Electron zoom level (-2 to 0.4). */ export function zoomPercentageToLevel(percentage: Percentage): number { if (percentage === undefined) { @@ -27,8 +27,8 @@ export function zoomPercentageToLevel(percentage: Percentage): number { * Zoom level to percentage. 0 is the recommended zoom level (100%). * If somehow the zoom level is not set, it will return 100, the default zoom percentage. * - * @param zoom - Electron zoom level (-2 to 0.5). - * @returns Zoom percentage (0–150). + * @param zoom - Electron zoom level (-2 to 0.4). + * @returns Zoom percentage (0–120). */ export function zoomLevelToPercentage(zoom: number): Percentage { if (zoom === undefined) { diff --git a/src/shared/logger.ts b/src/shared/logger.ts index ead385be6..30daea71c 100644 --- a/src/shared/logger.ts +++ b/src/shared/logger.ts @@ -4,6 +4,9 @@ type LogLevel = 'info' | 'warn' | 'error'; /** * Safely coerce an unknown caught value into an Error instance. + * + * @param err - The caught value to coerce. + * @returns An `Error` carrying the value's message. */ export function toError(err: unknown): Error { return err instanceof Error ? err : new Error(String(err)); diff --git a/vite.config.mts b/vite.config.mts index 20736220b..24b084915 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -248,7 +248,7 @@ export default defineConfig({ 'process.env.OAUTH_CLIENT_ID': JSON.stringify('Ov23liQIkFs5ehQLNzHF'), }, lint: { - plugins: ['typescript', 'react', 'unicorn', 'oxc', 'import', 'vitest'], + plugins: ['typescript', 'react', 'unicorn', 'oxc', 'import', 'vitest', 'jsdoc'], // Enable the type-aware path so `vp check` runs TypeScript type checks // natively (via the TS Go toolchain / tsgolint). `typeCheck` requires // `typeAware`; enabling both also turns on the type-aware lint rules. @@ -296,6 +296,45 @@ export default defineConfig({ 'unicorn/prefer-number-properties': 'error', }, overrides: [ + { + // TSDoc conventions apply to the TypeScript sources. The JavaScript + // build scripts use JSDoc for type hints and are intentionally out of + // scope for the TypeScript-specific rules. + files: ['src/**/*.ts', 'src/**/*.tsx'], + rules: { + 'jsdoc/check-tag-names': ['error', { typed: true }], + 'jsdoc/no-blank-blocks': 'error', + 'jsdoc/require-param-name': 'error', + 'jsdoc/require-param-description': 'error', + 'jsdoc/require-returns-description': 'error', + }, + }, + { + // Contract-heavy scopes: every documented function parameter must have + // a matching @param tag. + files: ['src/preload/**/*.ts', 'src/shared/**/*.ts'], + excludeFiles: TEST_FILES, + rules: { + 'jsdoc/require-param': 'error', + }, + }, + { + // Shared utilities have block-bodied functions, so the return rule does + // not produce false positives on void concise arrows here. + files: ['src/shared/**/*.ts'], + excludeFiles: TEST_FILES, + rules: { + 'jsdoc/require-returns': 'error', + }, + }, + { + // Next clean scope after preload/shared. + files: ['src/renderer/utils/auth/**/*.ts'], + excludeFiles: TEST_FILES, + rules: { + 'jsdoc/require-param': 'error', + }, + }, { // Renderer boundary: every renderer module reaches the main process // through the preload bridge (`window.gitify`). Importing main/preload