Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Palette's Journal 🎨

A record of critical UX and accessibility learnings.

## 2025-02-15 - Interactive Soft-Keyboard Forms in Jetpack Compose
**Learning:** Text input forms lacking appropriate `ImeAction` and `KeyboardActions` force the user to manually dismiss the virtual keyboard and tap submission buttons, creating friction, especially for users relying on screen readers or single-hand navigation. By configuring standard text field focus transitions and executing the button's action directly from the soft keyboard's Search/Done key, form ergonomics improve dramatically.
**Action:** Always configure `keyboardOptions = KeyboardOptions(imeAction = ...)` and `keyboardActions = KeyboardActions(on... = { ... })` for input forms with multiple fields or quick actions in Jetpack Compose.
39 changes: 35 additions & 4 deletions app/src/main/java/com/sayanthrock/freeairock/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModelProvider
Expand Down Expand Up @@ -203,7 +206,16 @@ private fun CodeAnalyzerScreen(
label = { Text("GitHub token") },
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth(),
singleLine = true
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(
onDone = {
if (githubToken.isNotBlank()) {
onSave(githubToken)
savedMessage = "Saved securely on this device"
}
}
)
Comment on lines +211 to +218

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟑 Minor | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n \
  '2024\.06\.00|compose-bom|androidx\.compose|KeyboardActionScope|defaultKeyboardAction' \
  --glob 'build.gradle*' \
  --glob 'settings.gradle*' \
  --glob 'libs.versions.toml' \
  --glob 'gradle.properties' \
  --glob '*.kt' . || true

Repository: SayanthRock/FREE-AI-ROCK-

Length of output: 20047


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== MainActivity import and keyboard section =="
sed -n '1,50p;160,235p' app/src/main/java/com/sayanthrock/freeairock/MainActivity.kt | cat -n

echo
echo "== KeyboardActions usages =="
rg -n "KeyboardActions|defaultKeyboardAction|LocalSoftwareKeyboardController|dismiss|ImeAction" app/src/main/java -S || true

Repository: SayanthRock/FREE-AI-ROCK-

Length of output: 7683


🌐 Web query:

Android Jetpack Compose KeyboardActions defaultKeyboardAction ImeAction Done source documentation

πŸ’‘ Result:

In Jetpack Compose, defaultKeyboardAction is a function provided within the KeyboardActionScope interface [1]. It is designed to trigger the system's default behavior associated with a specific ImeAction [1]. When you invoke defaultKeyboardAction(ImeAction.Done), it executes the standard implementation for the "Done" action, which is to close the software keyboard [2]. Key technical details include: - KeyboardActionScope: This interface provides the context for executing custom keyboard actions [1]. Within its scope, you can call defaultKeyboardAction(imeAction) to trigger the default behavior for any given ImeAction [1]. - ImeAction.Done: This constant represents the "Done" action, signaling that the user has finished providing input to a field or a group of fields [3]. When used with defaultKeyboardAction, it specifically invokes the platform-level implementation that hides the software keyboard [2]. - Default Behavior: If you provide a custom lambda for an action (e.g., onDone in KeyboardActions), the default behavior is overridden [4]. To execute the default behavior while still performing custom logic, you can manually call defaultKeyboardAction(ImeAction.Done) inside your custom callback [1]. This mechanism ensures that developers can maintain standard platform expectationsβ€”such as keyboard dismissal upon completionβ€”while still having the flexibility to implement custom application-specific logic [4][5].

Citations:


Restore the default Done action after saving the token.

KeyboardActions.onDone overrides the default IME action. The custom callback saves the token but does not hide the keyboard, so the confirmation text can remain covered by the software keyboard. Run defaultKeyboardAction(ImeAction.Done) after saving to keep the normal Done behavior.

Proposed fix
                         if (githubToken.isNotBlank()) {
                             onSave(githubToken)
                             savedMessage = "Saved securely on this device"
+                            defaultKeyboardAction(ImeAction.Done)
                         }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
keyboardActions = KeyboardActions(
onDone = {
if (githubToken.isNotBlank()) {
onSave(githubToken)
savedMessage = "Saved securely on this device"
}
}
)
keyboardActions = KeyboardActions(
onDone = {
if (githubToken.isNotBlank()) {
onSave(githubToken)
savedMessage = "Saved securely on this device"
defaultKeyboardAction(ImeAction.Done)
}
}
)
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/sayanthrock/freeairock/MainActivity.kt` around lines
211 - 218, Update the KeyboardActions.onDone callback in the token input to
invoke defaultKeyboardAction(ImeAction.Done) after the existing save logic,
preserving token validation and confirmation behavior while restoring the
default keyboard dismissal.

)


Expand Down Expand Up @@ -243,17 +255,36 @@ private fun CodeAnalyzerScreen(
onValueChange = { fileName = it },
label = { Text("File name") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next)
)

Spacer(modifier = Modifier.height(12.dp))

OutlinedTextField(
value = fileUrl,
onValueChange = { fileUrl = it },
onValueChange = { url ->
fileUrl = url
if (url.isNotBlank()) {
val cleanUrl = url.substringBefore("?").substringBefore("#")
val parts = cleanUrl.split('/')
val lastPart = parts.lastOrNull()
if (!lastPart.isNullOrBlank() && lastPart.contains('.')) {
fileName = lastPart
}
}
},
Comment on lines +266 to +276

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Every nonblank URL edit overwrites fileName whenever the final URL segment contains a dot. If a user enters or pastes a URL and then intentionally changes the filename, any subsequent URL edit silently discards that custom value, and the overwritten name is passed to analyzeCode. Restrict auto-extraction to an untouched/default filename or otherwise preserve an explicitly edited filename. [logic error]

Severity Level: Major ⚠️
- ⚠️ Manual filename choices are lost during URL edits.
- ❌ Code analysis can target the wrong filename.
- ⚠️ Search submission passes the silently replaced value.

Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent πŸ€–
This is a comment left during a code review.

**Path:** app/src/main/java/com/sayanthrock/freeairock/MainActivity.kt
**Line:** 266:276
**Comment:**
	*Logic Error: Every nonblank URL edit overwrites `fileName` whenever the final URL segment contains a dot. If a user enters or pastes a URL and then intentionally changes the filename, any subsequent URL edit silently discards that custom value, and the overwritten name is passed to `analyzeCode`. Restrict auto-extraction to an untouched/default filename or otherwise preserve an explicitly edited filename.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
πŸ‘ | πŸ‘Ž

Comment on lines +266 to +276

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟑 Minor | ⚑ Quick win

Extract names for extensionless GitHub files.

The lastPart.contains('.') check excludes valid files such as LICENSE, README, and Dockerfile. For these URLs, fileName remains MainActivity.kt or a previous value, so onAnalyze(fileName, fileUrl) receives the wrong filename. A URL that is cleared or ends with / can also leave the previous filename in place. Parse a non-empty final segment for recognized raw/blob file URLs and reset the derived name when no filename exists.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/sayanthrock/freeairock/MainActivity.kt` around lines
266 - 276, Update the URL onValueChange handler to derive fileName from any
non-empty final URL segment, including extensionless GitHub filenames such as
LICENSE, README, and Dockerfile; remove the dot requirement. Reset fileName when
the URL is blank or ends with a slash, and preserve the existing behavior for
recognized raw/blob file URLs only.

label = { Text("GitHub raw/blob file URL") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(
onSearch = {
if (uiState !is CodeAnalysisState.Loading && fileUrl.isNotBlank()) {
onAnalyze(fileName, fileUrl)
}
}
)
)

Spacer(modifier = Modifier.height(16.dp))
Expand Down
20 changes: 17 additions & 3 deletions app/src/main/java/com/sayanthrock/freeairock/ui/ReviewScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
Expand All @@ -28,6 +29,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.sayanthrock.freeairock.data.ai.CodeAnalysisState
Expand Down Expand Up @@ -69,15 +71,17 @@ fun ReviewScreen(
onValueChange = { ownerText = it },
label = { Text("Owner") },
modifier = Modifier.weight(1f),
singleLine = true
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next)
)

OutlinedTextField(
value = repoText,
onValueChange = { repoText = it },
label = { Text("Repo") },
modifier = Modifier.weight(1f),
singleLine = true
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next)
)
}

Expand All @@ -87,7 +91,17 @@ fun ReviewScreen(
value = prNumberText,
onValueChange = { prNumberText = it },
label = { Text("Pull Request #") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Search
),
keyboardActions = KeyboardActions(
onSearch = {
if (uiState !is CodeAnalysisState.Loading && prNumberText.isNotBlank()) {
viewModel.run(ownerText, repoText, prNumberText)
}
}
),
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
Expand Down
Loading