From 5a4386ebe32ef9970962b0c9cba8c01d9840737d Mon Sep 17 00:00:00 2001 From: GPTomek <314538226+gptomek[bot]@users.noreply.github.com> Date: Sat, 17 Oct 2026 20:57:59 +0000 Subject: [PATCH 1/2] feat(agent-lab): add Claude Code Android task handoff --- README.md | 1 + .../tee/ui/screens/ClaudeCodeHandoffPanel.kt | 201 ++++++++++++++++++ .../tee/ui/screens/StudioToolsHubScreen.kt | 7 + .../java/ais/tee/ui/screens/WebChatScreen.kt | 34 +++ .../ais/tee/ui/screens/WebNavigationSafety.kt | 4 +- .../tee/ui/screens/ClaudeCodeHandoffTest.kt | 52 +++++ .../ui/screens/StudioToolsHubScreenTest.kt | 4 + docs/provider-runtime.md | 8 + 8 files changed, 309 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/ais/tee/ui/screens/ClaudeCodeHandoffPanel.kt create mode 100644 app/src/test/java/ais/tee/ui/screens/ClaudeCodeHandoffTest.kt diff --git a/README.md b/README.md index 2e4bffec8..9fd914763 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Android also accepts shared text, images and application files. Web content is s | --- | --- | --- | --- | --- | --- | | ChatGPT Web | Full | Provider WebView | Yes | Generating + unread | Persistent session; mobile/desktop mode; Codex Cloud shortcut | | Claude Web | Full | Provider WebView | Yes | Generating + unread | Persistent session | +| Claude Code (official app) | External handoff | Claude-owned app/site | Via Claude | Not tracked by Aistee | Studio Tools → Agents; optional plan/code task, GitHub repo + branch, explicit confirmation, no token proxy | | Gemini Chat Web | Partial | Fresh Google OAuth may reject embedded user-agents | Yes | Generating + unread | Existing sessions work; embedded OAuth is provider-limited | | DeepSeek Web | Full | Provider WebView | Yes | Generating + unread | Persistent session | | Kimi Web | Full | Provider WebView | Yes | Generating + unread | Persistent session | diff --git a/app/src/main/java/ais/tee/ui/screens/ClaudeCodeHandoffPanel.kt b/app/src/main/java/ais/tee/ui/screens/ClaudeCodeHandoffPanel.kt new file mode 100644 index 000000000..bebc027ec --- /dev/null +++ b/app/src/main/java/ais/tee/ui/screens/ClaudeCodeHandoffPanel.kt @@ -0,0 +1,201 @@ +package ais.tee.ui.screens + +import android.net.Uri +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.FilterChip +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import java.net.URLEncoder + +/** Only official Claude Code links. Aistee does not access Claude authentication or run a proxy. */ +internal object ClaudeCodeHandoff { + const val SESSIONS_URL = "claude://code" + const val WEB_URL = "https://claude.ai/code" + private const val NEW_SESSION_URL = "https://claude.ai/code" + const val MAX_TASK_CHARS = 2_000 + private val ownerPattern = Regex("^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$") + private val repoPattern = Regex("^[A-Za-z0-9._-]{1,100}$") + + private fun validRepository(value: String): Boolean { + val parts = value.split("/") + return parts.size == 2 && ownerPattern.matches(parts[0]) && + repoPattern.matches(parts[1]) && parts[1] !in setOf(".", "..") + } + + private fun validBranch(value: String): Boolean { + if (value.isEmpty() || value.length > 150 || value == "@" || + value.startsWith("-") || value.startsWith("/") || value.endsWith("/") + ) return false + if (value.contains("..") || value.contains("@{") || value.contains("//")) return false + if (value.any { it.isWhitespace() || it.code < 32 || it.code == 127 || + it.code == 92 || it in "~^:?*[" + } + ) return false + return value.split("/").all { part -> + part.isNotEmpty() && !part.startsWith(".") && !part.endsWith(".") && + !part.endsWith(".lock") + } + } + + enum class Mode(val value: String) { PLAN("plan"), CODE("code") } + + fun canOpen(task: String, repository: String, branch: String): Boolean { + val repo = repository.trim() + val ref = branch.trim() + return task.isNotBlank() && task.length <= MAX_TASK_CHARS && + (repo.isEmpty() || validRepository(repo)) && + (ref.isEmpty() || (repo.isNotEmpty() && validBranch(ref))) + } + + fun newSessionUrl(task: String, repository: String = "", branch: String = "", mode: Mode = Mode.PLAN): String { + require(canOpen(task, repository, branch)) { "Invalid Claude Code handoff fields" } + fun encode(value: String): String = URLEncoder.encode(value, "UTF-8").replace("+", "%20") + return buildString { + append(NEW_SESSION_URL) + append("?prompt=").append(encode(buildString { + if (mode == Mode.PLAN) append("Plan the work before making changes.\n\n") + if (branch.isNotBlank()) append("Preferred branch: ").append(branch.trim()).append("\n\n") + append(task.trim()) + })) + if (repository.isNotBlank()) { + append("&repositories=").append(encode(repository.trim())) + } + } + } +} + +/** Optional subscription-account handoff inside the maintained Studio Tools hub. */ +@Composable +internal fun ClaudeCodeHandoffPanel(modifier: Modifier = Modifier) { + val context = LocalContext.current + var task by rememberSaveable { mutableStateOf("") } + var repository by rememberSaveable { mutableStateOf("") } + var branch by rememberSaveable { mutableStateOf("") } + var mode by rememberSaveable { mutableStateOf(ClaudeCodeHandoff.Mode.PLAN) } + var confirmation by remember { mutableStateOf(false) } + var notice by remember { mutableStateOf(null) } + + Column( + modifier = modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp) + .testTag("claude_code_handoff"), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text("Claude Code", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + Text( + "Open a task in the official Claude app or website, using your own Claude account. " + + "No Aistee API key, token transfer or paired computer is needed.", + style = MaterialTheme.typography.bodySmall, + ) + OutlinedTextField( + value = task, + onValueChange = { if (it.length <= ClaudeCodeHandoff.MAX_TASK_CHARS) task = it }, + label = { Text("Task") }, + minLines = 3, + maxLines = 6, + modifier = Modifier.fillMaxWidth().testTag("claude_code_task"), + ) + OutlinedTextField( + value = repository, + onValueChange = { repository = it; if (it.isBlank()) branch = "" }, + label = { Text("GitHub owner/repository (optional)") }, + singleLine = true, + modifier = Modifier.fillMaxWidth().testTag("claude_code_repo"), + ) + OutlinedTextField( + value = branch, + onValueChange = { branch = it }, + label = { Text("Preferred branch (prompt hint)") }, + enabled = repository.isNotBlank(), + singleLine = true, + modifier = Modifier.fillMaxWidth().testTag("claude_code_branch"), + ) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + ClaudeCodeHandoff.Mode.entries.forEach { choice -> + FilterChip( + selected = choice == mode, + onClick = { mode = choice }, + label = { Text(if (choice == ClaudeCodeHandoff.Mode.PLAN) "Plan" else "Code") }, + ) + } + } + Text( + "Claude controls access, usage limits and optional extra credits. " + + "No local Aistee files or chats are attached.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + "Plan and branch are prompt hints. Confirm the actual mode and branch in Claude Code.", + style = MaterialTheme.typography.bodySmall, + ) + Button( + onClick = { confirmation = true }, + enabled = ClaudeCodeHandoff.canOpen(task, repository, branch), + modifier = Modifier.testTag("claude_code_open"), + ) { Text("Open in Claude Code") } + TextButton(onClick = { + notice = if (openExternalUri(context, Uri.parse(ClaudeCodeHandoff.SESSIONS_URL))) { + null + } else if (openExternalUri(context, Uri.parse(ClaudeCodeHandoff.WEB_URL))) { + "Claude app unavailable. Opened the Claude Code website instead." + } else { + "No app or browser could open Claude Code." + } + }) { Text("Open Claude Code sessions") } + notice?.let { Text(it, color = MaterialTheme.colorScheme.error) } + } + + if (confirmation) { + AlertDialog( + onDismissRequest = { confirmation = false }, + title = { Text("Send task to Claude?") }, + text = { + Column( + modifier = Modifier.heightIn(max = 320.dp).verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text("Review everything sent to the official Claude Code composer:") + Text("Mode: " + mode.value) + if (repository.isNotBlank()) Text("Repository: " + repository.trim()) + if (branch.isNotBlank()) Text("Branch: " + branch.trim()) + Text("Task:", fontWeight = FontWeight.Bold) + Text(task, style = MaterialTheme.typography.bodySmall) + } + }, + confirmButton = { + TextButton(onClick = { + confirmation = false + val url = ClaudeCodeHandoff.newSessionUrl(task, repository, branch, mode) + notice = if (openExternalUri(context, Uri.parse(url))) null + else "No app or browser could open Claude Code." + }) { Text("Open Claude") } + }, + dismissButton = { + TextButton(onClick = { confirmation = false }) { Text("Cancel") } + }, + ) + } +} diff --git a/app/src/main/java/ais/tee/ui/screens/StudioToolsHubScreen.kt b/app/src/main/java/ais/tee/ui/screens/StudioToolsHubScreen.kt index bad69b24a..6a1f9968e 100644 --- a/app/src/main/java/ais/tee/ui/screens/StudioToolsHubScreen.kt +++ b/app/src/main/java/ais/tee/ui/screens/StudioToolsHubScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import ais.tee.ui.viewmodel.StudioViewModel @@ -16,6 +17,7 @@ import ais.tee.ui.viewmodel.StudioViewModel internal enum class StudioToolsSection { SKILLS, BENCHES, + AGENTS, JOBS, PRIVACY } @@ -31,6 +33,7 @@ internal enum class StudioToolsSection { @Composable fun SkillsBrowserScreen(viewModel: StudioViewModel) { var section by rememberSaveable { mutableStateOf(StudioToolsSection.SKILLS) } + val stateHolder = rememberSaveableStateHolder() Column(modifier = Modifier.fillMaxSize()) { PrimaryTabRow(selectedTabIndex = section.ordinal) { @@ -43,6 +46,7 @@ fun SkillsBrowserScreen(viewModel: StudioViewModel) { when (candidate) { StudioToolsSection.SKILLS -> "Skills" StudioToolsSection.BENCHES -> "Benches" + StudioToolsSection.AGENTS -> "Agents" StudioToolsSection.JOBS -> "Jobs" StudioToolsSection.PRIVACY -> "Privacy" } @@ -60,6 +64,9 @@ fun SkillsBrowserScreen(viewModel: StudioViewModel) { StudioToolsSection.BENCHES -> BenchToolsScreen( modifier = Modifier.weight(1f) ) + StudioToolsSection.AGENTS -> stateHolder.SaveableStateProvider("claude-code") { + ClaudeCodeHandoffPanel(modifier = Modifier.weight(1f)) + } StudioToolsSection.JOBS -> AsyncJobsPanel( viewModel = viewModel, modifier = Modifier.weight(1f) diff --git a/app/src/main/java/ais/tee/ui/screens/WebChatScreen.kt b/app/src/main/java/ais/tee/ui/screens/WebChatScreen.kt index 42e66537e..c7cdf9e62 100644 --- a/app/src/main/java/ais/tee/ui/screens/WebChatScreen.kt +++ b/app/src/main/java/ais/tee/ui/screens/WebChatScreen.kt @@ -178,6 +178,40 @@ fun WebChatScreen( focusManager.clearFocus() keyboard?.hide() } + // SPA pushState/replaceState may not trigger WebViewClient navigation callbacks. + // Compare successive JS locations only while Find is active, never the two JSON encodings. + val webChatActive by rememberUpdatedState(isActive) + LaunchedEffect(findSession, isActive) { + if (!isActive) return@LaunchedEffect + val activeSearch = findSession ?: return@LaunchedEffect + var stableLocation: String? = null + var candidateLocation: String? = null + var candidateReads = 0 + while (findSession === activeSearch && webChatActive) { + activeSearch.webView.evaluateJavascript("window.location.href") { observed -> + if (webChatActive && findSession === activeSearch) { + val location = runCatching { + org.json.JSONTokener(observed).nextValue() as? String + }.getOrNull() + if (location != null) { + if (stableLocation == null || activeSearch.query.isBlank()) { + stableLocation = location + candidateLocation = null + candidateReads = 0 + } else if (location != stableLocation) { + candidateReads = if (candidateLocation == location) candidateReads + 1 else 1 + candidateLocation = location + if (candidateReads >= 2) closeFindInPage() + } else { + candidateLocation = null + candidateReads = 0 + } + } + } + } + delay(300L) + } + } val navigationSuiteType = NavigationSuiteScaffoldDefaults.navigationSuiteType( currentWindowAdaptiveInfoV2() ) diff --git a/app/src/main/java/ais/tee/ui/screens/WebNavigationSafety.kt b/app/src/main/java/ais/tee/ui/screens/WebNavigationSafety.kt index 0d2658e28..910d08ed1 100644 --- a/app/src/main/java/ais/tee/ui/screens/WebNavigationSafety.kt +++ b/app/src/main/java/ais/tee/ui/screens/WebNavigationSafety.kt @@ -66,8 +66,8 @@ internal fun openExternalUri(context: Context, uri: Uri): Boolean { } val error = runCatching { context.startActivity(intent) }.exceptionOrNull() ?: return true when (error) { - is ActivityNotFoundException -> Log.w(WEBVIEW_LOG_TAG, "External URI handler unavailable", error) - is SecurityException -> Log.w(WEBVIEW_LOG_TAG, "External URI launch rejected", error) + is ActivityNotFoundException -> Log.w(WEBVIEW_LOG_TAG, "External URI handler unavailable") + is SecurityException -> Log.w(WEBVIEW_LOG_TAG, "External URI launch rejected") else -> throw error } return false diff --git a/app/src/test/java/ais/tee/ui/screens/ClaudeCodeHandoffTest.kt b/app/src/test/java/ais/tee/ui/screens/ClaudeCodeHandoffTest.kt new file mode 100644 index 000000000..32e72c686 --- /dev/null +++ b/app/src/test/java/ais/tee/ui/screens/ClaudeCodeHandoffTest.kt @@ -0,0 +1,52 @@ +package ais.tee.ui.screens + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Test + +class ClaudeCodeHandoffTest { + @Test + fun officialLinkEncodesTaskAndGitHubContextWithoutAnyCredentials() { + val link = ClaudeCodeHandoff.newSessionUrl( + task = "Fix failing CI & add tests", + repository = "travnie/aistee", + branch = "feat/agent-lab", + ) + assertEquals( + "https://claude.ai/code?prompt=Plan%20the%20work%20before%20making%20changes.%0A%0A" + + "Preferred%20branch%3A%20feat%2Fagent-lab%0A%0AFix%20failing%20CI%20%26%20add%20tests" + + "&repositories=travnie%2Faistee", + link, + ) + assertFalse(link.contains("token")) + assertFalse(link.contains("api_key")) + } + + @Test + fun codeModeAndNoRepositoryRemainValid() { + assertEquals( + "https://claude.ai/code?prompt=Review%20a%20patch", + ClaudeCodeHandoff.newSessionUrl(" Review a patch ", mode = ClaudeCodeHandoff.Mode.CODE), + ) + assertEquals("claude://code", ClaudeCodeHandoff.SESSIONS_URL) + assertEquals("https://claude.ai/code", ClaudeCodeHandoff.WEB_URL) + } + + @Test + fun invalidTaskAndReferencesCannotOpen() { + assertFalse(ClaudeCodeHandoff.canOpen(" ", "", "")) + assertFalse(ClaudeCodeHandoff.canOpen("x".repeat(2_001), "", "")) + assertFalse(ClaudeCodeHandoff.canOpen("task", "invalid-repo", "")) + assertFalse(ClaudeCodeHandoff.canOpen("task", "", "main")) + assertFalse(ClaudeCodeHandoff.canOpen("task", "trvny/repo", "bad branch?name")) + assertThrows(IllegalArgumentException::class.java) { + ClaudeCodeHandoff.newSessionUrl("task", "bad/repo/extra") + } + assertTrue(ClaudeCodeHandoff.canOpen("task", "trvny/repo", "feature/task")) + assertTrue(ClaudeCodeHandoff.canOpen("task", "trvny/repo", "release+hotfix@2")) + assertFalse(ClaudeCodeHandoff.canOpen("task", "trvny/repo", "bad..ref")) + assertFalse(ClaudeCodeHandoff.canOpen("task", "trvny/repo", "dir/.hidden")) + } +} diff --git a/app/src/test/java/ais/tee/ui/screens/StudioToolsHubScreenTest.kt b/app/src/test/java/ais/tee/ui/screens/StudioToolsHubScreenTest.kt index 87a208ee9..eed796c19 100644 --- a/app/src/test/java/ais/tee/ui/screens/StudioToolsHubScreenTest.kt +++ b/app/src/test/java/ais/tee/ui/screens/StudioToolsHubScreenTest.kt @@ -8,4 +8,8 @@ class StudioToolsHubScreenTest { fun skillsRemainTheFirstStudioToolsSection() { assertEquals(StudioToolsSection.SKILLS, StudioToolsSection.entries.first()) } + @Test + fun agentsAreAnOptionalSectionOfTheExistingToolsHub() { + assertEquals(StudioToolsSection.AGENTS, StudioToolsSection.entries[2]) + } } diff --git a/docs/provider-runtime.md b/docs/provider-runtime.md index eff8d0718..6eafb621c 100644 --- a/docs/provider-runtime.md +++ b/docs/provider-runtime.md @@ -215,3 +215,11 @@ Native Android tool calling now uses a separate namespaced tool schema on the `A **Claude and Google subscription boundaries (verified October 2026):** Anthropic's [Help Center](https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan) says subscription allowances may cover Claude Agent SDK / `claude -p` / third-party usage, while its [legal guidance](https://code.claude.com/docs/en/legal-and-compliance) prohibits a third-party app from offering Claude.ai sign-in or brokering Claude consumer tokens. Do not implement direct Claude subscription OAuth or credential transfer in Aistee. An independently authenticated, unmodified Claude Code runtime is a possible optional adapter, subject to Anthropic's current terms. Google [Antigravity FAQ](https://www.antigravity.google/docs/faq/) and [terms](https://antigravity.google/terms) prohibit consumer-plan third-party bridging; use the official app separately or a distinct API-key route. Neither subscription route may silently incur metered API usage. Protocol reference: https://developers.openai.com/siwc/token-sharing-open-source/codex-app-server + +### Claude Code on Android (official account handoff) + +Studio Tools → Agents now offers **Claude Code** as an explicit, outbound handoff through the official https://claude.ai/code/new universal link. The user composes a task in Aistee, optionally selects a GitHub owner/repository and branch, and chooses Plan (the default) or Code mode. A confirmation gate shows the text before opening the Claude app or browser. The separate Open Claude Code sessions action uses the official claude://code Android deep link, falling back to the Claude Code website if the app is unavailable (the HTTPS fallback may open the new-session composer). There is no silent sharing of existing native chats, files, API keys or tokens. Claude handles login, model access, session storage, plan allowance and any extra usage on its own site/app. + +This is an **independent Android entry point**, not a Claude inference adapter, API proxy, Agent SDK client or remote shell bridge. It works without Legion or any paired computer, subject to Claude Code availability on the user's account. It never bypasses Anthropic's published restrictions on third-party Claude OAuth or consumer credential brokerage. + +Official documented URL parameters: https://support.claude.com/en/articles/14898120-open-the-claude-mobile-app-with-a-link . Future optional CLI support may integrate only the unmodified Claude Code binary independently authenticated by its end user, with consent and transport security; it must not replace the ordinary standalone Aistee workflow or silently fall back to metered Claude API billing. From 16a3743a0da36c759b1800767ee9df697c2fcf31 Mon Sep 17 00:00:00 2001 From: GPTomek <314538226+gptomek[bot]@users.noreply.github.com> Date: Sun, 18 Oct 2026 08:39:00 +0000 Subject: [PATCH 2/2] fix(agent-lab): honor official Claude mobile link parameters --- .../tee/ui/screens/ClaudeCodeHandoffPanel.kt | 18 ++++++------------ .../tee/ui/screens/ClaudeCodeHandoffTest.kt | 7 +++---- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/ais/tee/ui/screens/ClaudeCodeHandoffPanel.kt b/app/src/main/java/ais/tee/ui/screens/ClaudeCodeHandoffPanel.kt index bebc027ec..715f71a0c 100644 --- a/app/src/main/java/ais/tee/ui/screens/ClaudeCodeHandoffPanel.kt +++ b/app/src/main/java/ais/tee/ui/screens/ClaudeCodeHandoffPanel.kt @@ -34,7 +34,7 @@ import java.net.URLEncoder internal object ClaudeCodeHandoff { const val SESSIONS_URL = "claude://code" const val WEB_URL = "https://claude.ai/code" - private const val NEW_SESSION_URL = "https://claude.ai/code" + private const val NEW_SESSION_URL = "https://claude.ai/code/new" const val MAX_TASK_CHARS = 2_000 private val ownerPattern = Regex("^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$") private val repoPattern = Regex("^[A-Za-z0-9._-]{1,100}$") @@ -75,13 +75,11 @@ internal object ClaudeCodeHandoff { fun encode(value: String): String = URLEncoder.encode(value, "UTF-8").replace("+", "%20") return buildString { append(NEW_SESSION_URL) - append("?prompt=").append(encode(buildString { - if (mode == Mode.PLAN) append("Plan the work before making changes.\n\n") - if (branch.isNotBlank()) append("Preferred branch: ").append(branch.trim()).append("\n\n") - append(task.trim()) - })) + append("?q=").append(encode(task.trim())) + append("&mode=").append(mode.value) if (repository.isNotBlank()) { - append("&repositories=").append(encode(repository.trim())) + append("&repo=").append(encode(repository.trim())) + if (branch.isNotBlank()) append("&branch=").append(encode(branch.trim())) } } } @@ -127,7 +125,7 @@ internal fun ClaudeCodeHandoffPanel(modifier: Modifier = Modifier) { OutlinedTextField( value = branch, onValueChange = { branch = it }, - label = { Text("Preferred branch (prompt hint)") }, + label = { Text("Branch (optional)") }, enabled = repository.isNotBlank(), singleLine = true, modifier = Modifier.fillMaxWidth().testTag("claude_code_branch"), @@ -147,10 +145,6 @@ internal fun ClaudeCodeHandoffPanel(modifier: Modifier = Modifier) { style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) - Text( - "Plan and branch are prompt hints. Confirm the actual mode and branch in Claude Code.", - style = MaterialTheme.typography.bodySmall, - ) Button( onClick = { confirmation = true }, enabled = ClaudeCodeHandoff.canOpen(task, repository, branch), diff --git a/app/src/test/java/ais/tee/ui/screens/ClaudeCodeHandoffTest.kt b/app/src/test/java/ais/tee/ui/screens/ClaudeCodeHandoffTest.kt index 32e72c686..a60cc31ed 100644 --- a/app/src/test/java/ais/tee/ui/screens/ClaudeCodeHandoffTest.kt +++ b/app/src/test/java/ais/tee/ui/screens/ClaudeCodeHandoffTest.kt @@ -15,9 +15,8 @@ class ClaudeCodeHandoffTest { branch = "feat/agent-lab", ) assertEquals( - "https://claude.ai/code?prompt=Plan%20the%20work%20before%20making%20changes.%0A%0A" + - "Preferred%20branch%3A%20feat%2Fagent-lab%0A%0AFix%20failing%20CI%20%26%20add%20tests" + - "&repositories=travnie%2Faistee", + "https://claude.ai/code/new?q=Fix%20failing%20CI%20%26%20add%20tests" + + "&mode=plan&repo=travnie%2Faistee&branch=feat%2Fagent-lab", link, ) assertFalse(link.contains("token")) @@ -27,7 +26,7 @@ class ClaudeCodeHandoffTest { @Test fun codeModeAndNoRepositoryRemainValid() { assertEquals( - "https://claude.ai/code?prompt=Review%20a%20patch", + "https://claude.ai/code/new?q=Review%20a%20patch&mode=code", ClaudeCodeHandoff.newSessionUrl(" Review a patch ", mode = ClaudeCodeHandoff.Mode.CODE), ) assertEquals("claude://code", ClaudeCodeHandoff.SESSIONS_URL)