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
159 changes: 159 additions & 0 deletions apps/desktop/src/main/__tests__/relay-thinking-bulk.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import {
applyBulkThinkingLevel,
bulkThinkingLevelStates,
relayProfileWithThinkingLevels,
} from '../../renderer/settings/relay-thinking-bulk.js';
import { DECLARABLE_RELAY_THINKING_LEVELS } from '@maka/core/model-thinking';
import type { RelayModelProfile } from '@maka/core/model-thinking';

const MODELS = ['alpha', 'beta', 'gamma'];

test('a level nobody declares reads as absent, and one everybody declares ticks the box', () => {
const draft: Record<string, RelayModelProfile> = {
alpha: { thinkingLevels: ['high'] },
beta: { thinkingLevels: ['high'] },
gamma: { thinkingLevels: ['high'] },
};
const states = bulkThinkingLevelStates(MODELS, draft, ['high', 'low']);
assert.deepEqual(states[0], { level: 'high', declaredCount: 3, total: 3, checked: true });
assert.deepEqual(states[1], { level: 'low', declaredCount: 0, total: 3, checked: false });
});

test('partial coverage does not tick the box — only the count separates it from none', () => {
// The box is the affordance for "give this to everyone". Ticking at
// partial coverage would make the next click take the level AWAY from the
// rows that have it, which is the opposite of what the user just asked for.
const draft: Record<string, RelayModelProfile> = {
alpha: { thinkingLevels: ['high'] },
beta: { thinkingLevels: ['high'] },
};
const [state] = bulkThinkingLevelStates(MODELS, draft, ['high']);
assert.equal(state?.checked, false);
assert.equal(state?.declaredCount, 2);
assert.equal(state?.total, 3);
});

test('a repeated model id is one model, not two', () => {
const draft: Record<string, RelayModelProfile> = { alpha: { thinkingLevels: ['high'] } };
const [state] = bulkThinkingLevelStates(['alpha', 'alpha'], draft, ['high']);
assert.deepEqual(state, { level: 'high', declaredCount: 1, total: 1, checked: true });
});

test('an empty selection ticks nothing rather than reading as fully covered', () => {
// 0 === 0 is the trap: `declaredCount === total` is true of an empty
// selection, which would present every level as declared everywhere.
for (const state of bulkThinkingLevelStates([], {}, DECLARABLE_RELAY_THINKING_LEVELS)) {
assert.equal(state.checked, false);
assert.equal(state.total, 0);
}
});

test('ticking a level adds it to every model, including ones with no entry yet', () => {
const next = applyBulkThinkingLevel(MODELS, { alpha: { vision: true } }, 'high', true);
assert.deepEqual(next, {
alpha: { vision: true, thinkingLevels: ['high'] },
beta: { thinkingLevels: ['high'] },
gamma: { thinkingLevels: ['high'] },
});
});

test('a bulk add leaves the levels a model already declared alone', () => {
const next = applyBulkThinkingLevel(
MODELS,
{ alpha: { thinkingLevels: ['low', 'medium'] } },
'high',
true,
);
assert.deepEqual(next.alpha?.thinkingLevels, ['low', 'medium', 'high']);
});

test('ticking a level a model already has does not duplicate it', () => {
const next = applyBulkThinkingLevel(
['alpha'],
{ alpha: { thinkingLevels: ['high'] } },
'high',
true,
);
assert.deepEqual(next.alpha?.thinkingLevels, ['high']);
});

test('unticking removes only that level, and only from the selection', () => {
const next = applyBulkThinkingLevel(
['alpha', 'beta'],
{
alpha: { thinkingLevels: ['low', 'high'] },
beta: { thinkingLevels: ['high'] },
gamma: { thinkingLevels: ['high'] },
},
'high',
false,
);
assert.deepEqual(next.alpha?.thinkingLevels, ['low']);
// beta held nothing but `high`: an entry with no keys left is not an
// entry, or the row keeps reading as declared and 保存 stays armed.
assert.equal('beta' in next, false);
// gamma is outside the selection — a bulk edit is scoped to the rows the
// control sits above.
assert.deepEqual(next.gamma?.thinkingLevels, ['high']);
});

test('unticking keeps the other declarations on a model whose levels it empties', () => {
const next = applyBulkThinkingLevel(
['alpha'],
{ alpha: { thinkingLevels: ['high'], vision: true, contextWindow: 128_000 } },
'high',
false,
);
assert.deepEqual(next.alpha, { vision: true, contextWindow: 128_000 });
});

test('a bulk edit does not reshuffle the draft under the rows being edited', () => {
const next = applyBulkThinkingLevel(
['gamma', 'alpha'],
{ alpha: { vision: true }, beta: { vision: false }, gamma: { vision: true } },
'high',
true,
);
assert.deepEqual(Object.keys(next), ['alpha', 'beta', 'gamma']);
});

