Add in-app micro-feedback prompts for new features

Show a one-time toast asking how a feature is working after its third
successful use, with an optional comment sent anonymously to the Yaak
API (feature key, text, app version, and OS only — nothing identifying,
and nothing is sent unless the user clicks Send).

- New cmd_send_feedback Tauri command posts fire-and-forget via the
  shared API client (localhost in dev)
- Feature keys registry (cookie-editor, response-history, sse-summary,
  git-sync) with per-feature use counting in the key-value store
- "Never ask for feedback" setting to disable prompts entirely
- Toast gains dynamicHeight and hideDismiss props for richer content
- Fix missing vertical padding on xs/2xs multiline inputs
- Fix unused-import and dead-code warnings in yaak-system-appearance
  on non-Linux builds

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-07-04 15:06:21 -07:00
parent e52853cc2d
commit b1f1363502
18 changed files with 283 additions and 13 deletions
@@ -3,7 +3,7 @@ import { extractSseValueAtPath, type ServerSentEvent } from "@yaakapp-internal/s
import { HStack, Icon, InlineCode, SplitLayout, VStack } from "@yaakapp-internal/ui";
import classNames from "classnames";
import type { CSSProperties, ReactNode } from "react";
import { Fragment, useMemo, useState } from "react";
import { Fragment, useEffect, useMemo, useState } from "react";
import { useKeyValue } from "../../hooks/useKeyValue";
import { useFormatText } from "../../hooks/useFormatText";
import { useResponseBodyEventSource } from "../../hooks/useResponseBodyEventSource";
@@ -13,6 +13,7 @@ import {
useSseSummaryResultKeyPath,
} from "../../hooks/useSseSummaryResultKeyPath";
import { isJSON } from "../../lib/contentType";
import { trackFeatureUsage } from "../../lib/featureFeedback";
import { EmptyStateText } from "../EmptyStateText";
import { Markdown } from "../Markdown";
import { Button } from "../core/Button";
@@ -71,6 +72,16 @@ function ActualEventStreamViewer({ response }: Props) {
summary.data.fragmentCount === 0 &&
!summary.isFetching &&
summary.error == null;
// The component remounts per response (keyed above), so this counts at most
// one use for each response that successfully extracts summary text
const hasSummaryFragments = showExtractedText && (summary.data?.fragmentCount ?? 0) > 0;
const [trackedSummaryUse, setTrackedSummaryUse] = useState<boolean>(false);
useEffect(() => {
if (!hasSummaryFragments || trackedSummaryUse) return;
setTrackedSummaryUse(true);
trackFeatureUsage("sse-summary");
}, [hasSummaryFragments, trackedSummaryUse]);
const filterEventPreviews = showExtractedText && filterEventPreviewsSetting.value === true;
const applyToDetails = showExtractedText && applyToDetailsSetting.value === true;
const renderMarkdown = showExtractedText && renderMarkdownSetting.value === true;