taskchef 8.3.1 → 8.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,7 +11,7 @@ vi.mock('@mantine/core', async () => {
11
11
  const block = ({ children }: { children?: import('react').ReactNode }) => createElement('div', null, children);
12
12
  return {
13
13
  MantineProvider: block, Tooltip: block, Badge: block, Group: block, Paper: block, SimpleGrid: block, Stack: block, Text: block, Title: block,
14
- Select: ({ 'aria-busy': busy, 'aria-label': label, data = [], disabled, onChange, readOnly, value }: { 'aria-busy'?: boolean; 'aria-label': string; data?: Array<string | { value: string; label: string }>; disabled?: boolean; onChange: (value: string | null) => void; readOnly?: boolean; value: string | null }) => createElement('select', { 'aria-busy': busy, 'aria-label': label, disabled, 'aria-readonly': readOnly, value: value ?? '', onChange: (event: { currentTarget: { value: string } }) => onChange(event.currentTarget.value) }, [createElement('option', { key: '', value: '' }), ...data.map((item) => typeof item === 'string' ? createElement('option', { key: item, value: item }, item) : createElement('option', { key: item.value, value: item.value }, item.label))]),
14
+ Select: ({ 'aria-busy': busy, 'aria-label': label, data = [], disabled, onChange, readOnly, value }: { 'aria-busy'?: boolean; 'aria-label': string; data?: Array<string | { value: string; label: string; disabled?: boolean }>; disabled?: boolean; onChange: (value: string | null) => void; readOnly?: boolean; value: string | null }) => createElement('select', { 'aria-busy': busy, 'aria-label': label, disabled, 'aria-readonly': readOnly, value: value ?? '', onChange: (event: { currentTarget: { value: string } }) => onChange(event.currentTarget.value) }, [createElement('option', { key: '', value: '' }), ...data.map((item) => typeof item === 'string' ? createElement('option', { key: item, value: item }, item) : createElement('option', { key: item.value, value: item.value, disabled: item.disabled }, item.label))]),
15
15
  Alert: ({ children }: { children?: import('react').ReactNode }) => createElement('div', { role: 'alert' }, children),
16
16
  ActionIcon: ({ children, loading, onClick, 'aria-label': label, ...props }: { children?: import('react').ReactNode; loading?: boolean; onClick: () => void; 'aria-label': string; className?: string }) => createElement('button', { ...props, disabled: loading, onClick, 'aria-label': label }, children),
17
17
  };
@@ -112,3 +112,52 @@ test('serializes role saves so an older response cannot replace a newer selectio
112
112
  fireEvent.change(reviewerModel, { target: { value: 'gpt-two' } });
113
113
  await waitFor(() => expect(apiMocks.updateModelRole).toHaveBeenCalledTimes(2));
114
114
  });
115
+
116
+ const effortRole = { role: 'planner', source: null, model: 'gpt-one', effort: 'low', status: 'configured', problems: [] as string[] };
117
+ const effortProfile = { id: 'personal', roles: [effortRole], problems: [], modelOptions: [
118
+ { value: 'gpt-one', label: 'GPT One', efforts: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'] },
119
+ { value: 'gpt-two', label: 'GPT Two', efforts: ['max', 'medium'] },
120
+ ] };
121
+
122
+ test.each([['Light', 'low'], ['Medium', 'medium'], ['High', 'high'], ['Extra High', 'xhigh'], ['Ultra', 'ultra']])('saves %s as native %s and omits max choices', async (label, native) => {
123
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => ({ profiles: [effortProfile] }) }));
124
+ apiMocks.updateModelRole.mockResolvedValue({ profile: { ...effortProfile, roles: [{ ...effortRole, effort: native }] } });
125
+ render(<ModelSettings />);
126
+ const effort = await screen.findByRole('combobox', { name: 'planner reasoning effort' });
127
+ expect(screen.getByRole('option', { name: label })).toHaveValue(native);
128
+ expect(screen.queryByRole('option', { name: /max/i })).not.toBeInTheDocument();
129
+ fireEvent.change(effort, { target: { value: native } });
130
+ await waitFor(() => expect(apiMocks.updateModelRole).toHaveBeenCalledWith('planner', 'gpt-one', native));
131
+ });
132
+
133
+ test('model changes filter supported efforts and never default to max', async () => {
134
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => ({ profiles: [effortProfile] }) }));
135
+ apiMocks.updateModelRole.mockResolvedValue({ profile: { ...effortProfile, roles: [{ ...effortRole, model: 'gpt-two', effort: 'medium' }] } });
136
+ render(<ModelSettings />);
137
+ fireEvent.change(await screen.findByRole('combobox', { name: 'planner model' }), { target: { value: 'gpt-two' } });
138
+ await waitFor(() => expect(apiMocks.updateModelRole).toHaveBeenCalledWith('planner', 'gpt-two', 'medium'));
139
+ expect(screen.getByRole('option', { name: 'Medium' })).toHaveValue('medium');
140
+ expect(screen.queryByRole('option', { name: 'Light' })).not.toBeInTheDocument();
141
+ expect(screen.queryByRole('option', { name: /max/i })).not.toBeInTheDocument();
142
+ });
143
+
144
+ test('saved max is displayed without being selectable and survives refresh and supported model changes', async () => {
145
+ const profile = { ...effortProfile, roles: [{ ...effortRole, effort: 'max' }] };
146
+ vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => ({ profiles: [profile] }) }));
147
+ apiMocks.updateModelRole.mockResolvedValue({ profile: { ...profile, roles: [{ ...effortRole, model: 'gpt-two', effort: 'max' }] } });
148
+ render(<ModelSettings />);
149
+ const effort = await screen.findByRole('combobox', { name: 'planner reasoning effort' });
150
+ expect(effort).toHaveValue('max');
151
+ expect(screen.getByRole('option', { name: 'Max (current setting)' })).toBeDisabled();
152
+ fireEvent.click(screen.getByRole('button', { name: 'Refresh' }));
153
+ await waitFor(() => expect(fetch).toHaveBeenCalledTimes(2));
154
+ expect(apiMocks.updateModelRole).not.toHaveBeenCalled();
155
+ await waitFor(() => expect(effort).toHaveAttribute('aria-readonly', 'false'));
156
+ fireEvent.change(screen.getByRole('combobox', { name: 'planner model' }), { target: { value: 'gpt-two' } });
157
+ await waitFor(() => expect(apiMocks.updateModelRole).toHaveBeenCalledWith('planner', 'gpt-two', 'max'));
158
+ await waitFor(() => expect(effort).toHaveAttribute('aria-readonly', 'false'));
159
+ apiMocks.updateModelRole.mockResolvedValue({ profile: { ...profile, roles: [{ ...effortRole, model: 'gpt-two', effort: 'medium' }] } });
160
+ fireEvent.change(effort, { target: { value: 'medium' } });
161
+ await waitFor(() => expect(apiMocks.updateModelRole).toHaveBeenLastCalledWith('planner', 'gpt-two', 'medium'));
162
+ expect(screen.queryByRole('option', { name: /max/i })).not.toBeInTheDocument();
163
+ });
@@ -3,6 +3,8 @@ import { IconRefresh } from '@tabler/icons-react';
3
3
  import { useEffect, useState } from 'react';
