mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user