test('a model id colliding with a prototype key stores an entry, not a prototype write', () => {
// Ids come off the relay's /models response. `draft['constructor']` on a
// plain object answers with Object's constructor rather than "absent",
// and assigning `__proto__` writes through the prototype.
const ids = ['__proto__', 'constructor', 'toString'];
const next = applyBulkThinkingLevel(ids, {}, 'high', true);
for (const id of ids) {
assert.deepEqual(Object.getOwnPropertyDescriptor(next, id)?.value, {
thinkingLevels: ['high'],
});
}
assert.equal(({} as Record<string, unknown>).thinkingLevels, undefined);
// And the read side sees all three as declaring it, rather than answering
// "absent" for keys that resolve on Object.prototype.
const [state] = bulkThinkingLevelStates(ids, next, ['high']);
assert.deepEqual(state, { level: 'high', declaredCount: 3, total: 3, checked: true });
});

test('an emptied declaration collapses to undefined so the caller drops the key', () => {
assert.equal(relayProfileWithThinkingLevels({ thinkingLevels: ['high'] }, []), undefined);
assert.equal(relayProfileWithThinkingLevels({ thinkingLevels: ['high'] }, undefined), undefined);
assert.deepEqual(relayProfileWithThinkingLevels({ vision: true }, ['high']), {
vision: true,
thinkingLevels: ['high'],
});
});

test('clearing a level a model never declared leaves the draft untouched', () => {
const draft: Record<string, RelayModelProfile> = { alpha: { vision: true } };
const next = applyBulkThinkingLevel(MODELS, draft, 'high', false);
assert.deepEqual(next, { alpha: { vision: true } });
});

