refactor: extract permission mode utilities and consolidate styling logic

Extract permission mode display logic into shared utilities for better reusability and maintainability. Add PermissionModeTone type and related helpers to centralize mode-based styling rules across components. Update components to use new PermissionModeOption type for consistent permission mode presentation.
This commit is contained in:
weishu
2026-01-03 22:49:40 +08:00
parent f2f46e9383
commit 4d18789d50
9 changed files with 102 additions and 35 deletions
@@ -1,4 +1,4 @@
import { getPermissionModesForFlavor, MODEL_MODE_LABELS, MODEL_MODES, PERMISSION_MODE_LABELS } from '@hapi/protocol'
import { getPermissionModeOptionsForFlavor, MODEL_MODE_LABELS, MODEL_MODES } from '@hapi/protocol'
import { ComposerPrimitive, useAssistantApi, useAssistantState } from '@assistant-ui/react'
import {
type ChangeEvent as ReactChangeEvent,
@@ -207,10 +207,14 @@ export function HappyComposer(props: {
}
}, [switchDisabled, onSwitchToRemote, haptic])
const permissionModes = useMemo(
() => getPermissionModesForFlavor(agentFlavor),
const permissionModeOptions = useMemo(
() => getPermissionModeOptionsForFlavor(agentFlavor),
[agentFlavor]
)
const permissionModes = useMemo(
() => permissionModeOptions.map((option) => option.mode),
[permissionModeOptions]
)
const handleKeyDown = useCallback((e: ReactKeyboardEvent<HTMLTextAreaElement>) => {
const key = e.key
@@ -327,7 +331,7 @@ export function HappyComposer(props: {
haptic('light')
}, [onModelModeChange, controlsDisabled, haptic])
const showPermissionSettings = Boolean(onPermissionModeChange && permissionModes.length > 0)
const showPermissionSettings = Boolean(onPermissionModeChange && permissionModeOptions.length > 0)
const showModelSettings = Boolean(onModelModeChange && agentFlavor !== 'codex' && agentFlavor !== 'gemini')
const showSettingsButton = Boolean(showPermissionSettings || showModelSettings)
const showAbortButton = true
@@ -342,9 +346,9 @@ export function HappyComposer(props: {
<div className="px-3 pb-1 text-xs font-semibold text-[var(--app-hint)]">
Permission Mode
</div>
{permissionModes.map((mode) => (
{permissionModeOptions.map((option) => (
<button
key={mode}
key={option.mode}
type="button"
disabled={controlsDisabled}
className={`flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors ${
@@ -352,22 +356,22 @@ export function HappyComposer(props: {
? 'cursor-not-allowed opacity-50'
: 'cursor-pointer hover:bg-[var(--app-secondary-bg)]'
}`}
onClick={() => handlePermissionChange(mode)}
onClick={() => handlePermissionChange(option.mode)}
onMouseDown={(e) => e.preventDefault()}
>
<div
className={`flex h-4 w-4 items-center justify-center rounded-full border-2 ${
permissionMode === mode
permissionMode === option.mode
? 'border-[var(--app-link)]'
: 'border-[var(--app-hint)]'
}`}
>
{permissionMode === mode && (
{permissionMode === option.mode && (
<div className="h-2 w-2 rounded-full bg-[var(--app-link)]" />
)}
</div>
<span className={permissionMode === mode ? 'text-[var(--app-link)]' : ''}>
{PERMISSION_MODE_LABELS[mode]}
<span className={permissionMode === option.mode ? 'text-[var(--app-link)]' : ''}>
{option.label}
</span>
</button>
))}
@@ -443,7 +447,7 @@ export function HappyComposer(props: {
controlsDisabled,
permissionMode,
modelMode,
permissionModes,
permissionModeOptions,
handlePermissionChange,
handleModelChange,
handleSuggestionSelect
+17 -13
View File
@@ -1,4 +1,5 @@
import { PERMISSION_MODE_LABELS } from '@hapi/protocol'
import { getPermissionModeLabel, getPermissionModeTone, isPermissionModeAllowedForFlavor } from '@hapi/protocol'
import type { PermissionModeTone } from '@hapi/protocol'
import { useMemo } from 'react'
import type { AgentState, ModelMode, PermissionMode } from '@/types/api'
import { getContextBudgetTokens } from '@/chat/modelConfig'
@@ -22,6 +23,13 @@ const VIBING_MESSAGES = [
"Wibbling", "Wizarding", "Working", "Wrangling"
]
const PERMISSION_TONE_CLASSES: Record<PermissionModeTone, string> = {
neutral: 'text-[var(--app-hint)]',
info: 'text-blue-500',
warning: 'text-amber-500',
danger: 'text-red-500'
}
function getConnectionStatus(
active: boolean,
thinking: boolean,
@@ -103,12 +111,16 @@ export function StatusBar(props: {
)
const permissionMode = props.permissionMode
const displayPermissionMode = props.agentFlavor !== 'gemini'
&& permissionMode
const displayPermissionMode = permissionMode
&& permissionMode !== 'default'
&& isPermissionModeAllowedForFlavor(permissionMode, props.agentFlavor)
? permissionMode
: null
const permissionModeLabel = displayPermissionMode ? getPermissionModeLabel(displayPermissionMode) : null
const permissionModeTone = displayPermissionMode ? getPermissionModeTone(displayPermissionMode) : null
const permissionModeColor = permissionModeTone ? PERMISSION_TONE_CLASSES[permissionModeTone] : 'text-[var(--app-hint)]'
return (
<div className="flex items-center justify-between px-2 pb-1">
<div className="flex items-baseline gap-3">
@@ -128,16 +140,8 @@ export function StatusBar(props: {
</div>
{displayPermissionMode ? (
<span className={`text-xs ${
displayPermissionMode === 'acceptEdits' ? 'text-amber-500' :
displayPermissionMode === 'bypassPermissions' ? 'text-red-500' :
displayPermissionMode === 'plan' ? 'text-blue-500' :
displayPermissionMode === 'read-only' ? 'text-amber-500' :
displayPermissionMode === 'safe-yolo' ? 'text-amber-500' :
displayPermissionMode === 'yolo' ? 'text-red-500' :
'text-[var(--app-hint)]'
}`}>
{PERMISSION_MODE_LABELS[displayPermissionMode]}
<span className={`text-xs ${permissionModeColor}`}>
{permissionModeLabel}
</span>
) : null}
</div>
+7 -2
View File
@@ -36,7 +36,12 @@ export function SessionChat(props: {
const controlsDisabled = !props.session.active
const normalizedCacheRef = useRef<Map<string, { source: DecryptedMessage; normalized: NormalizedMessage | null }>>(new Map())
const blocksByIdRef = useRef<Map<string, ChatBlock>>(new Map())
const { abortSession, switchSession, setPermissionMode, setModelMode } = useSessionActions(props.api, props.session.id)
const agentFlavor = props.session.metadata?.flavor ?? null
const { abortSession, switchSession, setPermissionMode, setModelMode } = useSessionActions(
props.api,
props.session.id,
agentFlavor
)
useEffect(() => {
normalizedCacheRef.current.clear()
@@ -179,7 +184,7 @@ export function SessionChat(props: {
disabled={props.isSending || controlsDisabled}
permissionMode={props.session.permissionMode}
modelMode={props.session.modelMode}
agentFlavor={props.session.metadata?.flavor ?? 'claude'}
agentFlavor={agentFlavor}
active={props.session.active}
thinking={props.session.thinking}
agentState={props.session.agentState}
+5 -1
View File
@@ -74,7 +74,11 @@ export function SessionHeader(props: {
const [archiveOpen, setArchiveOpen] = useState(false)
const [deleteOpen, setDeleteOpen] = useState(false)
const { archiveSession, renameSession, deleteSession, isPending } = useSessionActions(api, session.id)
const { archiveSession, renameSession, deleteSession, isPending } = useSessionActions(
api,
session.id,
session.metadata?.flavor ?? null
)
const handleDelete = async () => {
await deleteSession()
+5 -1
View File
@@ -177,7 +177,11 @@ function SessionItem(props: {
const [archiveOpen, setArchiveOpen] = useState(false)
const [deleteOpen, setDeleteOpen] = useState(false)
const { archiveSession, renameSession, deleteSession, isPending } = useSessionActions(api, s.id)
const { archiveSession, renameSession, deleteSession, isPending } = useSessionActions(
api,
s.id,
s.metadata?.flavor ?? null
)
const longPressHandlers = useLongPress({
onLongPress: () => {
+10 -1
View File
@@ -1,9 +1,14 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { isPermissionModeAllowedForFlavor } from '@hapi/protocol'
import type { ApiClient } from '@/api/client'
import type { ModelMode, PermissionMode } from '@/types/api'
import { queryKeys } from '@/lib/query-keys'
export function useSessionActions(api: ApiClient | null, sessionId: string | null): {
export function useSessionActions(
api: ApiClient | null,
sessionId: string | null,
agentFlavor?: string | null
): {
abortSession: () => Promise<void>
archiveSession: () => Promise<void>
switchSession: () => Promise<void>
@@ -56,6 +61,10 @@ export function useSessionActions(api: ApiClient | null, sessionId: string | nul
if (!api || !sessionId) {
throw new Error('Session unavailable')
}
const isKnownFlavor = agentFlavor === 'claude' || agentFlavor === 'codex' || agentFlavor === 'gemini'
if (isKnownFlavor && !isPermissionModeAllowedForFlavor(mode, agentFlavor)) {
throw new Error('Invalid permission mode for session flavor')
}
await api.setPermissionMode(sessionId, mode)
},
onSuccess: () => void invalidateSession(),