From 8337836199a40b6908919c250f297f6853face5a Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Tue, 27 Oct 2026 22:32:55 +0200 Subject: [PATCH] Use sans settings labels and link Deepgram key setup --- components/dictation-language.tsx | 10 +++------- components/settings/chatgpt-connection.tsx | 4 ++-- components/settings/connection-controls.tsx | 4 ++-- components/settings/keys-section.tsx | 6 +++--- .../settings/subscription-connections.tsx | 9 ++++++--- docs/development.md | 2 +- tests/dictation-picker.test.tsx | 8 +++++--- tests/render-settings.test.tsx | 20 +++++++++++++++++++ 8 files changed, 42 insertions(+), 21 deletions(-) diff --git a/components/dictation-language.tsx b/components/dictation-language.tsx index b0db181..94a9c38 100644 --- a/components/dictation-language.tsx +++ b/components/dictation-language.tsx @@ -4,7 +4,7 @@ import { useEffect, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } import { DICTATION_LANGUAGES, type DictationLanguage } from "@/src/core"; import { MenuShell } from "@/components/menu-shell"; import { Hint } from "@/components/ui/hint"; -import { SettingsMenuTrigger } from "@/components/settings/controls"; +import { SettingsMenuTrigger, SettingsRow } from "@/components/settings/controls"; import { SearchMenuList } from "@/components/settings/search-menu-list"; import { useDictationActive } from "@/components/dictation-state"; import { useDictationLanguage } from "@/components/dictation-preference"; @@ -14,13 +14,9 @@ type LanguagePreference = ReturnType; export function DictationLanguageSetting() { const preference = useDictationLanguage(); const active = useDictationActive(); - return
- Language + return -
; + ; } function DictationLanguagePicker({ preference, disabled }: { diff --git a/components/settings/chatgpt-connection.tsx b/components/settings/chatgpt-connection.tsx index f9d2d69..498ccf9 100644 --- a/components/settings/chatgpt-connection.tsx +++ b/components/settings/chatgpt-connection.tsx @@ -328,12 +328,12 @@ function AccountDialog({ children, onClose, busy }: { children: React.ReactNode; } /** Shared compact settings/setup row; account selection precedes the quiet action. */ -export function ChatGPTConnection(_props: { setup?: boolean } = {}) { +export function ChatGPTConnection({ labelClassName }: { setup?: boolean; labelClassName?: string } = {}) { const ui = useContext(Context); const active = ui.status.accounts.find((a) => a.id === ui.status.activeId && a.connected); const accounts = ui.status.accounts.filter((a) => a.connected); return
- active ? ui.manage(active.id) : ui.connect()}> + active ? ui.manage(active.id) : ui.connect()}> {active && } diff --git a/components/settings/connection-controls.tsx b/components/settings/connection-controls.tsx index 395d604..5a92319 100644 --- a/components/settings/connection-controls.tsx +++ b/components/settings/connection-controls.tsx @@ -4,9 +4,9 @@ import { createPortal } from "react-dom"; import { useBackdropDismiss } from "@/components/backdrop-dismiss"; import { Button } from "@/components/ui/button"; -export function ConnectionRow({ label, action, disabled, onAction, children, testid, actionTestid }: { label: string; action: string; disabled: boolean; onAction: () => void; children?: React.ReactNode; testid?: string; actionTestid?: string }) { +export function ConnectionRow({ label, action, disabled, onAction, children, testid, actionTestid, labelClassName }: { label: string; action: string; disabled: boolean; onAction: () => void; children?: React.ReactNode; testid?: string; actionTestid?: string; labelClassName?: string }) { return
- {label} + {label} {children}
; diff --git a/components/settings/keys-section.tsx b/components/settings/keys-section.tsx index e72f6fc..198e93d 100644 --- a/components/settings/keys-section.tsx +++ b/components/settings/keys-section.tsx @@ -493,12 +493,12 @@ export function KeysSection({ {label === "Dictation" && <>

- Create a Deepgram key with the Member role or higher, then add it above. + >Deepgram API key with the Member role or higher, then add it above.

} )} diff --git a/components/settings/subscription-connections.tsx b/components/settings/subscription-connections.tsx index 6c095d1..80f3870 100644 --- a/components/settings/subscription-connections.tsx +++ b/components/settings/subscription-connections.tsx @@ -119,16 +119,19 @@ export function NativeSubscriptionUIProvider({ children }: { children: React.Rea ; } -export function NativeSubscriptionConnection({ providerId }: { providerId: NativeSubscriptionProviderId }) { +export function NativeSubscriptionConnection({ providerId, labelClassName }: { providerId: NativeSubscriptionProviderId; labelClassName?: string }) { const ui = useContext(Context), descriptor = NATIVE_SUBSCRIPTIONS.find((p) => p.providerId === providerId)!; const status = ui.statuses.find((s) => s.providerId === providerId) ?? emptySubscriptionStatus(providerId); return
- s.pending)} onAction={() => ui.open(providerId)} /> + s.pending)} onAction={() => ui.open(providerId)} /> {status.error &&

{status.error}

}
; } export function SubscriptionConnections({ setup = false }: { setup?: boolean }) { - return
{NATIVE_SUBSCRIPTIONS.map((p) => )}
; + // Providers uses ordinary settings-label typography. The setup surface + // keeps its existing compact labels; dialogs and actions are untouched. + const labelClassName = setup ? undefined : "font-prose text-(length:--lh-prose-size) text-foreground"; + return
{NATIVE_SUBSCRIPTIONS.map((p) => )}
; } export function NativeSubscriptionBackgroundAccess({ provider }: { provider: string | undefined }) { const ui = useContext(Context), descriptor = NATIVE_SUBSCRIPTIONS.find((p) => p.logicalId === provider); diff --git a/docs/development.md b/docs/development.md index d5465bc..f775f61 100644 --- a/docs/development.md +++ b/docs/development.md @@ -176,7 +176,7 @@ pricing-generation step: Pi AI's catalog and public calculator are the authority **Practice (problem sets).** A node selection resolves to the STORED reasoning chains it fully contains (a chain rides iff every member is selected; unchained nodes ride as singletons); the `/practice` creation dialog picks a birth-session cluster (the graph view's own groups) and freezes its propositions + chains into a self-contained `ProblemSet`. Three named-level axes steer generation (ladder rung 0–4, cue distance 0–2, Theory|Application — never a number to interpolate; defaults Direct exercise · Adjacent context · Theory), plus an optional free-form note the agents weigh but need not follow. Creation has two entries: the cluster picker on `/practice`, or the graph view's chain table — a full-bleed sortable table of every reasoning chain, grouped by birth cluster under the shared top bar, selecting chips, chains, or whole clusters, whose floating selection bar shows the exact `N nodes → M chains` the agents will receive — handing a free-form node selection to the same dialog — and carries the fenced Delete action, making it the graph's management view (whole-cluster cleanup through the same full-death confirm dialog). A partitioner agent covers every chain exactly once, one generator per partition writes problems against the owner's problem-engineering framework (commit-before-pose: statement + solution + rubric together), and grading is **strengthening, not assessment** — one attempt per problem with its own submit button, instant solution/rubric reveal, then a score 0–10 with brief reasoning landing live; nothing ever written back to the graph. Runs are registry-backed background jobs on the activity dock (no steer, never wakes the teacher); sets live in `data/practice`, crash-swept on boot. -**Dictation.** Optional speech-to-text: the server mints short-lived Deepgram tokens; audio streams browser→Deepgram over WebSocket, never through the server. Settings → Providers → Dictation → Language (below the Deepgram key) offers Nova-3's documented monolingual languages/locales or `language=multi` for automatic multilingual transcription. The app-wide `dictation.language` preference persists through the existing settings record and `PUT /api/settings/dictation`; absent preferences resolve to English (`en`). The settings control locks from connecting through stopping, and reconnects keep the run's original language. A shared preference controller publishes confirmed PUT responses to every reader immediately; the confirmed preference is cached across page navigation and reopening Settings, with no polling or per-recording read. Explicit external dictation-settings notifications invalidate the cache, and capture waits for a successful refresh. Navigating away cancels the run, invalidates pending startup/reconnect work and releases its microphone/socket without appending into another page's input. All other streaming parameters remain unchanged. The picker uses the model dropdown's shared `SearchMenuList` and `MenuShell`: the same fuzzy search, arrow navigation, Enter selection, row styling and scrolling. Its compact mono `SettingsMenuTrigger` is shared with Agent and Appearance; a muted setup line below the key and language rows links the Member-or-higher key-role requirement. +**Dictation.** Optional speech-to-text: the server mints short-lived Deepgram tokens; audio streams browser→Deepgram over WebSocket, never through the server. Settings → Providers → Dictation → Language (below the Deepgram key) offers Nova-3's documented monolingual languages/locales or `language=multi` for automatic multilingual transcription. The app-wide `dictation.language` preference persists through the existing settings record and `PUT /api/settings/dictation`; absent preferences resolve to English (`en`). The settings control locks from connecting through stopping, and reconnects keep the run's original language. A shared preference controller publishes confirmed PUT responses to every reader immediately; the confirmed preference is cached across page navigation and reopening Settings, with no polling or per-recording read. Explicit external dictation-settings notifications invalidate the cache, and capture waits for a successful refresh. Navigating away cancels the run, invalidates pending startup/reconnect work and releases its microphone/socket without appending into another page's input. All other streaming parameters remain unchanged. The picker uses the model dropdown's shared `SearchMenuList` and `MenuShell`: the same fuzzy search, arrow navigation, Enter selection, row styling and scrolling. Its compact mono `SettingsMenuTrigger` is shared with Agent and Appearance; a muted setup line below the key and language rows links the Deepgram Console and states the Member-or-higher key-role requirement. ## Testing diff --git a/tests/dictation-picker.test.tsx b/tests/dictation-picker.test.tsx index 8e578ea..97cb1d6 100644 --- a/tests/dictation-picker.test.tsx +++ b/tests/dictation-picker.test.tsx @@ -229,10 +229,12 @@ for (const source of ["none", "file", "env"] as const) test(`Language is below t assert.equal(rows[0]!.getAttribute("data-testid"), "key-deepgram_api_key"); assert.equal(rows[1]!.getAttribute("data-testid"), "row-dictation-language"); assert.equal(rows[2]!.getAttribute("data-testid"), "dictation-setup-hint"); - assert.equal(byTest("dictation-setup-hint").textContent, "Create a Deepgram key with the Member role or higher, then add it above."); + assert.equal(byTest("dictation-setup-hint").textContent, "Create a Deepgram API key with the Member role or higher, then add it above."); assert.match(byTest("dictation-setup-hint").className, /text-muted-foreground/); - assert.equal(byTest("dictation-setup-hint").querySelector("a")!.href, "https://developers.deepgram.com/reference/auth/tokens/grant"); - assert.match(rows[1]!.querySelector("span")!.className, /font-ui text-xs/); + assert.equal(byTest("dictation-setup-hint").querySelector("a")!.href, "https://console.deepgram.com/"); + assert.equal(byTest("dictation-setup-hint").querySelector("a")!.textContent, "Deepgram API key"); + assert.equal(byTest("dictation-setup-hint").querySelectorAll("a").length, 1); + assert.match(rows[1]!.querySelector(".font-prose")!.className, /text-\(length:--lh-prose-size\)/); assert.equal(trigger().disabled, false, "language is independent of key ownership"); await click(byTest("key-deepgram_api_key-row")); if (source !== "env") assert.ok(byTest("key-deepgram_api_key-editor")); diff --git a/tests/render-settings.test.tsx b/tests/render-settings.test.tsx index 5cbb123..370bb12 100644 --- a/tests/render-settings.test.tsx +++ b/tests/render-settings.test.tsx @@ -21,6 +21,7 @@ import { SettingsDialogBody, SettingsDialogProvider } from "../components/settin import { KEYBOARD_ACTIONS } from "../src/core/index.ts"; import { DictationLanguageSetting } from "../components/dictation-language.tsx"; import { SettingsMenuTrigger } from "../components/settings/controls.tsx"; +import { SubscriptionConnections } from "../components/settings/subscription-connections.tsx"; /** The full opening tag of the element carrying a testid — attribute * order in React output is not contractual, so assertions run on the @@ -89,6 +90,25 @@ describe("the shared settings dropdown trigger", () => { }); }); +describe("Providers settings-label typography", () => { + it("Language uses the standard sans SettingsRow label without changing its mono trigger", () => { + const html = renderToStaticMarkup(); + assert.ok(html.includes('Language')); + assert.ok(tagOf(html, "dictation-language").includes("font-ui text-xs")); + }); + + it("only Providers subscription labels use sans; setup labels retain their compact mono styling", () => { + const settings = renderToStaticMarkup(); + const setup = renderToStaticMarkup(); + for (const label of ["ChatGPT", "GitHub Copilot", "Grok"]) { + assert.ok(settings.includes(`${label}`), `${label}: settings sans label`); + assert.ok(setup.includes(`${label}`), `${label}: unchanged setup label`); + } + assert.equal((settings.match(/>Connect<\/button>/g) ?? []).length, 3); + assert.equal((setup.match(/>Connect<\/button>/g) ?? []).length, 3); + }); +}); + describe("AgentSection (thin SSR)", () => { it("renders the three model SLOT rows (slots-only, fix wave: unpicked reads —, no Default option)", () => { const html = renderToStaticMarkup( {}} onModelsChange={() => {}} />);