test('the bulk edit does not mutate the draft it was handed', () => {
const draft: Record<string, RelayModelProfile> = { alpha: { thinkingLevels: ['low'] } };
applyBulkThinkingLevel(MODELS, draft, 'high', true);
assert.deepEqual(draft, { alpha: { thinkingLevels: ['low'] } });
});
11 changes: 11 additions & 0 deletions apps/desktop/src/renderer/locales/settings-provider-copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ const zhCapabilitiesCopy = {
thinkingEffortHelp: '勾选需要的思考强度档位,不勾选即为不声明。',
thinkingUndeclared: '未声明',
thinkingSelectedCount: (count: number) => `已选择 ${count} 个`,
thinkingBulk: '批量设置思考档位',
thinkingBulkHelp: '勾选写入下方全部已启用模型,取消勾选则从全部模型移除;其余档位不受影响。',
thinkingBulkTrigger: '应用到全部模型',
thinkingBulkCoverage: (declared: number, total: number) =>
declared === 0 ? '全部未声明' : `${declared}/${total} 个模型`,
visionInput: '视觉输入(vision)',
visionInputHelp: '「自动」跟随内置元数据;「启用/禁用」是显式声明,覆盖自动判断。',
visionAuto: '自动',
Expand All @@ -34,6 +39,12 @@ const enCapabilitiesCopy = {
thinkingEffortHelp: 'Tick the thinking levels this model supports; none ticked means undeclared.',
thinkingUndeclared: 'Undeclared',
thinkingSelectedCount: (count: number) => `${count} selected`,
thinkingBulk: 'Set thinking levels for all models',
thinkingBulkHelp:
'Ticking adds the level to every enabled model below; unticking removes it from all of them. Other levels are left alone.',
thinkingBulkTrigger: 'Apply to all models',
thinkingBulkCoverage: (declared: number, total: number) =>
declared === 0 ? 'On no model' : `On ${declared} of ${total} models`,
visionInput: 'Vision input',
visionInputHelp: 'Auto follows built-in metadata; Enabled/Disabled overrides it explicitly.',
visionAuto: 'Auto',
Expand Down
76 changes: 74 additions & 2 deletions apps/desktop/src/renderer/settings/provider-connection-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
savedRequestHeaderDrafts,
type RequestHeaderDraft,
} from './request-customization-editor';
import { bulkThinkingLevelStates } from './relay-thinking-bulk';

export function ConnectionDetail(props: ConnectionDetailProps) {
const defaults = PROVIDER_DEFAULTS[props.connection.providerType];
Expand Down Expand Up @@ -155,6 +156,7 @@ function ConnectionDetailInner(props: ConnectionDetailProps) {
relayProfileDraft,
hasRelayProfileChanges,
setDraftThinkingLevels,
setDraftThinkingLevelForAll,
setDraftVision,
setDraftContextWindow,
setDraftServiceTier,
Expand Down Expand Up @@ -186,6 +188,10 @@ function ConnectionDetailInner(props: ConnectionDetailProps) {
!hasModelMetadata(connection.providerType, modelId),
);
const showsCapabilities = capabilityModelIds.length > 0;
// The bulk control shares the 思考档位 row's relay gate — it edits exactly
// that row — and needs repetition to be worth a control at all: with one
// row it would be a second widget doing what the row under it already does.
const showsThinkingBulk = isRelay && capabilityModelIds.length > 1;
// One row is a form at a time, the way the settings-sidebar template does it.
// Opening a row discards the other's draft: leaving an abandoned draft in
// state meant it reappeared when the user came back to that row, and — until
Expand Down Expand Up @@ -568,6 +574,68 @@ function ConnectionDetailInner(props: ConnectionDetailProps) {
<Divider />
<DetailSection title={copy.capabilities} description={copy.capabilitiesHelp}>
<VStack gap={4}>
{/* One control for the whole table, above the rows it edits. A
relay usually fronts one model family that accepts the same
reasoning_effort values, and declaring that per row was
models × levels clicks for a single fact. */}
{showsThinkingBulk && (
<CapabilityRow label={copy.thinkingBulk} description={copy.thinkingBulkHelp}>
<DropdownMenu
button={{
variant: 'secondary',
size: 'sm',
label: copy.thinkingBulkTrigger,
// Starts with the visible label so voice control can
// act on what the button says, then names the thing it
// sets — the row's heading is beside it visually but is
// not attached to the control.
'aria-label': `${copy.thinkingBulkTrigger} — ${copy.thinkingBulk}`,
isDisabled: allActionsBusy,
}}
hasChevron
menuWidth={240}
>
{/* The declarable vocabulary, which is the whole of what
a draft can hold: the seed sanitizes through
`normalizeRelayModelProfiles`, so `off` — a disable
wire no generic relay is presumed to speak — cannot
reach a row here either. */}
{bulkThinkingLevelStates(
capabilityModelIds,
relayProfileDraft,
DECLARABLE_RELAY_THINKING_LEVELS,
).map((state) => (
<DropdownMenuCheckboxItem
key={state.level}
label={state.level}
/* The box only ticks at full coverage, so the count
is the sole place partial coverage is legible —
without it "3 of 5 declare high" and "none do"
present as the same empty box. */
description={copy.thinkingBulkCoverage(state.declaredCount, state.total)}

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.

[P2] Expose partial model coverage to assistive technology

Thanks for showing the partial n/total state visually—it makes the batch behavior much easier to understand. Because this item also supplies an aria-label, however, the screen reader does not receive that visible description and cannot distinguish "none of the models declare this level" from "some models already declare it."

Could we expose thinkingBulkCoverage(...) through aria-description or an aria-describedby relationship? A focused accessibility assertion for the partial state would be enough; the existing pure-function coverage does not need to be expanded further.

aria-label={`${copy.thinkingBulk} ${state.level}`}
/* The item's `description` is visible text only — the
component does not wire it to `aria-describedby`,
and this item's own `aria-label` replaces the name
the description would otherwise have joined. Without
this, "1/4 个模型" and "全部未声明" both reach a screen
reader as an unchecked box with the same name, which
is exactly the partial state the count exists to
show. */
aria-description={copy.thinkingBulkCoverage(
state.declaredCount,
state.total,
)}
value={state.checked}
onChange={(checked) => {
setDraftThinkingLevelForAll(capabilityModelIds, state.level, checked);
}}
isDisabled={allActionsBusy}
/>
))}
</DropdownMenu>
</CapabilityRow>
)}
{capabilityModelIds.map((modelId, modelIndex) => {
const declared: RelayModelProfile | undefined = relayProfileDraft[modelId];
// Vision resolves to one of three states: absent (Auto),
Expand Down Expand Up @@ -598,7 +666,7 @@ function ConnectionDetailInner(props: ConnectionDetailProps) {
);
return (
<VStack key={modelId} gap={3}>
{modelIndex > 0 && <Divider />}
{(modelIndex > 0 || showsThinkingBulk) && <Divider />}
<Text weight="semibold">{modelId}</Text>
{/* One row per declaration: label + what it does on the
left, one compact control on the right (the 模型功能
Expand Down Expand Up @@ -677,7 +745,11 @@ function ConnectionDetailInner(props: ConnectionDetailProps) {
<DeclaredContextWindowField
declared={declared?.contextWindow}
disabled={allActionsBusy}
label={copy.contextWindow}
/* Named per model, like the three controls around it:
the visible label is the row's, but the field's own
name is all a screen reader gets, and every row in
the section carries the same one. */
label={`${copy.contextWindow} — ${modelId}`}
onCommit={(value) =>
setDraftContextWindow(modelId, value ?? undefined)
}
Expand Down
Loading