4
4
  import { updateModelRole } from '../api';
5
5
 
6
+ const effortLabels: Record<string, string> = { low: 'Light', medium: 'Medium', high: 'High', xhigh: 'Extra High', ultra: 'Ultra' };
7
+
6
8
  type ModelOption = { value: string; label: string; efforts: string[] };
7
9
  type Role = { role: string; source: string | null; displaySource?: string | null; model: unknown; effort: unknown; status: string; problems: string[] };
8
10
  type Profile = { id: string; roles: Role[]; problems: string[]; modelOptions?: ModelOption[] };
@@ -77,11 +79,14 @@ export function ModelSettings() {
77
79
  <Select label="Model" aria-label={`${role.role} model`} aria-busy={loading || saving !== null} data={profile.modelOptions ?? []} value={typeof role.model === 'string' ? role.model : null} readOnly={loading || saving !== null} allowDeselect={false} searchable onChange={(model) => {
78
80
  if (loading || saving || !model) return;
79
81
  const option = profile.modelOptions?.find((candidate) => candidate.value === model);
80
- const effort = typeof role.effort === 'string' && option?.efforts.includes(role.effort) ? role.effort : option?.efforts[0];
82
+ const effort = typeof role.effort === 'string' && option?.efforts.includes(role.effort) ? role.effort : option?.efforts.find((value) => value !== 'max');
81
83
  if (effort) void save(role, model, effort);
82
84
  }} />
83
- <Select label="Reasoning effort" aria-label={`${role.role} reasoning effort`} aria-busy={loading || saving !== null} data={(profile.modelOptions?.find((option) => option.value === role.model)?.efforts ?? []).map((value) => ({ value, label: value }))} value={typeof role.effort === 'string' ? role.effort : null} disabled={typeof role.model !== 'string'} readOnly={loading || saving !== null} allowDeselect={false} onChange={(effort) => {
84
- if (!loading && !saving && effort && typeof role.model === 'string') void save(role, role.model, effort);
85
+ <Select label="Reasoning effort" aria-label={`${role.role} reasoning effort`} aria-busy={loading || saving !== null} data={[
86
+ ...(profile.modelOptions?.find((option) => option.value === role.model)?.efforts ?? []).filter((value) => value !== 'max').map((value) => ({ value, label: effortLabels[value] ?? value })),
87
+ ...(role.effort === 'max' ? [{ value: 'max', label: 'Max (current setting)', disabled: true }] : []),
88
+ ]} value={typeof role.effort === 'string' ? role.effort : null} disabled={typeof role.model !== 'string'} readOnly={loading || saving !== null} allowDeselect={false} onChange={(effort) => {
89
+ if (!loading && !saving && effort && effort !== 'max' && typeof role.model === 'string') void save(role, role.model, effort);
85
90
  }} />
86
91
  </SimpleGrid>
87
92
  <Text size="sm" style={{ overflowWrap: 'anywhere' }}>Source: {role.displaySource ?? role.source ?? 'No role file'}</Text>