feat: add reasoning effort picker to session screen (#47) (#51)

* feat(#49): improve project picker with recents + server projects

- New Session modal now shows:
  - Current project as tappable row (tap to create session immediately)
  - Recent Projects section: list of previously used dirs as tappable rows
  - Server Projects section: projects known to opencode server (from /project API)
  - Manual path input as fallback (unchanged behavior)
- Modal body is now scrollable to handle long lists
- All selection paths call addRecentDirectory to keep recents up to date
- TypeScript clean (pre-existing VariantPicker.tsx error unrelated)

* feat: add reasoning effort (variant) picker to session screen (#47)

- Add VariantPicker bottom sheet component (low/medium/high/auto)
- Add variant state to catalog store, reset on model change
- Pass variant through sendMessage -> sdk.session.prompt()
- Add reasoning chip to toolbar, shown only for models with variants
- Parse model.variants from provider API response in catalog and sdk types

API field: variant in POST /session/:id/prompt_async
Server maps variant -> reasoningEffort via model variant config

* fix(models): preserve reasoning effort across messages

Reset the selected variant only when the provider/model pair actually changes, including catalog reloads and agent-driven model switches.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Den
2026-07-16 17:32:56 -07:00
committed by GitHub
parent 36421b4d30
commit c1736bd426
6 changed files with 192 additions and 6 deletions

View File

@@ -26,6 +26,7 @@ import {
StatusIndicator,
SlashPopover,
ModelPicker,
VariantPicker,
ImageAttachments,
SessionInfo,
type SlashCommand,
@@ -78,6 +79,7 @@ export default function SessionScreen() {
const flatListRef = useRef<FlatList>(null)
const modelSheetRef = useRef<BottomSheet>(null)
const variantSheetRef = useRef<BottomSheet>(null)
const [input, setInput] = useState("")
const [attachments, setAttachments] = useState<Attachment[]>([])
const [showInfo, setShowInfo] = useState(false)
@@ -115,6 +117,8 @@ export default function SessionScreen() {
const agent = catalog.agent || ""
const model = catalog.model
const setModel = catalog.setModel
const variant = catalog.variant
const setVariant = catalog.setVariant
const cycleAgent = catalog.cycleAgent
// Permission & question state
@@ -338,7 +342,7 @@ export default function SessionScreen() {
// Messages are queued server-side when the session is busy.
// No need to abort - just send and it will be processed after current response.
try {
await sendMessage(text, model || undefined, agent || undefined, files)
await sendMessage(text, model || undefined, agent || undefined, files, variant || undefined)
} catch (err) {
console.error("Send failed:", err)
// Restore the user's text and attachments so their input isn't lost.
@@ -456,6 +460,14 @@ export default function SessionScreen() {
const agentColor = currentAgent?.color || "#8b5cf6"
const modelLabel = model?.modelID ? model.modelID.split("/").pop() || model.modelID : "default"
// Variants for current model (for reasoning effort picker)
const currentModelVariants = useMemo(() => {
if (!model) return undefined
const provider = providers.find((p) => p.id === model.providerID)
const found = provider?.models.find((m) => m.id === model.modelID)
return found?.variants
}, [model, providers])
return (
<>
<Stack.Screen
@@ -611,6 +623,18 @@ export default function SessionScreen() {
{modelLabel}
</Text>
</TouchableOpacity>
{currentModelVariants && Object.keys(currentModelVariants).length > 0 && (
<TouchableOpacity
style={[s.variantChip, isDark && s.variantChipDark, variant && s.variantChipActive]}
onPress={() => variantSheetRef.current?.expand()}
>
<Ionicons name="flash-outline" size={14} color={variant ? "#8b5cf6" : isDark ? "#888888" : "#666666"} />
<Text style={[s.variantLabel, isDark && s.metaDark, variant && s.variantLabelActive]} numberOfLines={1}>
{variant ? variant.charAt(0).toUpperCase() + variant.slice(1) : "Auto"}
</Text>
</TouchableOpacity>
)}
</View>
{/* Attachment preview */}
@@ -678,6 +702,15 @@ export default function SessionScreen() {
isDark={isDark}
onSelect={handleModelSelect}
/>
{/* Reasoning effort (variant) picker bottom sheet */}
<VariantPicker
sheetRef={variantSheetRef}
variants={currentModelVariants}
selected={variant}
isDark={isDark}
onSelect={setVariant}
/>
</>
)
}
@@ -771,6 +804,21 @@ const s = StyleSheet.create({
modelChipDark: { backgroundColor: "#1a1a1a" },
modelLabel: { fontSize: 12, color: "#666666", maxWidth: 160 },
// Variant (reasoning effort) chip
variantChip: {
flexDirection: "row",
alignItems: "center",
gap: 4,
backgroundColor: "#f5f5f5",
borderRadius: 12,
paddingHorizontal: 10,
paddingVertical: 4,
},
variantChipDark: { backgroundColor: "#1a1a1a" },
variantChipActive: { backgroundColor: "#f5f3ff" },
variantLabel: { fontSize: 12, color: "#666666" },
variantLabelActive: { color: "#8b5cf6" },
// Input
inputContainer: {
padding: 12,