202 lines · 7.2 KB · TypeScript
RawDownload
import type { MenuAction } from "@react-native-menu/menu";import type { ModelSelection, ProviderOptionDescriptor, RuntimeMode } from "@t3tools/contracts";import {  getProviderOptionCurrentLabel,  getProviderOptionCurrentValue,} from "@t3tools/shared/model"; import type { ModelOption, ProviderGroup } from "../../lib/modelOptions"; /** * Desktop-oriented effort keywords that don't belong in the phone picker. * Prompt-injected values (ultrathink and friends) are filtered from the * descriptor metadata; ultracode is a real option but a workflow trigger, not * a reasoning level. A value set elsewhere still displays, it just isn't * offered. */export const HIDDEN_EFFORT_OPTION_IDS: ReadonlySet<string> = new Set(["ultracode"]); export const RUNTIME_MODE_CHOICES: ReadonlyArray<{  readonly mode: RuntimeMode;  readonly label: string;  readonly shortLabel: string;}> = [  { mode: "approval-required", label: "Approve actions", shortLabel: "Approve" },  { mode: "auto-accept-edits", label: "Auto-accept edits", shortLabel: "Edits" },  { mode: "auto", label: "Auto", shortLabel: "Auto" },  { mode: "full-access", label: "Full access", shortLabel: "Full" },]; export function selectableChoices(  descriptor: Extract<ProviderOptionDescriptor, { type: "select" }>,) {  const injected = new Set(descriptor.promptInjectedValues ?? []);  return descriptor.options.filter(    (option) => !injected.has(option.id) && !HIDDEN_EFFORT_OPTION_IDS.has(option.id),  );} export type ThreadSettingsMenuEvent =  | { readonly type: "select-model"; readonly option: ModelOption }  | { readonly type: "set-option"; readonly optionId: string; readonly value: string | boolean }  | { readonly type: "set-runtime"; readonly mode: RuntimeMode }; export type ThreadSettingsMenu = {  readonly actions: MenuAction[];  /** Menu action id → the change it applies, for the onPressAction dispatch. */  readonly events: ReadonlyMap<string, ThreadSettingsMenuEvent>;}; /** * Native menu replacement for the thread settings sheet (model, select and * boolean provider options, runtime mode). The menu presents from the * composer pill without resigning the keyboard, so adjusting settings never * bounces focus. A thread is bound to one harness, so the menu covers the * sheet's full surface for existing threads; the sheet remains the Android * and new-task-draft surface. * * Selections apply immediately — the sheet's stage-then-Save flow only exists * because the sheet batches a model change with its option edits. */export function buildThreadSettingsMenu(input: {  readonly providerGroups: ReadonlyArray<ProviderGroup>;  readonly selectedModel: ModelSelection | null;  readonly optionDescriptors: ReadonlyArray<ProviderOptionDescriptor>;  readonly runtimeMode: RuntimeMode;}): ThreadSettingsMenu {  const events = new Map<string, ThreadSettingsMenuEvent>();  const actions: MenuAction[] = [];   const isSelected = (option: ModelOption) =>    option.selection.instanceId === input.selectedModel?.instanceId &&    option.selection.model === input.selectedModel.model;   // Only top-level leaves (boolean toggles) keep the menu presented (iOS  // 16+): the root refreshes in place with clean chrome. Picks inside nested  // submenus close the menu — keeping the submenu presented renders an  // expanded-submenu header with no way to pop back to the root, and the  // bottom-anchored collapse back out travels the levels' height difference.  const keepPresented = { keepsMenuPresented: true } as const;   const modelAction = (option: ModelOption, id: string): MenuAction => {    events.set(id, { type: "select-model", option });    return {      id,      title: option.label,      ...(option.isDefault ? { subtitle: "Default" } : {}),      state: isSelected(option) ? "on" : "off",    };  };   const modelItems: MenuAction[] = [];  const legacyItems: MenuAction[] = [];  let selectedModelLabel: string | undefined;  input.providerGroups.forEach((group, groupIndex) => {    const groupItems: MenuAction[] = [];    group.models.forEach((option, modelIndex) => {      if (isSelected(option)) {        selectedModelLabel = option.label;      }      const id = `model:${groupIndex}:${modelIndex}`;      // A highlighted legacy model stays in the main list (mirroring the      // sheet) so the checkmark isn't hidden behind the Legacy fold.      if (option.isLegacy && !isSelected(option)) {        legacyItems.push(modelAction(option, id));      } else {        groupItems.push(modelAction(option, id));      }    });    if (groupItems.length === 0) {      return;    }    // A thread is bound to one harness, so provider sections only appear for    // multi-group callers (the new-task draft, if it ever adopts the menu).    if (input.providerGroups.length > 1) {      modelItems.push({        id: `model-group:${groupIndex}`,        title: group.providerLabel,        displayInline: true,        subactions: groupItems,      });    } else {      modelItems.push(...groupItems);    }  });  if (legacyItems.length > 0) {    modelItems.push({      id: "legacy-models",      title: "Legacy Models",      subactions: legacyItems,    });  }  if (modelItems.length > 0) {    actions.push({      id: "model",      title: "Model",      ...(selectedModelLabel === undefined        ? input.selectedModel          ? { subtitle: input.selectedModel.model }          : {}        : { subtitle: selectedModelLabel }),      subactions: modelItems,    });  }   for (const descriptor of input.optionDescriptors) {    if (descriptor.type === "boolean") {      const id = `option:${descriptor.id}`;      events.set(id, {        type: "set-option",        optionId: descriptor.id,        value: !(descriptor.currentValue ?? false),      });      actions.push({        id,        title: descriptor.label,        state: descriptor.currentValue ? "on" : "off",        attributes: keepPresented,      });      continue;    }    const currentValue = getProviderOptionCurrentValue(descriptor);    const choices = selectableChoices(descriptor).map((choice): MenuAction => {      const id = `option:${descriptor.id}:${choice.id}`;      events.set(id, { type: "set-option", optionId: descriptor.id, value: choice.id });      return {        id,        title: choice.label,        state: choice.id === currentValue ? "on" : "off",      };    });    if (choices.length === 0) {      continue;    }    const currentLabel = getProviderOptionCurrentLabel(descriptor);    actions.push({      id: `option:${descriptor.id}`,      title: descriptor.label,      ...(currentLabel === undefined ? {} : { subtitle: currentLabel }),      subactions: choices,    });  }   const runtimeLabel = RUNTIME_MODE_CHOICES.find(    (choice) => choice.mode === input.runtimeMode,  )?.label;  actions.push({    id: "runtime",    title: "Runtime",    ...(runtimeLabel === undefined ? {} : { subtitle: runtimeLabel }),    subactions: RUNTIME_MODE_CHOICES.map((choice): MenuAction => {      const id = `runtime:${choice.mode}`;      events.set(id, { type: "set-runtime", mode: choice.mode });      return {        id,        title: choice.label,        state: choice.mode === input.runtimeMode ? "on" : "off",      };    }),  });   return { actions, events };}