You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by zh...@apache.org on 2026/09/17 15:56:12 UTC
(maka) branch fix/5452-workhub-stale-model-repair created (now 8b4e31d607)
This is an automated email from the ASF dual-hosted git repository.
me2seeks pushed a change to branch fix/5452-workhub-stale-model-repair
in repository https://gitbox.apache.org/repos/asf/maka.git
at 8b4e31d607 test(workhub): await searchable model menu
This branch includes the following new commits:
new ed6fd85d05 feat(workhub): streamline delegated model repair
new 31e8eec665 Merge remote-tracking branch 'origin/main' into fix/5452-workhub-stale-model-repair
new a545089c89 refactor(workhub): use searchable model selector
new 8b4e31d607 test(workhub): await searchable model menu
The 4 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 03/04: refactor(workhub): use searchable model selector
Posted by zh...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
me2seeks pushed a commit to branch fix/5452-workhub-stale-model-repair
in repository https://gitbox.apache.org/repos/asf/maka.git
commit a545089c8915fcc9e5c42cdc79b2f8c86d748059
Author: me2seeks <me...@gmail.com>
AuthorDate: Thu Sep 17 23:37:37 2026 +0800
refactor(workhub): use searchable model selector
---
.../src/renderer/styles/interaction-prompts.css | 7 ---
.../target-model-selection.png | Bin 31561 -> 52770 bytes
packages/ui/src/form-interaction-prompt.tsx | 47 +++++++++++++++------
3 files changed, 35 insertions(+), 19 deletions(-)
diff --git a/apps/desktop/src/renderer/styles/interaction-prompts.css b/apps/desktop/src/renderer/styles/interaction-prompts.css
index 2d329bb243..fb6a45f68c 100644
--- a/apps/desktop/src/renderer/styles/interaction-prompts.css
+++ b/apps/desktop/src/renderer/styles/interaction-prompts.css
@@ -141,13 +141,6 @@
border-radius: var(--radius-control);
}
-/* The shared model wheel paints in a layer. Reserve its three-row footprint in
- forms so the wheel does not cover the request or the form actions. */
-.maka-form-interaction-prompt .maka-model-wheel-anchor[data-open='true'] {
- width: min(264px, 55vw);
- height: var(--maka-model-wheel-height);
-}
-
.maka-form-interaction-field-heading {
display: flex;
align-items: baseline;
diff --git a/docs/images/pr/workhub-stale-model-repair/target-model-selection.png b/docs/images/pr/workhub-stale-model-repair/target-model-selection.png
index 04bda72fe4..d265ce2cb5 100644
Binary files a/docs/images/pr/workhub-stale-model-repair/target-model-selection.png and b/docs/images/pr/workhub-stale-model-repair/target-model-selection.png differ
diff --git a/packages/ui/src/form-interaction-prompt.tsx b/packages/ui/src/form-interaction-prompt.tsx
index f4b1a86646..007ce2f8dc 100644
--- a/packages/ui/src/form-interaction-prompt.tsx
+++ b/packages/ui/src/form-interaction-prompt.tsx
@@ -21,6 +21,7 @@ import { useId, useRef, useState } from 'react';
import type { FormRequestEvent } from '@maka/core/events';
import type { InteractionFormField, InteractionFormResponse } from '@maka/core/interaction';
import { Button, CheckboxInput, RadioList, RadioListItem, Text, TextInput } from '@astryxdesign/core';
+import { Selector, type SelectorOptionType, type SelectorSection } from '@astryxdesign/core/Selector';
import { getConversationCopy } from './conversation-copy.js';
import {
buildInteractionFormResponse,
@@ -30,7 +31,7 @@ import {
} from './form-interaction-prompt-state.js';
import { useUiLocale } from './locale-context.js';
import { ChoicePanel } from './choice-panel.js';
-import { ModelWheelPicker } from './model-wheel-picker.js';
+import { renderModelPickerOption, renderModelPickerValue } from './model-picker-internals.js';
import { useMountedRef } from './use-mounted-ref.js';
export function FormInteractionPrompt(props: {
@@ -142,18 +143,19 @@ function ActiveFormInteractionPrompt(props: {
{props.request.fields.length === 1 && field.kind === 'single_select' && field.required ? (
<>
{field.presentation === 'model_picker' ? (
- <ModelWheelPicker
- options={field.options.map((option) => ({
- value: option.value,
- label: option.label,
- ...(option.description ? { heading: option.description } : {}),
- }))}
- value={typeof draft.value === 'string' ? draft.value : undefined}
- label={field.options.find((option) => option.value === draft.value)?.label ?? field.label}
- ariaLabel={field.label}
+ <Selector
+ label={field.label}
+ isLabelHidden
+ options={modelPickerOptions(field.options)}
+ value={typeof draft.value === 'string' ? draft.value : ''}
+ hasSearch
size="md"
- disabled={responsePending || props.stopPending}
- onValueChange={(value) => updateDraft(index, { ...draft, value })}
+ placement="above"
+ isDisabled={responsePending || props.stopPending}
+ className="maka-form-model-picker"
+ onChange={(value) => updateDraft(index, { ...draft, value })}
+ renderOption={renderModelPickerOption}
+ renderValue={renderModelPickerValue}
/>
) : (
<>
@@ -212,6 +214,27 @@ function ActiveFormInteractionPrompt(props: {
);
}
+function modelPickerOptions(
+ options: Extract<InteractionFormField, { kind: 'single_select' }>['options'],
+): SelectorOptionType[] {
+ const ungrouped = options.filter((option) => !option.description);
+ const groups = new Map<string, typeof options>();
+ for (const option of options) {
+ if (!option.description) continue;
+ groups.set(option.description, [...(groups.get(option.description) ?? []), option]);
+ }
+ return [
+ ...ungrouped.map(({ value, label }) => ({ value, label })),
+ ...[...groups].map(
+ ([title, choices]): SelectorSection => ({
+ type: 'section',
+ title,
+ options: choices.map(({ value, label }) => ({ value, label })),
+ }),
+ ),
+ ];
+}
+
function formFieldConstraint(
field: InteractionFormField,
copy: ReturnType<typeof getConversationCopy>['forms'],
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 02/04: Merge remote-tracking branch 'origin/main' into fix/5452-workhub-stale-model-repair
Posted by zh...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
me2seeks pushed a commit to branch fix/5452-workhub-stale-model-repair
in repository https://gitbox.apache.org/repos/asf/maka.git
commit 31e8eec66578a0421c539670ebc2514daecc57cb
Merge: ed6fd85d05 cd93f13da7
Author: me2seeks <me...@gmail.com>
AuthorDate: Thu Sep 17 23:34:17 2026 +0800
Merge remote-tracking branch 'origin/main' into fix/5452-workhub-stale-model-repair
apps/desktop/e2e-budget.json | 2 +-
apps/desktop/e2e/workhub-layout.spec.ts | 10 +-
.../connection-settings-locale-render.test.ts | 53 +++
.../main/__tests__/provider-add-submission.test.ts | 34 ++
.../__tests__/session-health-recovery-flow.test.ts | 21 +-
.../connection-settings/settings-provider-copy.ts | 15 +
.../renderer/features/workhub/ui/workhub-root.tsx | 2 +
.../settings/daily-review-settings-page.tsx | 16 +-
.../src/renderer/settings/provider-add-form.tsx | 74 +++-
.../renderer/settings/provider-add-submission.ts | 28 +-
.../src/renderer/settings/provider-display-copy.ts | 6 +-
.../renderer/settings/subagent-settings-page.tsx | 36 +-
apps/desktop/src/renderer/styles/composer.css | 1 -
.../desktop/src/renderer/styles/model-switcher.css | 44 ++-
apps/desktop/src/renderer/styles/workhub.css | 15 +-
.../stories/settings/settings-pages.stories.tsx | 37 +-
apps/desktop/stories/workhub.stories.tsx | 58 +---
docs/astryx-surface-file-inventory.md | 11 +-
docs/astryx-surface-file-inventory.paths | 1 +
docs/commandcode-cli-transport.md | 129 -------
packages/core/src/provider-registry.ts | 6 +-
.../commandcode-cli-language-model.test.ts | 33 +-
.../src/__tests__/provider-contract-overrides.ts | 45 +--
.../runtime/src/commandcode-cli-language-model.ts | 42 +--
packages/runtime/src/model-factory.ts | 10 +-
packages/runtime/src/test-connection.ts | 5 -
.../composer-model-picker-recovery.test.tsx | 89 +++--
.../src/__tests__/use-pending-selection.test.tsx | 159 +++++++++
packages/ui/src/chat-model-switcher.tsx | 384 ++++++++++++++-------
packages/ui/src/composer.tsx | 31 +-
packages/ui/src/model-picker-internals.tsx | 132 +++++++
packages/ui/src/model-picker.tsx | 96 ++++--
packages/ui/src/use-pending-selection.ts | 89 +++++
packages/ui/stories/model-picker.stories.tsx | 158 +++++----
34 files changed, 1236 insertions(+), 636 deletions(-)
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 01/04: feat(workhub): streamline delegated model repair
Posted by zh...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
me2seeks pushed a commit to branch fix/5452-workhub-stale-model-repair
in repository https://gitbox.apache.org/repos/asf/maka.git
commit ed6fd85d05bff30b46ab730b70aa2ebdb871b6b9
Author: me2seeks <me...@gmail.com>
AuthorDate: Thu Sep 17 23:33:52 2026 +0800
feat(workhub): streamline delegated model repair
---
.../src/renderer/styles/interaction-prompts.css | 7 ++
apps/desktop/stories/workhub.stories.tsx | 15 ++-
.../target-model-selection.png | Bin 62945 -> 31561 bytes
packages/core/src/__tests__/interaction.test.ts | 22 +++-
packages/core/src/interaction.ts | 36 +++++-
.../workhub-target-execution-authority.test.ts | 125 +++++++++++++++++----
.../src/server/execution-composition.ts | 6 +-
.../server/workhub-target-execution-authority.ts | 122 +++++++++++++-------
packages/ui/src/form-interaction-prompt.tsx | 33 ++++--
9 files changed, 288 insertions(+), 78 deletions(-)
diff --git a/apps/desktop/src/renderer/styles/interaction-prompts.css b/apps/desktop/src/renderer/styles/interaction-prompts.css
index fb6a45f68c..2d329bb243 100644
--- a/apps/desktop/src/renderer/styles/interaction-prompts.css
+++ b/apps/desktop/src/renderer/styles/interaction-prompts.css
@@ -141,6 +141,13 @@
border-radius: var(--radius-control);
}
+/* The shared model wheel paints in a layer. Reserve its three-row footprint in
+ forms so the wheel does not cover the request or the form actions. */
+.maka-form-interaction-prompt .maka-model-wheel-anchor[data-open='true'] {
+ width: min(264px, 55vw);
+ height: var(--maka-model-wheel-height);
+}
+
.maka-form-interaction-field-heading {
display: flex;
align-items: baseline;
diff --git a/apps/desktop/stories/workhub.stories.tsx b/apps/desktop/stories/workhub.stories.tsx
index 2d8e4a5b9b..81778d7d19 100644
--- a/apps/desktop/stories/workhub.stories.tsx
+++ b/apps/desktop/stories/workhub.stories.tsx
@@ -136,8 +136,16 @@ function makeServices(failFirst: boolean, withHistory: boolean | 'usage', colore
? '任务“支付回调幂等性”使用的模型“qwen3.8-27b-sglang”已不可用。请选择替代模型以继续。'
: '选择要继续的工作', requester: { name: 'WorkHub' },
fields: [{ kind: 'single_select', name: repairModel ? 'targetModel' : 'target', label: repairModel ? '替代模型' : '工作 / 工作区', required: true,
+ ...(repairModel ? { presentation: 'model_picker' as const } : {}),
options: repairModel
- ? [{ value: 'qwen3.8-32b', label: 'Qwen 3.8 32B' }, { value: 'deepseek-v4-pro', label: 'DeepSeek V4 Pro' }]
+ ? [
+ { value: 'qwen3.8-32b', label: 'Qwen 3.8 32B', description: 'Coding Plan' },
+ { value: 'deepseek-v4-pro', label: 'DeepSeek V4 Pro', description: 'DeepSeek' },
+ { value: 'claude-sonnet-4.5', label: 'Claude Sonnet 4.5', description: 'Anthropic' },
+ { value: 'gpt-5.4', label: 'GPT-5.4', description: 'OpenAI' },
+ { value: 'gemini-3-pro', label: 'Gemini 3 Pro', description: 'Google' },
+ { value: 'kimi-k2.5', label: 'Kimi K2.5', description: 'Moonshot' },
+ ]
: [{ value: 'candidate-0', label: '支付回调幂等性 / maka' }, { value: 'candidate-1', label: '发布检查清单 / desktop' }] }] };
messages = [...messages, { type: 'user', id: input.turnId, turnId: input.turnId, ts: 4, text: input.text }];
interactionUpdate?.({ sessionId, interactions: [pendingForm] });
@@ -448,8 +456,9 @@ export const TargetModelRepair: Story = {
await userEvent.keyboard('{Enter}');
const prompt = await within(canvasElement).findByText(/qwen3\.8-27b-sglang.*已不可用/);
expect(prompt).toBeVisible();
- expect(within(canvasElement).getByRole('option', { name: 'Qwen 3.8 32B' })).toBeVisible();
- expect(within(canvasElement).getByRole('option', { name: 'DeepSeek V4 Pro' })).toBeVisible();
+ await userEvent.click(within(canvasElement).getByRole('button', { name: '替代模型' }));
+ expect(within(canvasElement.ownerDocument.body).getByRole('listbox', { name: '替代模型' })).toBeVisible();
+ expect(within(canvasElement.ownerDocument.body).getByRole('option', { name: /Qwen 3.8 32B/ })).toBeVisible();
},
};
diff --git a/docs/images/pr/workhub-stale-model-repair/target-model-selection.png b/docs/images/pr/workhub-stale-model-repair/target-model-selection.png
index 6729902d69..04bda72fe4 100644
Binary files a/docs/images/pr/workhub-stale-model-repair/target-model-selection.png and b/docs/images/pr/workhub-stale-model-repair/target-model-selection.png differ
diff --git a/packages/core/src/__tests__/interaction.test.ts b/packages/core/src/__tests__/interaction.test.ts
index c824cda0c8..08bcb86213 100644
--- a/packages/core/src/__tests__/interaction.test.ts
+++ b/packages/core/src/__tests__/interaction.test.ts
@@ -1107,8 +1107,9 @@ describe('Interaction decoding and validity', () => {
name: 'environment',
label: 'Environment',
required: true,
+ presentation: 'model_picker',
options: [
- { value: 'staging', label: 'Staging' },
+ { value: 'staging', label: 'Staging', description: 'Local provider' },
{ value: 'production', label: 'Production' },
],
default: 'staging',
@@ -1173,8 +1174,13 @@ describe('Interaction decoding and validity', () => {
label: '\u0007 password=field-secret',
description: '\nclient_secret=description-secret',
required: true,
+ presentation: 'model_picker',
options: [
- { value: 'production', label: '\u202e token=option-secret' },
+ {
+ value: 'production',
+ label: '\u202e token=option-secret',
+ description: '\npassword=option-description-secret',
+ },
{ value: 'staging', label: 'Staging' },
],
},
@@ -1192,12 +1198,20 @@ describe('Interaction decoding and validity', () => {
label: '\\u{7} password=[redacted]',
description: '\\u{A}client_secret=[redacted]',
required: true,
+ presentation: 'model_picker',
options: [
- { value: 'production', label: '\\u{202E} token=[redacted]' },
+ {
+ value: 'production',
+ label: '\\u{202E} token=[redacted]',
+ description: '\\u{A}password=[redacted]',
+ },
{ value: 'staging', label: 'Staging' },
],
});
- assert.doesNotMatch(JSON.stringify(projected), /message-secret|requester-secret|source-secret/);
+ assert.doesNotMatch(
+ JSON.stringify(projected),
+ /message-secret|requester-secret|source-secret|option-description-secret/,
+ );
});
test('drops string defaults that cannot be represented without changing canonical semantics', () => {
diff --git a/packages/core/src/interaction.ts b/packages/core/src/interaction.ts
index a05a4cde46..d072680320 100644
--- a/packages/core/src/interaction.ts
+++ b/packages/core/src/interaction.ts
@@ -114,6 +114,7 @@ export interface InteractionRequesterProjection {
export interface InteractionFormOption {
readonly value: string;
readonly label: string;
+ readonly description?: string;
}
interface InteractionFormFieldBase {
@@ -151,6 +152,7 @@ export type InteractionFormField =
readonly kind: 'single_select';
readonly options: readonly InteractionFormOption[];
readonly default?: string;
+ readonly presentation?: 'model_picker';
})
| (InteractionFormFieldBase & {
readonly kind: 'multi_select';
@@ -379,7 +381,10 @@ const FORM_REQUESTER_SHAPE = defineObjectShape<InteractionRequesterProjection>()
['name'],
['source'],
);
-const FORM_OPTION_SHAPE = defineObjectShape<InteractionFormOption>()(['value', 'label'], []);
+const FORM_OPTION_SHAPE = defineObjectShape<InteractionFormOption>()(
+ ['value', 'label'],
+ ['description'],
+);
const FORM_STRING_FIELD_SHAPE = defineObjectShape<
Extract<InteractionFormField, { kind: 'string' }>
>()(
@@ -394,7 +399,7 @@ const FORM_BOOLEAN_FIELD_SHAPE = defineObjectShape<
>()(['kind', 'name', 'label', 'required'], ['description', 'default']);
const FORM_SINGLE_SELECT_FIELD_SHAPE = defineObjectShape<
Extract<InteractionFormField, { kind: 'single_select' }>
->()(['kind', 'name', 'label', 'required', 'options'], ['description', 'default']);
+>()(['kind', 'name', 'label', 'required', 'options'], ['description', 'default', 'presentation']);
const FORM_MULTI_SELECT_FIELD_SHAPE = defineObjectShape<
Extract<InteractionFormField, { kind: 'multi_select' }>
>()(
@@ -754,6 +759,15 @@ function projectInteractionFormField(field: InteractionFormField): InteractionFo
const options = field.options.map((option) => ({
...option,
label: projectInteractionReviewText(option.label, INTERACTION_FORM_FIELD_LABEL_MAX_BYTES),
+ ...(option.description === undefined
+ ? {}
+ : {
+ description: projectInteractionReviewText(
+ option.description,
+ INTERACTION_FORM_FIELD_DESCRIPTION_MAX_BYTES,
+ true,
+ ),
+ }),
}));
if (new Set(options.map((option) => option.label)).size !== options.length) {
throw new Error('Form option labels collide after safe projection');
@@ -1041,6 +1055,15 @@ function decodeFormField(value: unknown): InteractionFormField {
...common,
kind: 'single_select',
options,
+ ...(record.presentation === undefined
+ ? {}
+ : {
+ presentation: oneOf(
+ record.presentation,
+ ['model_picker'] as const,
+ 'single-select presentation',
+ ),
+ }),
...(record.default === undefined
? {}
: {
@@ -1113,6 +1136,15 @@ function decodeFormOptions(value: unknown): readonly InteractionFormOption[] {
'form option label',
INTERACTION_FORM_FIELD_LABEL_MAX_BYTES,
),
+ ...(record.description === undefined
+ ? {}
+ : {
+ description: boundedText(
+ record.description,
+ 'form option description',
+ INTERACTION_FORM_FIELD_DESCRIPTION_MAX_BYTES,
+ ),
+ }),
});
},
);
diff --git a/packages/runtime-host/src/__tests__/workhub-target-execution-authority.test.ts b/packages/runtime-host/src/__tests__/workhub-target-execution-authority.test.ts
index f2ff27fd89..2935e0156f 100644
--- a/packages/runtime-host/src/__tests__/workhub-target-execution-authority.test.ts
+++ b/packages/runtime-host/src/__tests__/workhub-target-execution-authority.test.ts
@@ -52,9 +52,31 @@ describe('WorkHub target execution authority', () => {
assert.equal(fixture.model(), 'replacement-model');
assert.equal(fixture.formRequests.length, 1);
assert.equal(fixture.updates.length, 1);
+ const request = await fixture.formRequests[0]!.create();
+ assert.equal(request.fields[0]?.kind, 'single_select');
+ assert.equal(request.fields[0]?.presentation, 'model_picker');
await fixture.authority.assertReady('target');
});
+ test('offers compatible models from every configured connection', async () => {
+ const fixture = createFixture('removed-model', ['same-connection-model'], 'accept', [
+ configuredConnection('connection-2', 'second', 'Second account', ['cross-connection-model']),
+ ]);
+
+ await fixture.authority.prepare(PREPARATION, CONTEXT);
+
+ const request = await fixture.formRequests[0]!.create();
+ const field = request.fields[0];
+ assert.equal(field?.kind, 'single_select');
+ assert.deepEqual(
+ field.options.map(({ label, description }) => ({ label, description })),
+ [
+ { label: 'same-connection-model', description: 'Test' },
+ { label: 'cross-connection-model', description: 'Second account' },
+ ],
+ );
+ });
+
test('does not interrupt delegation when the saved model remains enabled', async () => {
const fixture = createFixture('current-model', ['current-model', 'other-model']);
await fixture.authority.prepare(PREPARATION, CONTEXT);
@@ -63,6 +85,22 @@ describe('WorkHub target execution authority', () => {
assert.equal(fixture.updates.length, 0);
});
+ test('reopens model selection when the accepted replacement disappears before admission', async () => {
+ const fixture = createFixture(
+ 'removed-model',
+ ['first-replacement', 'second-replacement'],
+ 'accept',
+ [],
+ true,
+ );
+
+ await fixture.authority.prepare(PREPARATION, CONTEXT);
+
+ assert.equal(fixture.model(), 'second-replacement');
+ assert.equal(fixture.formRequests.length, 2);
+ assert.equal(fixture.updates.length, 2);
+ });
+
test('cancellation leaves the target unchanged and fails before admission', async () => {
const fixture = createFixture('removed-model', ['replacement-model'], 'cancel');
@@ -90,10 +128,13 @@ function createFixture(
initialModel: string,
enabledModelIds: readonly string[],
answer: 'accept' | 'cancel' = 'accept',
+ additionalConnections: readonly ReturnType<typeof configuredConnection>[] = [],
+ invalidateFirstSelection = false,
) {
let revision = 3;
let model = initialModel;
- const formRequests: unknown[] = [];
+ const formRequests: Array<Parameters<WorkHubTargetExecutionAuthorityOptions['requestForm']>[0]> =
+ [];
const updates: unknown[] = [];
const snapshot = (): SessionHeaderSnapshot => ({
header: {
@@ -122,27 +163,37 @@ function createFixture(
revision,
committedAt: revision,
});
- const connection = {
- connectionId: 'connection-1',
- revision: 1,
- slug: 'test',
- name: 'Test',
- providerType: 'openai' as const,
- enabled: true,
- enabledModelIds,
- models: enabledModelIds.map((id) => ({ id, capabilities: { chat: true } })),
- modelSource: 'fetched' as const,
- };
+ const connection = configuredConnection('connection-1', 'test', 'Test', enabledModelIds);
+ const connections = [connection, ...additionalConnections];
const options: WorkHubTargetExecutionAuthorityOptions = {
readSession: async () => snapshot(),
runtimePolicy: {
- resolveExecutionConnection: async () => ({
- kind: 'ready',
- connection,
- secretMaterial: {},
- networkProxy: { enabled: false },
- }),
- } as unknown as Pick<RuntimePolicyStoresWriter['operations'], 'resolveExecutionConnection'>,
+ resolveExecutionConnection: async (
+ locator: Parameters<
+ RuntimePolicyStoresWriter['operations']['resolveExecutionConnection']
+ >[0],
+ ) => {
+ const selected =
+ locator.kind === 'bound'
+ ? connections.find((candidate) => candidate.connectionId === locator.connectionId)
+ : connections.find((candidate) => candidate.slug === locator.connectionSlug);
+ return selected
+ ? {
+ kind: 'ready',
+ connection: selected,
+ secretMaterial: {},
+ networkProxy: { enabled: false },
+ }
+ : { kind: 'not_found' };
+ },
+ connectionCatalog: {
+ getSnapshot: async () => ({
+ revision: 1,
+ defaultTarget: null,
+ connections,
+ }),
+ },
+ } as unknown as WorkHubTargetExecutionAuthorityOptions['runtimePolicy'],
requestForm: async (input) => {
formRequests.push(input);
const request = await input.create();
@@ -161,6 +212,15 @@ function createFixture(
if (input.expectedRevision !== revision) return 'revision_conflict';
model = input.modelTarget.model;
revision += 1;
+ if (invalidateFirstSelection && updates.length === 1) {
+ connection.enabledModelIds = connection.enabledModelIds.filter(
+ (candidate) => candidate !== model,
+ );
+ connection.models = connection.models.filter((candidate) => candidate.id !== model);
+ connection.catalogEntries = connection.catalogEntries.filter(
+ (candidate) => candidate.id !== model,
+ );
+ }
return 'committed';
},
};
@@ -171,3 +231,30 @@ function createFixture(
model: () => model,
};
}
+
+function configuredConnection(
+ connectionId: string,
+ slug: string,
+ name: string,
+ enabledModelIds: readonly string[],
+) {
+ return {
+ connectionId,
+ revision: 1,
+ slug,
+ name,
+ providerType: 'openai' as const,
+ enabled: true,
+ enabledModelIds,
+ models: enabledModelIds.map((id) => ({ id, capabilities: { chat: true } })),
+ modelSource: 'fetched' as const,
+ catalogEntries: enabledModelIds.map((id) => ({
+ id,
+ displayName: id,
+ canUseAsChatDefault: true,
+ isDefault: false,
+ thinkingLevels: [],
+ supportsVision: false,
+ })),
+ };
+}
diff --git a/packages/runtime-host/src/server/execution-composition.ts b/packages/runtime-host/src/server/execution-composition.ts
index 26cb34270b..99764d13ef 100644
--- a/packages/runtime-host/src/server/execution-composition.ts
+++ b/packages/runtime-host/src/server/execution-composition.ts
@@ -2050,7 +2050,11 @@ export async function createExecutionRuntimeHostComposition(
});
const workHubTargetExecution = new HostWorkHubTargetExecutionAuthority({
readSession: (sessionId) => stores.sessionStore.readHeaderRecordSnapshot(sessionId),
- runtimePolicy: runtimePolicyStores.operations,
+ runtimePolicy: {
+ resolveExecutionConnection: (locator) =>
+ runtimePolicyStores.operations.resolveExecutionConnection(locator),
+ connectionCatalog: runtimePolicyStores.connectionCatalog,
+ },
requestForm: (input) => interactions.requestForm(input),
updateModel: async (input, operationContext) => {
const outcome = await sessionCatalog.handlers['session.configuration.update'](
diff --git a/packages/runtime-host/src/server/workhub-target-execution-authority.ts b/packages/runtime-host/src/server/workhub-target-execution-authority.ts
index 3bbfa9fa97..8d468a5236 100644
--- a/packages/runtime-host/src/server/workhub-target-execution-authority.ts
+++ b/packages/runtime-host/src/server/workhub-target-execution-authority.ts
@@ -22,7 +22,11 @@ import {
INTERACTION_FORM_FIELD_LABEL_MAX_BYTES,
INTERACTION_FORM_MAX_OPTIONS,
} from '@maka/core/interaction';
-import { authorizeConnectionModel, connectionEnabledModelIds } from '@maka/core/llm-connections';
+import {
+ authorizeConnectionModel,
+ connectionEnabledModelIds,
+ providerMenuLabel,
+} from '@maka/core/llm-connections';
import { isModelExplicitlyUnsupportedForChat } from '@maka/core/model-catalog';
import { thinkingVariantsForConnection } from '@maka/core/model-thinking';
import { WORKHUB_COORDINATION_SESSION_ID } from '@maka/core/session';
@@ -51,7 +55,9 @@ export interface WorkHubTargetExecutionAuthorityOptions {
readonly runtimePolicy: Pick<
RuntimePolicyStoresWriter['operations'],
'resolveExecutionConnection'
- >;
+ > & {
+ readonly connectionCatalog: Pick<RuntimePolicyStoresWriter['connectionCatalog'], 'getSnapshot'>;
+ };
readonly requestForm: HostInteractionCoordinator['requestForm'];
readonly updateModel: (
input: {
@@ -66,6 +72,7 @@ export interface WorkHubTargetExecutionAuthorityOptions {
interface ReplacementModel {
readonly value: string;
readonly label: string;
+ readonly description: string;
readonly target: Extract<SessionModelTarget, { readonly kind: 'explicit' }>;
}
@@ -89,7 +96,7 @@ export class HostWorkHubTargetExecutionAuthority implements WorkHubTargetExecuti
input: WorkHubTargetExecutionPreparationInput,
context: ConnectionContext,
): Promise<void> {
- for (let attempt = 0; attempt < 2; attempt++) {
+ for (let attempt = 0; attempt < 3; attempt++) {
const readiness = await this.#readiness(input.targetSessionId);
if (readiness.kind === 'ready') return;
if (readiness.replacements.length === 0) {
@@ -125,7 +132,12 @@ export class HostWorkHubTargetExecutionAuthority implements WorkHubTargetExecuti
name: 'targetModel',
label: `Model for ${input.targetSessionName}`,
required: true,
- options: readiness.replacements.map(({ value, label }) => ({ value, label })),
+ presentation: 'model_picker',
+ options: readiness.replacements.map(({ value, label, description }) => ({
+ value,
+ label,
+ description,
+ })),
},
],
}),
@@ -153,8 +165,7 @@ export class HostWorkHubTargetExecutionAuthority implements WorkHubTargetExecuti
context,
);
if (updated === 'committed') {
- await this.assertReady(input.targetSessionId);
- return;
+ if ((await this.#readiness(input.targetSessionId)).kind === 'ready') return;
}
}
throw new WorkHubActionEffectFailure(
@@ -186,45 +197,72 @@ export class HostWorkHubTargetExecutionAuthority implements WorkHubTargetExecuti
connectionSlug: header.llmConnectionSlug,
},
);
- if (resolved.kind !== 'ready') {
- throw new WorkHubActionEffectFailure(
- 'operation_unavailable',
- `Target task model connection is not ready: ${resolved.kind}`,
- );
+ if (resolved.kind === 'ready') {
+ const current = authorizeConnectionModel(resolved.connection, header.model);
+ if (current && !isModelExplicitlyUnsupportedForChat(current)) return { kind: 'ready' };
}
- const current = authorizeConnectionModel(resolved.connection, header.model);
- if (current && !isModelExplicitlyUnsupportedForChat(current)) return { kind: 'ready' };
- const replacements = connectionEnabledModelIds(resolved.connection)
- .flatMap((modelId): ReplacementModel[] => {
- const model = authorizeConnectionModel(resolved.connection, modelId);
- if (!model || isModelExplicitlyUnsupportedForChat(model)) return [];
- if (
- header.thinkingLevel !== undefined &&
- !thinkingVariantsForConnection(
+ const catalog = await this.#options.runtimePolicy.connectionCatalog.getSnapshot();
+ const resolvedConnections = await Promise.all(
+ catalog.connections.map((connection) =>
+ this.#options.runtimePolicy.resolveExecutionConnection({
+ kind: 'bound',
+ connectionId: connection.connectionId,
+ connectionSlug: connection.slug,
+ }),
+ ),
+ );
+ const replacements = resolvedConnections
+ .flatMap((candidate): ReplacementModel[] => {
+ if (candidate.kind !== 'ready') return [];
+ const connection = candidate.connection;
+ return connectionEnabledModelIds(connection).flatMap((modelId): ReplacementModel[] => {
+ const model = authorizeConnectionModel(connection, modelId);
+ if (!model || isModelExplicitlyUnsupportedForChat(model)) return [];
+ if (
+ header.thinkingLevel !== undefined &&
+ !thinkingVariantsForConnection(
+ {
+ providerType: connection.providerType,
+ modelOverrides: connection.modelOverrides,
+ },
+ modelId,
+ ).includes(header.thinkingLevel)
+ ) {
+ return [];
+ }
+ const target = {
+ kind: 'explicit' as const,
+ connectionId: connection.connectionId,
+ connectionSlug: connection.slug,
+ model: modelId,
+ };
+ const connectionLabel =
+ connection.name.trim() || providerMenuLabel(connection.providerType) || connection.slug;
+ const modelLabel = model.displayName?.trim() || modelId;
+ return [
{
- providerType: resolved.connection.providerType,
- modelOverrides: resolved.connection.modelOverrides,
+ value: createHash('sha256')
+ .update(`${target.connectionId}\0${target.connectionSlug}\0${target.model}`, 'utf8')
+ .digest('hex'),
+ label: boundedUtf8(modelLabel, INTERACTION_FORM_FIELD_LABEL_MAX_BYTES),
+ description: boundedUtf8(connectionLabel, INTERACTION_FORM_FIELD_LABEL_MAX_BYTES),
+ target,
},
- modelId,
- ).includes(header.thinkingLevel)
- ) {
- return [];
- }
- const target = {
- kind: 'explicit' as const,
- connectionId: resolved.connection.connectionId,
- connectionSlug: resolved.connection.slug,
- model: modelId,
- };
- return [
- {
- value: createHash('sha256')
- .update(`${target.connectionId}\0${target.connectionSlug}\0${target.model}`, 'utf8')
- .digest('hex'),
- label: boundedUtf8(modelId, INTERACTION_FORM_FIELD_LABEL_MAX_BYTES),
- target,
- },
- ];
+ ];
+ });
+ })
+ .sort((left, right) => {
+ const isSameConnection = (candidate: ReplacementModel) =>
+ header.llmConnectionId
+ ? candidate.target.connectionId === header.llmConnectionId
+ : candidate.target.connectionSlug === header.llmConnectionSlug;
+ const leftSameConnection = isSameConnection(left) ? 0 : 1;
+ const rightSameConnection = isSameConnection(right) ? 0 : 1;
+ return (
+ leftSameConnection - rightSameConnection ||
+ left.description.localeCompare(right.description) ||
+ left.label.localeCompare(right.label)
+ );
})
.slice(0, INTERACTION_FORM_MAX_OPTIONS);
return { kind: 'model_repair_required', record, replacements };
diff --git a/packages/ui/src/form-interaction-prompt.tsx b/packages/ui/src/form-interaction-prompt.tsx
index 0d405c70e1..f4b1a86646 100644
--- a/packages/ui/src/form-interaction-prompt.tsx
+++ b/packages/ui/src/form-interaction-prompt.tsx
@@ -30,6 +30,7 @@ import {
} from './form-interaction-prompt-state.js';
import { useUiLocale } from './locale-context.js';
import { ChoicePanel } from './choice-panel.js';
+import { ModelWheelPicker } from './model-wheel-picker.js';
import { useMountedRef } from './use-mounted-ref.js';
export function FormInteractionPrompt(props: {
@@ -140,13 +141,31 @@ function ActiveFormInteractionPrompt(props: {
) : null}
{props.request.fields.length === 1 && field.kind === 'single_select' && field.required ? (
<>
- <ChoicePanel label={field.label} options={field.options}
- value={typeof draft.value === 'string' ? draft.value : ''}
- disabled={responsePending || props.stopPending}
- onChange={(value) => updateDraft(index, { ...draft, value })}
- onConfirm={accept}
- onEscape={() => void respond({ requestId: props.request.requestId, action: 'cancel' })} />
- <Text as="p" type="supporting" color="secondary" className="maka-choice-hint">{copy.keyboardHint}</Text>
+ {field.presentation === 'model_picker' ? (
+ <ModelWheelPicker
+ options={field.options.map((option) => ({
+ value: option.value,
+ label: option.label,
+ ...(option.description ? { heading: option.description } : {}),
+ }))}
+ value={typeof draft.value === 'string' ? draft.value : undefined}
+ label={field.options.find((option) => option.value === draft.value)?.label ?? field.label}
+ ariaLabel={field.label}
+ size="md"
+ disabled={responsePending || props.stopPending}
+ onValueChange={(value) => updateDraft(index, { ...draft, value })}
+ />
+ ) : (
+ <>
+ <ChoicePanel label={field.label} options={field.options}
+ value={typeof draft.value === 'string' ? draft.value : ''}
+ disabled={responsePending || props.stopPending}
+ onChange={(value) => updateDraft(index, { ...draft, value })}
+ onConfirm={accept}
+ onEscape={() => void respond({ requestId: props.request.requestId, action: 'cancel' })} />
+ <Text as="p" type="supporting" color="secondary" className="maka-choice-hint">{copy.keyboardHint}</Text>
+ </>
+ )}
</>
) : draft.included ? renderFormControl({
field,
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 04/04: test(workhub): await searchable model menu
Posted by zh...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
me2seeks pushed a commit to branch fix/5452-workhub-stale-model-repair
in repository https://gitbox.apache.org/repos/asf/maka.git
commit 8b4e31d6073a2bf4cb107b28712cc7bbcc52e5e2
Author: me2seeks <me...@gmail.com>
AuthorDate: Thu Sep 17 23:38:55 2026 +0800
test(workhub): await searchable model menu
---
apps/desktop/stories/workhub.stories.tsx | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/apps/desktop/stories/workhub.stories.tsx b/apps/desktop/stories/workhub.stories.tsx
index 4a742f86e6..c815d4360b 100644
--- a/apps/desktop/stories/workhub.stories.tsx
+++ b/apps/desktop/stories/workhub.stories.tsx
@@ -423,8 +423,9 @@ export const TargetModelRepair: Story = {
const prompt = await within(canvasElement).findByText(/qwen3\.8-27b-sglang.*已不可用/);
expect(prompt).toBeVisible();
await userEvent.click(within(canvasElement).getByRole('button', { name: '替代模型' }));
- expect(within(canvasElement.ownerDocument.body).getByRole('listbox', { name: '替代模型' })).toBeVisible();
- expect(within(canvasElement.ownerDocument.body).getByRole('option', { name: /Qwen 3.8 32B/ })).toBeVisible();
+ const page = within(canvasElement.ownerDocument.body);
+ await waitFor(() => expect(page.getByRole('listbox', { name: '替代模型' })).toBeVisible());
+ await waitFor(() => expect(page.getByRole('option', { name: /Qwen 3.8 32B/ })).toBeVisible());
},
};
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org