Support interactive add/stage input flow

This commit is contained in:
Joe Tretter
2026-05-13 19:12:06 -05:00
parent 482483deed
commit 3f6a764603
7 changed files with 150 additions and 5 deletions

View File

@@ -26,6 +26,9 @@ object GitSandboxEngine {
val shellParts = tokenizeShellCommand(command)
val parts = shellParts.map { it.value }
if (parts.isEmpty()) return repo to emptyList()
repo.interactiveAddSession?.let {
return handleInteractiveAddInput(repo, command)
}
return when {
parts[0] == "help" || parts[0] == "?" -> repo to commandReferenceLines()
parts[0] == "touch" && parts.size >= 2 -> {
@@ -174,7 +177,7 @@ object GitSandboxEngine {
val candidates = repo.files.filter { file ->
!file.deleted && (target == null || target == "." || file.name == target || file.name.startsWith(target.trimEnd('/') + "/"))
}
return repo to interactiveAddConsoleLines(candidates)
return repo.copy(interactiveAddSession = InteractiveAddSession(target = target)) to interactiveAddConsoleLines(candidates)
}
private fun interactiveAddConsoleLines(candidates: List<GitFile>): List<String> {
@@ -198,6 +201,69 @@ object GitSandboxEngine {
}
}
private fun handleInteractiveAddInput(repo: RepoState, input: String): Pair<RepoState, List<String>> {
val session = repo.interactiveAddSession ?: return repo to emptyList()
val answer = input.trim()
return if (session.awaitingUpdateSelection) {
applyInteractiveAddUpdateSelection(repo, session, answer)
} else {
when (answer.lowercase()) {
"1", "s", "status" -> repo to listOf("What now> $answer") + interactiveAddConsoleLines(interactiveAddCandidates(repo, session.target))
"2", "u", "update" -> repo.copy(
interactiveAddSession = session.copy(awaitingUpdateSelection = true),
) to listOf("What now> $answer", "Update>>")
"7", "q", "quit" -> repo.copy(interactiveAddSession = null) to listOf("What now> $answer", "Bye.")
"8", "h", "help" -> repo to listOf("What now> $answer") + interactiveAddConsoleLines(interactiveAddCandidates(repo, session.target))
else -> repo to listOf("What now> $answer", "Huh ($answer)?") + interactiveAddConsoleLines(interactiveAddCandidates(repo, session.target))
}
}
}
private fun applyInteractiveAddUpdateSelection(
repo: RepoState,
session: InteractiveAddSession,
answer: String,
): Pair<RepoState, List<String>> {
val candidates = interactiveAddCandidates(repo, session.target)
val selectedNames = selectedInteractiveAddNames(candidates, answer)
if (selectedNames.isEmpty()) {
return repo to listOf("Update>> $answer", "No files selected.", "Update>>")
}
val updatedFiles = repo.files.map { file ->
if (file.name in selectedNames && !file.deleted) file.copy(staged = true) else file
}
val updatedRepo = repo.copy(
files = updatedFiles,
interactiveAddSession = session.copy(awaitingUpdateSelection = false),
)
val stagedCount = updatedFiles.count { updatedFile ->
val before = repo.files.firstOrNull { it.name == updatedFile.name }
updatedFile.staged && before?.staged != true
}
return updatedRepo to listOf(
"Update>> $answer",
"updated $stagedCount path(s)",
) + interactiveAddConsoleLines(interactiveAddCandidates(updatedRepo, session.target))
}
private fun interactiveAddCandidates(repo: RepoState, target: String?): List<GitFile> {
return repo.files.filter { file ->
!file.deleted && (target == null || target == "." || file.name == target || file.name.startsWith(target.trimEnd('/') + "/"))
}
}
private fun selectedInteractiveAddNames(candidates: List<GitFile>, answer: String): Set<String> {
if (answer == "*") return candidates.map { it.name }.toSet()
return answer.split(Regex("[,\\s]+"))
.mapNotNull { token ->
token.toIntOrNull()
?.takeIf { it in 1..candidates.size }
?.let { candidates[it - 1].name }
}
.toSet()
}
fun tokenizeCommand(command: String): List<String> {
return tokenizeShellCommand(command).map { it.value }
}