-
-
Notifications
You must be signed in to change notification settings - Fork 3
feat: let the agent show and drive the guest browser #222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,45 @@ | ||||||||||||||||||||||||
| package com.yugahashimoto.andcode.feature.browser | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| import androidx.compose.runtime.Composable | ||||||||||||||||||||||||
| import androidx.compose.runtime.LaunchedEffect | ||||||||||||||||||||||||
| import kotlinx.coroutines.Dispatchers | ||||||||||||||||||||||||
| import kotlinx.coroutines.delay | ||||||||||||||||||||||||
| import kotlinx.coroutines.withContext | ||||||||||||||||||||||||
| import org.json.JSONObject | ||||||||||||||||||||||||
| import java.io.File | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| private const val COMMAND_FILE_RELATIVE_PATH = ".and-code/browser-command.json" | ||||||||||||||||||||||||
| private const val POLL_INTERVAL_MILLIS = 1000L | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||
| * Watches the active workspace for a browser command written by the in-guest agent | ||||||||||||||||||||||||
| * (`.and-code/browser-command.json`, e.g. `{"action":"open","url":"http://127.0.0.1:8080/"}`) | ||||||||||||||||||||||||
| * and opens the guest browser at the requested URL so the user can watch and join in. | ||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||
| @Composable | ||||||||||||||||||||||||
| fun GuestBrowserCommandWatcher( | ||||||||||||||||||||||||
| workspacePath: String?, | ||||||||||||||||||||||||
| onOpenUrl: (String) -> Unit, | ||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||
| LaunchedEffect(workspacePath) { | ||||||||||||||||||||||||
| val path = workspacePath ?: return@LaunchedEffect | ||||||||||||||||||||||||
| val commandFile = File(path, COMMAND_FILE_RELATIVE_PATH) | ||||||||||||||||||||||||
| while (true) { | ||||||||||||||||||||||||
| delay(POLL_INTERVAL_MILLIS) | ||||||||||||||||||||||||
| val url = withContext(Dispatchers.IO) { consumeOpenCommand(commandFile) } | ||||||||||||||||||||||||
| if (url != null) { | ||||||||||||||||||||||||
| onOpenUrl(url) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| private fun consumeOpenCommand(commandFile: File): String? = | ||||||||||||||||||||||||
| runCatching { | ||||||||||||||||||||||||
| if (!commandFile.exists()) { | ||||||||||||||||||||||||
| return null | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| val text = commandFile.readText() | ||||||||||||||||||||||||
| commandFile.delete() | ||||||||||||||||||||||||
| JSONObject(text).optString("url").takeIf { it.isNotBlank() } | ||||||||||||||||||||||||
|
Comment on lines
+42
to
+44
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [bug · medium] Suggestion:
Suggested change
|
||||||||||||||||||||||||
| }.getOrNull() | ||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -82,6 +82,10 @@ fun GuestBrowserScreen( | |
| AndroidView( | ||
| modifier = Modifier.weight(1f), | ||
| factory = { context -> | ||
| // Lets the in-guest agent drive this WebView over CDP (the devtools abstract | ||
| // socket is reachable from the guest, which shares the app's UID), so a page | ||
| // can be shown to the user and operated by human and agent at the same time. | ||
| WebView.setWebContentsDebuggingEnabled(true) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [security · medium]
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [maintainability · low] |
||
| WebView(context).apply { | ||
| layoutParams = | ||
| ViewGroup.LayoutParams( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -72,6 +72,7 @@ import com.yugahashimoto.andcode.feature.assistant.SpeechResult | |
| import com.yugahashimoto.andcode.feature.assistant.SpeechTranscriptAccumulator | ||
| import com.yugahashimoto.andcode.feature.assistant.VoiceDictationOutcome | ||
| import com.yugahashimoto.andcode.feature.assistant.VoiceDictationPolicy | ||
| import com.yugahashimoto.andcode.feature.browser.GuestBrowserCommandWatcher | ||
| import com.yugahashimoto.andcode.feature.chat.ChatHomeScreen | ||
| import com.yugahashimoto.andcode.feature.chat.ChatViewModel | ||
| import com.yugahashimoto.andcode.feature.chat.SubagentInfo | ||
|
|
@@ -105,6 +106,7 @@ import com.yugahashimoto.andcode.ui.navigation.SCHEDULE_DETAIL_ROUTE_PATTERN | |
| import com.yugahashimoto.andcode.ui.navigation.SCHEDULE_EDIT_ARG_ID | ||
| import com.yugahashimoto.andcode.ui.navigation.SCHEDULE_EDIT_ROUTE_PATTERN | ||
| import com.yugahashimoto.andcode.ui.navigation.decodeRouteArg | ||
| import com.yugahashimoto.andcode.ui.navigation.guestBrowserRoute | ||
| import com.yugahashimoto.andcode.ui.navigation.scheduleDetailRoute | ||
| import com.yugahashimoto.andcode.ui.navigation.scheduleEditRoute | ||
| import com.yugahashimoto.andcode.ui.navigation.settingsNavGraph | ||
|
|
@@ -563,6 +565,15 @@ fun AndCodeApp( | |
| } | ||
| } | ||
|
|
||
| // Lets the in-guest agent pop the guest browser open for the user by dropping a command | ||
| // file into the active workspace (see GuestBrowserCommandWatcher). | ||
| GuestBrowserCommandWatcher( | ||
| workspacePath = chatState.selectedWorkspacePath, | ||
| onOpenUrl = { url -> | ||
| navController.navigate(guestBrowserRoute(url)) { launchSingleTop = true } | ||
| }, | ||
| ) | ||
|
Comment on lines
+570
to
+575
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [performance · low] 機能上はエージェントがバックグラウンドで動作している前提なので常時監視自体は意図的と思われますが、ローカルランタイムが起動中の時だけ監視する、コマンドファイルを置くディレクトリの変更を |
||
|
|
||
| val onHandoff: (String) -> Unit = { targetRuntimeId -> | ||
| val prompt = buildHandoffPrompt(chatState.messages) | ||
| pendingHandoffPrompt = targetRuntimeId to prompt | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -185,9 +185,12 @@ fun NavGraphBuilder.workspaceNavGraph( | |||||||||||
| ) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| composable(ROUTE_GUEST_BROWSER) { | ||||||||||||
| composable(GUEST_BROWSER_ROUTE_PATTERN) { backStack -> | ||||||||||||
| val requestedUrl = backStack.arguments?.getString(GUEST_BROWSER_ARG_URL)?.let { decodeRouteArg(it) } | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [bug · medium] Suggestion:
Suggested change
|
||||||||||||
| GuestBrowserScreen( | ||||||||||||
| initialUrl = app.localRuntimeManager.installedPort()?.let { "http://127.0.0.1:$it/" }.orEmpty(), | ||||||||||||
| initialUrl = | ||||||||||||
| requestedUrl | ||||||||||||
| ?: app.localRuntimeManager.installedPort()?.let { "http://127.0.0.1:$it/" }.orEmpty(), | ||||||||||||
| onBack = { navController.popBackStack() }, | ||||||||||||
| ) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[maintainability · low]
runCatchingは inline 関数のため、このreturn nullは非ローカル return となりconsumeOpenCommand全体から抜けます(getOrNull()もスキップ)。動作はしますが意図が分かりにくく、将来このブロックを inline でない関数に抽出した際にコンパイルエラーになる等、壊れやすい構造です。return nullではなくreturn@runCatching nullを使うか、明示的な return を持つ関数本体に書き換えると読みやすくなります。