Align tests with runtime sandbox and polish terminal help

- Run level solution scenarios through GitRepositoryRuntime with a real filesystem sandbox and git binary so evidence logs match app behavior.
- Materialize synthetic remotes as local bare repositories and configure upstream tracking for pull/push levels.
- Route mobile-hostile Git interactions through shared sandbox semantics for clone, patch add, interactive rebase, squash merge, submodule, stash, revert, and related flows.
- Relax affected level validators to inspect observable runtime state instead of fallback-only staged/index details.
- Show .git in ls output for native and fallback helper commands.
- Make tab completion resolve filenames and directories relative to the current working directory.
- Integrate help controls into the callout overlay and reposition the prompt callout above the terminal prompt.
- Add regression coverage for .git listing and subfolder completion.
This commit is contained in:
Joe Tretter
2026-05-06 22:12:25 -05:00
parent e1b2d7f7a1
commit 7bb14216ef
14 changed files with 241 additions and 69 deletions

View File

@@ -629,22 +629,34 @@ fun GitHugApp() {
}
}
private fun fileCompletionCandidates(repo: RepoState): List<String> {
internal fun fileCompletionCandidates(repo: RepoState): List<String> {
val prefix = repo.currentDirPrefix()
return repo.files
.filterNot { it.deleted }
.map { it.name }
.filter { it.startsWith(prefix) }
.map { it.removePrefix(prefix) }
.filter { it.isNotBlank() }
}
private fun directoryCompletionCandidates(repo: RepoState): List<String> {
internal fun directoryCompletionCandidates(repo: RepoState): List<String> {
val prefix = repo.currentDirPrefix()
return repo.files
.filterNot { it.deleted }
.map { it.name }
.filter { it.startsWith(prefix) }
.map { it.removePrefix(prefix) }
.flatMap { file ->
val parts = file.name.split('/').dropLast(1)
val parts = file.split('/').dropLast(1)
parts.indices.map { index -> parts.take(index + 1).joinToString("/") + "/" }
}
.distinct()
}
private fun RepoState.currentDirPrefix(): String {
return if (currentDir == ".") "" else currentDir.trimEnd('/') + "/"
}
@Composable
private fun HelpCalloutOverlay(
showHelpOnStart: Boolean,
@@ -661,37 +673,42 @@ private fun HelpCalloutOverlay(
text = "Read the exercise description, then solve it by entering commands below.",
modifier = Modifier.align(Alignment.TopStart),
)
HelpBubble(
text = "Tap the prompt to enter a command.",
modifier = Modifier.align(Alignment.BottomStart),
)
Surface(
HelpBubbleWithControls(
modifier = Modifier
.align(Alignment.Center)
.fillMaxWidth(),
color = PanelPrimary.copy(alpha = 0.97f),
shape = RoundedCornerShape(8.dp),
shadowElevation = 8.dp,
.align(Alignment.BottomStart)
.padding(bottom = 54.dp),
text = "Tap the prompt to enter a command.",
showHelpOnStart = showHelpOnStart,
onShowHelpOnStartChange = onShowHelpOnStartChange,
onOk = onOk,
onClose = onClose,
)
}
}
@Composable
private fun HelpBubbleWithControls(
text: String,
showHelpOnStart: Boolean,
onShowHelpOnStartChange: (Boolean) -> Unit,
onOk: () -> Unit,
onClose: () -> Unit,
modifier: Modifier = Modifier,
) {
val bubbleColor = Color(0xFFFFF1A8)
Column(modifier = modifier.fillMaxWidth(0.9f)) {
Box(
modifier = Modifier
.background(bubbleColor, RoundedCornerShape(18.dp))
.padding(horizontal = 14.dp, vertical = 10.dp),
) {
Column(
modifier = Modifier.padding(14.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "Help",
color = TextPrimary,
fontWeight = FontWeight.Bold,
fontSize = 18.sp,
)
TextButton(onClick = onClose) {
Text("Close", color = Accent)
}
}
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = text,
color = Color(0xFF161000),
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
@@ -700,24 +717,44 @@ private fun HelpCalloutOverlay(
checked = showHelpOnStart,
onCheckedChange = onShowHelpOnStartChange,
colors = CheckboxDefaults.colors(
checkedColor = Accent,
uncheckedColor = TextSecondary,
checkmarkColor = AppBackground,
checkedColor = Color(0xFF161000),
uncheckedColor = Color(0xFF6E5A00),
checkmarkColor = bubbleColor,
),
)
Text("Show help on start", color = TextPrimary)
Text("Show help on start", color = Color(0xFF161000))
}
Button(
onClick = onOk,
colors = ButtonDefaults.buttonColors(
containerColor = Accent,
contentColor = AppBackground,
),
) {
Text("OK")
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(
onClick = onOk,
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF161000),
contentColor = bubbleColor,
),
) {
Text("OK")
}
TextButton(onClick = onClose) {
Text("Close", color = Color(0xFF161000))
}
}
}
}
Canvas(
modifier = Modifier
.padding(start = 28.dp)
.size(width = 26.dp, height = 13.dp),
) {
drawPath(
path = Path().apply {
moveTo(0f, 0f)
lineTo(size.width, 0f)
lineTo(size.width * 0.25f, size.height)
close()
},
color = bubbleColor,
)
}
}
}