import { basename, sep } from 'path'
import React, { type ReactNode } from 'react'
import { getOriginalCwd } from '../../bootstrap/state.js'
import { Text } from '../../ink.js'
import type { PermissionUpdate } from '../../utils/permissions/PermissionUpdateSchema.js'
import { permissionRuleExtractPrefix } from '../../utils/permissions/shellRuleMatching.js'
function commandListDisplay(commands: string[]): ReactNode {
switch (commands.length) {
case 0:
return ''
case 1:
return {commands[0]}
case 2:
return (
{commands[0]} and {commands[1]}
)
default:
return (
{commands.slice(0, -1).join(', ')}, and{' '}
{commands.slice(-1)[0]}
)
}
}
function commandListDisplayTruncated(commands: string[]): ReactNode {
// Check if the plain text representation would be too long
const plainText = commands.join(', ')
if (plainText.length > 50) {
return 'similar'
}
return commandListDisplay(commands)
}
function formatPathList(paths: string[]): ReactNode {
if (paths.length === 0) return ''
// Extract directory names from paths
const names = paths.map(p => basename(p) || p)
if (names.length === 1) {
return (
{names[0]}
{sep}
)
}
if (names.length === 2) {
return (
{names[0]}
{sep} and {names[1]}
{sep}
)
}
// For 3+, show first two with "and N more"
return (
{names[0]}
{sep}, {names[1]}
{sep} and {paths.length - 2} more
)
}
/**
* Generate the label for the "Yes, and apply suggestions" option in shell
* permission dialogs (Bash, PowerShell). Parametrized by the shell tool name
* and an optional command transform (e.g., Bash strips output redirections so
* filenames don't show as commands).
*/
export function generateShellSuggestionsLabel(
suggestions: PermissionUpdate[],
shellToolName: string,
commandTransform?: (command: string) => string,
): ReactNode | null {
// Collect all rules for display
const allRules = suggestions
.filter(s => s.type === 'addRules')
.flatMap(s => s.rules || [])
// Separate Read rules from shell rules
const readRules = allRules.filter(r => r.toolName === 'Read')
const shellRules = allRules.filter(r => r.toolName === shellToolName)
// Get directory info
const directories = suggestions
.filter(s => s.type === 'addDirectories')
.flatMap(s => s.directories || [])
// Extract paths from Read rules (keep separate from directories)
const readPaths = readRules
.map(r => r.ruleContent?.replace('/**', '') || '')
.filter(p => p)
// Extract shell command prefixes, optionally transforming for display
const shellCommands = [
...new Set(
shellRules.flatMap(rule => {
if (!rule.ruleContent) return []
const command =
permissionRuleExtractPrefix(rule.ruleContent) ?? rule.ruleContent
return commandTransform ? commandTransform(command) : command
}),
),
]
// Check what we have
const hasDirectories = directories.length > 0
const hasReadPaths = readPaths.length > 0
const hasCommands = shellCommands.length > 0
// Handle single type cases
if (hasReadPaths && !hasDirectories && !hasCommands) {
// Only Read rules - use "reading from" language
if (readPaths.length === 1) {
const firstPath = readPaths[0]!
const dirName = basename(firstPath) || firstPath
return (
Yes, allow reading from {dirName}
{sep} from this project
)
}
// Multiple read paths
return (
Yes, allow reading from {formatPathList(readPaths)} from this project
)
}
if (hasDirectories && !hasReadPaths && !hasCommands) {
// Only directory permissions - use "access to" language
if (directories.length === 1) {
const firstDir = directories[0]!
const dirName = basename(firstDir) || firstDir
return (
Yes, and always allow access to {dirName}
{sep} from this project
)
}
// Multiple directories
return (
Yes, and always allow access to {formatPathList(directories)} from this
project
)
}
if (hasCommands && !hasDirectories && !hasReadPaths) {
// Only shell command permissions
return (
{"Yes, and don't ask again for "}
{commandListDisplayTruncated(shellCommands)} commands in{' '}
{getOriginalCwd()}
)
}
// Handle mixed cases
if ((hasDirectories || hasReadPaths) && !hasCommands) {
// Combine directories and read paths since they're both path access
const allPaths = [...directories, ...readPaths]
if (hasDirectories && hasReadPaths) {
// Mixed - use generic "access to"
return (
Yes, and always allow access to {formatPathList(allPaths)} from this
project
)
}
}
if ((hasDirectories || hasReadPaths) && hasCommands) {
// Build descriptive message for both types
const allPaths = [...directories, ...readPaths]
// Keep it concise but informative
if (allPaths.length === 1 && shellCommands.length === 1) {
return (
Yes, and allow access to {formatPathList(allPaths)} and{' '}
{commandListDisplayTruncated(shellCommands)} commands
)
}
return (
Yes, and allow {formatPathList(allPaths)} access and{' '}
{commandListDisplayTruncated(shellCommands)} commands
)
}
return null
}