blob: a586ed71c910557f6c34f2c2ec463b034370710d [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
window.__MakaModuleLoader__.load({
id: 'codex-app-server-executor',
factory(require) {
const React = require('react');
const { ModelWheelPicker, Selector, SelectorOption } = require('@maka/ui/client-plugin');
const EXECUTOR_ID = 'codex.app-server';
const NATIVE_PREFIX = 'native:';
const CODEX_PREFIX = 'codex:';
const nativeValue = (choice) =>
`${NATIVE_PREFIX}${encodeURIComponent(choice.connectionId)}:${encodeURIComponent(choice.connectionSlug)}:${encodeURIComponent(choice.model)}`;
const providerMark = (owner, providerType) => {
if (!providerType || !owner.renderProviderMark) return undefined;
return React.createElement(
'span',
{
className: 'modelPickerProviderMark',
'data-provider': providerType,
'aria-hidden': true,
},
owner.renderProviderMark(providerType),
);
};
const codexMark = () =>
React.createElement(
'span',
{ className: 'modelPickerProviderMark codexModelMark', 'aria-hidden': true },
'>_',
);
const optionLabel = (text) =>
React.createElement(
'span',
{ className: 'modelPickerOptionLabel' },
React.createElement('bdi', null, text),
);
const renderOption = (option) =>
React.createElement(SelectorOption, {
className: 'modelPickerOption',
icon: option.icon,
label: optionLabel(option.label || option.value),
description: option.description,
});
const renderValue = (option) =>
React.createElement(SelectorOption, {
icon: option.icon,
label: optionLabel(option.label || option.value),
});
const nativeSections = (owner, nativeChoices) => {
const groups = new Map();
for (const choice of nativeChoices) {
const group = groups.get(choice.connectionSlug);
if (group) group.push(choice);
else groups.set(choice.connectionSlug, [choice]);
}
return [...groups.values()].map((group) => ({
type: 'section',
title: group[0].connectionName || group[0].providerLabel,
options: group.map((choice) => ({
value: nativeValue(choice),
label: choice.label,
icon: providerMark(owner, choice.providerType),
description:
[choice.description, choice.knowledgeCutoff].filter(Boolean).join(' · ') || undefined,
})),
}));
};
function CodexControls(owner) {
const [models, setModels] = React.useState([]);
const [error, setError] = React.useState('');
const [loading, setLoading] = React.useState(false);
const modelLoad = React.useRef({ started: false, controller: null });
const selectedModel =
owner.executorTarget?.executorId === EXECUTOR_ID ? (owner.executorTarget.model ?? '') : '';
const selected = models.find((model) => model.model === selectedModel);
const efforts = selected?.supportedReasoningEfforts ?? [];
const selectedEffort = owner.executorTarget?.thinkingLevel ?? '';
const nativeChoices = owner.modelChoices ?? [];
const currentNative = owner.hasSession
? nativeChoices.find(
(choice) =>
choice.connectionId === owner.activeModelConnectionId &&
choice.connectionSlug === owner.activeModelConnectionSlug &&
choice.model === owner.activeModel,
)
: nativeChoices.find(
(choice) =>
choice.connectionId === owner.newChatModel?.llmConnectionId &&
choice.connectionSlug === owner.newChatModel?.llmConnectionSlug &&
choice.model === owner.newChatModel?.model,
);
const loadModels = React.useCallback(() => {
if (modelLoad.current.started) return;
const controller = new AbortController();
modelLoad.current = { started: true, controller };
setLoading(true);
ctx.remote
.call('codex.app-server.models', {}, { signal: controller.signal })
.then((value) => {
if (!Array.isArray(value)) throw new Error('Codex returned an invalid model list');
setModels(value);
setError('');
})
.catch((reason) => {
if (!controller.signal.aborted)
setError(reason instanceof Error ? reason.message : String(reason));
})
.finally(() => {
if (!controller.signal.aborted) setLoading(false);
});
}, []);
React.useEffect(() => {
// A selected Codex model needs its display metadata immediately. When
// Maka has no native choices, loading is also required to avoid a
// disabled empty picker that the user cannot open to trigger loading.
if (selectedModel || nativeChoices.length === 0) loadModels();
}, [selectedModel, nativeChoices.length, loadModels]);
React.useEffect(() => () => modelLoad.current.controller?.abort(), []);
const choose = (model, thinkingLevel) => {
if (!owner.onExecutorTargetChange || !model) return;
void owner.onExecutorTargetChange({
executorId: EXECUTOR_ID,
model,
...(thinkingLevel ? { thinkingLevel } : {}),
});
};
const codexOptions = models.map((model) => ({
value: `${CODEX_PREFIX}${model.model}`,
label: model.displayName || model.model,
icon: codexMark(),
description: model.description,
}));
const choices = [
...nativeSections(owner, nativeChoices),
...(codexOptions.length > 0
? [{ type: 'section', title: 'Codex', options: codexOptions }]
: []),
];
const currentValue = selectedModel
? `${CODEX_PREFIX}${selectedModel}`
: currentNative
? nativeValue(currentNative)
: '';
const currentLabel = selectedModel
? selected?.displayName || selectedModel
: currentNative?.label || owner.activeModelLabel || owner.activeModel || 'Choose model';
const controlLabel = owner.purpose === 'new-work-default' ? 'New work model' : 'Model';
const onModelChange = async (value) => {
if (value.startsWith(CODEX_PREFIX)) {
const modelId = value.slice(CODEX_PREFIX.length);
const model = models.find((candidate) => candidate.model === modelId);
choose(modelId, model?.defaultReasoningEffort);
return;
}
const native = nativeChoices.find((choice) => nativeValue(choice) === value);
if (!native || !owner.onNativeModelChange) return;
await owner.onNativeModelChange({
llmConnectionId: native.connectionId,
llmConnectionSlug: native.connectionSlug,
model: native.model,
});
};
const wheelOptions = [
...nativeChoices.map((choice) => ({
value: nativeValue(choice),
label: choice.label,
heading: choice.connectionName || choice.providerLabel,
description: choice.description,
})),
...codexOptions.map((option) => ({
value: option.value,
label: option.label,
heading: 'Codex',
description: option.description,
})),
];
const [wheelOpen, setWheelOpen] = React.useState(false);
React.useEffect(() => setWheelOpen(false), [owner.sessionId]);
const modelPickerControl =
owner.presentation === 'wheel'
? React.createElement(ModelWheelPicker, {
options: wheelOptions,
value: currentValue,
label: currentLabel,
ariaLabel: `${controlLabel}: ${currentLabel}`,
icon: selectedModel ? codexMark() : providerMark(owner, currentNative?.providerType),
tooltip: controlLabel,
triggerClassName: 'maka-model-switcher-trigger',
disabled: owner.disabled || owner.streaming || wheelOptions.length === 0,
open: wheelOpen,
onOpenChange: (open) => {
if (open) loadModels();
setWheelOpen(open);
},
onValueChange: onModelChange,
})
: React.createElement(Selector, {
label: `${controlLabel}: ${currentLabel}`,
isLabelHidden: true,
options: choices,
value: currentValue,
hasSearch: true,
variant: 'ghost',
size: 'sm',
placement: 'above',
presentation: owner.presentation,
isReadOnly: owner.isReadOnly,
isDisabled: owner.disabled || owner.streaming || choices.length === 0,
disabledMessage: error || undefined,
placeholder: loading ? 'Loading models…' : error ? 'Codex unavailable' : currentLabel,
className: owner.hasSession
? 'maka-model-switcher-trigger'
: 'maka-new-chat-model-selector',
onChange: onModelChange,
renderOption,
renderValue,
});
const modelPicker =
owner.presentation === 'wheel'
? modelPickerControl
: React.createElement(
'span',
{
style: { display: 'contents' },
onPointerDownCapture: loadModels,
onKeyDownCapture: (event) => {
if (
event.key === 'Enter' ||
event.key === ' ' ||
event.key === 'ArrowDown' ||
event.key === 'ArrowUp'
)
loadModels();
},
},
modelPickerControl,
);
return React.createElement(
React.Fragment,
null,
modelPicker,
selectedModel && efforts.length > 0
? React.createElement(Selector, {
label: `Reasoning: ${selectedEffort || 'Default'}`,
isLabelHidden: true,
options: [
{ value: '__default__', label: 'Default' },
...efforts.map((effort) => ({
value: effort.reasoningEffort,
label: effort.reasoningEffort,
})),
],
value: selectedEffort || '__default__',
variant: 'ghost',
size: 'sm',
placement: 'above',
presentation: owner.presentation === 'wheel' ? 'bottom-sheet' : owner.presentation,
isReadOnly: owner.isReadOnly,
isDisabled: owner.disabled || owner.streaming,
className: 'maka-thinking-level-selector',
onChange: (value) =>
choose(selectedModel, value === '__default__' ? undefined : value),
})
: selectedModel
? null
: (owner.renderNativeThinkingControl?.() ?? null),
);
}
let ctx;
return {
apply(context) {
ctx = context;
ctx.style(
`.codexModelMark { display: inline-grid; place-items: center; font: 600 9px/1 ui-monospace, monospace; letter-spacing: -1px; }`,
'Codex model mark',
);
return ctx.slots.register(
{
name: 'conversation.composer.model-selection',
select: (owner) => (owner.onExecutorTargetChange ? true : null),
priority: 20,
},
(owner) => React.createElement(CodexControls, owner),
);
},
};
},
});