Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions apps/desktop/src/components/settings/JevSettingsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export function JevSettingsCard({
return (
<SettingsCard
title={t("settings.jevTitle")}
description={t("settings.jevDescription")}
description={`${t("settings.jevDescription")}\n\n${t("settings.jevPrivacyNotice")}`}
>
<SettingsRow
title={t("settings.jevEnable")}
Expand Down Expand Up @@ -129,7 +129,6 @@ export function JevSettingsCard({
/>
</div>
</SettingsRow>
<p className="jev-settings-privacy-note">{t("settings.jevPrivacyNotice")}</p>
</SettingsCard>
);
}
48 changes: 24 additions & 24 deletions apps/desktop/src/components/settings/ModelConfigPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Model configuration tab: image model selection, the AI service list, and the
* models.dev enrichment snapshot status.
* Model configuration tab: AI services, Jev settings, image model selection,
* and the models.dev enrichment snapshot status.
*
* API services, plugin-declared services and vendor subscription accounts
* share one list (D625). An account row still lives and dies through the
Expand Down Expand Up @@ -401,28 +401,6 @@ export function ModelConfigPage() {

return (
<div className="settings-stack model-config-page">
{imageGenerationCandidates.length > 0 ? (
<section className="settings-card-block">
<div className="settings-panel model-default-panel">
<ImageGenerationModelRow
settings={settings}
providers={providers}
busy={changingImageModel}
onChange={setImageGenerationDefault}
/>
</div>
</section>
) : null}

<JevSettingsCard
settings={settings}
onConfigure={() => {
setJevSetup(true);
setSetupFor("");
}}
statusRevision={jevStatusRevision}
/>

<section className="settings-card-block">
<div className="model-config-section-head">
<div className="settings-card-heading-line">
Expand Down Expand Up @@ -547,6 +525,28 @@ export function ModelConfigPage() {
</Button>
</div>

<JevSettingsCard
settings={settings}
onConfigure={() => {
setJevSetup(true);
setSetupFor("");
}}
statusRevision={jevStatusRevision}
/>

{imageGenerationCandidates.length > 0 ? (
<section className="settings-card-block">
<div className="settings-panel model-default-panel">
<ImageGenerationModelRow
settings={settings}
providers={providers}
busy={changingImageModel}
onChange={setImageGenerationDefault}
/>
</div>
</section>
) : null}

{setupFor !== null ? (
<ProviderSetupDialog
key={setupFor}
Expand Down
6 changes: 5 additions & 1 deletion apps/desktop/src/styles/model-config.css
Original file line number Diff line number Diff line change
Expand Up @@ -1347,7 +1347,11 @@
.model-config-page {
display: flex;
flex-direction: column;
gap: 16px;
gap: 20px;
}

.model-config-page > .settings-card-block {
margin-bottom: 0;
}

.model-config-section-head {
Expand Down
8 changes: 0 additions & 8 deletions apps/desktop/src/styles/settings.css
Original file line number Diff line number Diff line change
Expand Up @@ -1064,14 +1064,6 @@
min-height: var(--ds-field-height);
}

.jev-settings-privacy-note {
margin: 0;
padding: 0 16px 12px;
color: var(--ds-text-secondary);
font-size: var(--text-sm);
line-height: var(--leading-body);
}

/*
The Jev row's control column: two actions and the switch. The row metric
above only matches a bare toggle, so the group keeps its own height.
Expand Down
8 changes: 8 additions & 0 deletions apps/desktop/test/jev-settings-user-path.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ window.jevSettingsProbe = async () => {
() => document.body.innerText.includes("API key saved securely"),
"the card did not appear for a stored key",
);
const inlinePrivacyNoticeHidden = !document.body.innerText.includes("sent directly to TypeSafe");
const privacyNoticeAvailableFromHelp = [...document.querySelectorAll("button")].some(
(candidate) => candidate.getAttribute("aria-label")?.includes("sent directly to TypeSafe"),
);
const toggle = () => document.querySelector('[role="switch"][aria-label="Enable Jev for Agent"]');
if (!toggle()) throw new Error("the Jev switch is missing");
const toggleReadyWithKey = !toggle().disabled;
Expand Down Expand Up @@ -149,6 +153,8 @@ window.jevSettingsProbe = async () => {
return {
hiddenUntilAdded,
toggleReadyWithKey,
inlinePrivacyNoticeHidden,
privacyNoticeAvailableFromHelp,
replaceOffered,
removeOffered,
configureOpened,
Expand Down Expand Up @@ -247,6 +253,8 @@ app.whenReady().then(async () => {
assert.deepEqual(result, {
hiddenUntilAdded: true,
toggleReadyWithKey: true,
inlinePrivacyNoticeHidden: true,
privacyNoticeAvailableFromHelp: true,
replaceOffered: true,
removeOffered: true,
configureOpened: true,
Expand Down
12 changes: 12 additions & 0 deletions apps/desktop/test/provider-model-config.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ const apiSource = await read("../src/lib/api.ts");
const catalogContractSource = await read("../../../packages/shared/src/model-catalog.ts");
const styles = await loadStyles();

test("model configuration places AI services before Jev and image generation", () => {
const services = pageSource.indexOf('className="model-config-section-head"');
const catalogStatus = pageSource.indexOf('className="model-catalog-status"');
const jev = pageSource.indexOf("<JevSettingsCard");
const image = pageSource.indexOf("<ImageGenerationModelRow");

assert.ok(services >= 0, "the AI services section is present");
assert.ok(catalogStatus > services, "catalog actions stay with AI services");
assert.ok(jev > catalogStatus, "Jev follows AI services");
assert.ok(image > jev, "image generation follows Jev");
});

test("the model list comes from the AI service, not from a browsable catalog", () => {
assert.match(hookSource, /api\.listProviderModels\(/);
// The rejected surface and its host-side search must be gone.
Expand Down
12 changes: 9 additions & 3 deletions docs/spec/04-ux/06-settings-ia.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,10 @@ a usage tab.
override is folded into it when the map is read (D438, D439)

### Model configuration (`agent` tab)
- The model configuration page groups its sections in this order: **AI
services** (including model-catalog status and refresh), **Jev**, then the
image-generation model selector. The sections use one consistent vertical
spacing rhythm.
- **Defaults** card: a compact settings row shows the provider name and exact
model ID beneath the Default model label. A quiet Change action opens the
picker without duplicating the current value. The picker groups model-level
Expand Down Expand Up @@ -557,9 +561,11 @@ contract modes are intent boundaries, not strict read-only security profiles.
and the actions that replace or remove the key; removing the key takes the
switch down first, then the card. The key itself is entered only in the
service dialog, which is also where it is checked. It stays in Host secure
storage; the renderer only learns whether one exists.
- Explain that `JevClassify` sends only the state and questions the Agent passes
to TypeSafe, and warn users not to include secrets or personal information.
storage; the renderer only learns whether one exists. Keep the card free of
persistent explanatory paragraphs: its heading help icon provides integration
and data-handling details, while the switch help icon explains Agent-mode
availability. The setup form keeps the privacy notice visible before a key
is stored.
- The tool is on demand and unavailable in Plan and Goal modes. Removing the
key turns Jev off first, then deletes it.

Expand Down
10 changes: 8 additions & 2 deletions docs/spec/06-delivery/04-e2e-test-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -17011,7 +17011,10 @@ host-created files. The full app's file-preview viewer is covered separately.
its own Classifiers group and absent when an existing row changes service,
and that no Jev card is on the model configuration page yet. 2) Open the Jev
form, paste a sentinel key and Check and save: the fixture answers the check,
the key reaches Host secure storage, Jev is on, and the card appears.
the key reaches Host secure storage, Jev is on, and the card appears. Confirm
AI services (with catalog status and refresh) precede Jev and image generation,
and that the card's privacy explanation is available from its heading help
control instead of as a persistent paragraph.
3) Resolve a session launch with Jev enabled, then disabled and in Plan mode.
4) Through the runtime's deferred catalog, request Jev in Agent mode and
inspect Plan/Goal catalogs. 5) Call `JevClassify` with one choice, one score
Expand All @@ -17025,7 +17028,10 @@ host-created files. The full app's file-preview viewer is covered separately.
the refusal is reported with TypeSafe's status. The card is on the page only
once Jev has been added, and it leaves when the key does. The UI never returns
the key to settings state, and removal disables Jev before deleting it. Only
an enabled Agent launch reads the key and passes it ephemerally to the sidecar.
the Jev title, key status and controls occupy the card; explanatory privacy
copy appears on demand from the heading help control. AI services stay above
Jev, followed by image generation. An enabled Agent launch reads the key and
passes it ephemerally to the sidecar.
`JevClassify` appears in the Agent's deferred catalog only with a key and
never in Plan or Goal. Closing the dialog cancels an in-flight check the same
way a refused key does: nothing stored, nothing enabled. The fixture receives
Expand Down
Loading