mirror of
https://github.com/openai/codex.git
synced 2026-04-24 06:35:50 +00:00
Adopt Android delegated session notifications
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -107,6 +107,15 @@ stub SDK docs and the local refactor doc:
|
||||
detail UI must cancel the parent and all child Genie sessions through the
|
||||
framework `cancelSession(...)` path, even when some of those sessions are
|
||||
already terminal.
|
||||
- Framework-owned session notifications now support delegated AGENT rendering:
|
||||
- user-facing question/result/error notifications should be rendered by the
|
||||
Agent app when the framework calls `onShowOrUpdateSessionNotification(...)`
|
||||
and cancelled when it calls `onCancelSessionNotification(...)`
|
||||
- the Agent must ACK a posted notification with `ackSessionNotification(...)`
|
||||
and route inline replies through `answerQuestionFromNotification(...)`
|
||||
- if delegated rendering is unavailable, the framework may post a generic
|
||||
fallback notification, so app-side notification code must remain
|
||||
token-aware and idempotent
|
||||
|
||||
## External reference implementations
|
||||
|
||||
|
||||
@@ -77,5 +77,9 @@
|
||||
<action android:name="com.openai.codex.agent.action.BOOTSTRAP_DESKTOP_BRIDGE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".AgentNotificationReplyReceiver"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.openai.codex.agent
|
||||
|
||||
import android.app.RemoteInput
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class AgentNotificationReplyReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action != AgentQuestionNotifier.ACTION_REPLY_FROM_NOTIFICATION) {
|
||||
return
|
||||
}
|
||||
val sessionId = intent.getStringExtra(AgentQuestionNotifier.EXTRA_SESSION_ID)?.trim().orEmpty()
|
||||
val notificationToken = intent.getStringExtra(
|
||||
AgentQuestionNotifier.EXTRA_NOTIFICATION_TOKEN,
|
||||
)?.trim().orEmpty()
|
||||
val answer = RemoteInput.getResultsFromIntent(intent)
|
||||
?.getCharSequence(AgentQuestionNotifier.REMOTE_INPUT_KEY)
|
||||
?.toString()
|
||||
?.trim()
|
||||
.orEmpty()
|
||||
if (sessionId.isEmpty() || answer.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val pendingResult = goAsync()
|
||||
thread(name = "CodexAgentNotificationReply-$sessionId") {
|
||||
try {
|
||||
runCatching {
|
||||
AgentSessionController(context).answerQuestionFromNotification(
|
||||
sessionId = sessionId,
|
||||
notificationToken = notificationToken,
|
||||
answer = answer,
|
||||
parentSessionId = null,
|
||||
)
|
||||
AgentQuestionNotifier.cancel(context, sessionId)
|
||||
}.onFailure { err ->
|
||||
Log.w(TAG, "Failed to answer notification question for $sessionId", err)
|
||||
}
|
||||
} finally {
|
||||
pendingResult.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val TAG = "CodexAgentReply"
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,27 @@ import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.RemoteInput
|
||||
import android.app.agent.AgentSessionInfo
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
|
||||
object AgentQuestionNotifier {
|
||||
const val ACTION_REPLY_FROM_NOTIFICATION =
|
||||
"com.openai.codex.agent.action.REPLY_FROM_NOTIFICATION"
|
||||
const val EXTRA_SESSION_ID = "sessionId"
|
||||
const val EXTRA_NOTIFICATION_TOKEN = "notificationToken"
|
||||
const val REMOTE_INPUT_KEY = "codexAgentNotificationReply"
|
||||
|
||||
private const val CHANNEL_ID = "codex_agent_questions"
|
||||
private const val CHANNEL_NAME = "Codex Agent Questions"
|
||||
private const val MAX_CONTENT_PREVIEW_CHARS = 400
|
||||
private val notificationStateLock = Any()
|
||||
private val activeNotificationTokens = mutableMapOf<String, String>()
|
||||
private val retiredNotificationTokens = mutableMapOf<String, MutableSet<String>>()
|
||||
|
||||
fun showQuestion(
|
||||
context: Context,
|
||||
@@ -23,7 +37,47 @@ object AgentQuestionNotifier {
|
||||
manager.notify(notificationId(sessionId), buildNotification(context, sessionId, targetPackage, question))
|
||||
}
|
||||
|
||||
fun showOrUpdateDelegatedNotification(
|
||||
context: Context,
|
||||
session: AgentSessionInfo,
|
||||
notificationToken: String,
|
||||
notificationText: String,
|
||||
): Boolean {
|
||||
if (notificationText.isBlank() || !activateNotificationToken(session.sessionId, notificationToken)) {
|
||||
return false
|
||||
}
|
||||
val manager = context.getSystemService(NotificationManager::class.java) ?: return false
|
||||
ensureChannel(manager)
|
||||
manager.notify(
|
||||
notificationId(session.sessionId),
|
||||
buildDelegatedNotification(
|
||||
context = context,
|
||||
session = session,
|
||||
notificationToken = notificationToken,
|
||||
notificationText = notificationText.trim(),
|
||||
),
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
fun cancel(context: Context, sessionId: String) {
|
||||
retireActiveNotificationToken(sessionId)
|
||||
val manager = context.getSystemService(NotificationManager::class.java) ?: return
|
||||
manager.cancel(notificationId(sessionId))
|
||||
}
|
||||
|
||||
fun clearSessionState(sessionId: String) {
|
||||
clearNotificationToken(sessionId)
|
||||
}
|
||||
|
||||
fun cancel(
|
||||
context: Context,
|
||||
sessionId: String,
|
||||
notificationToken: String,
|
||||
) {
|
||||
if (!retireNotificationToken(sessionId, notificationToken)) {
|
||||
return
|
||||
}
|
||||
val manager = context.getSystemService(NotificationManager::class.java) ?: return
|
||||
manager.cancel(notificationId(sessionId))
|
||||
}
|
||||
@@ -55,6 +109,95 @@ object AgentQuestionNotifier {
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun buildDelegatedNotification(
|
||||
context: Context,
|
||||
session: AgentSessionInfo,
|
||||
notificationToken: String,
|
||||
notificationText: String,
|
||||
): Notification {
|
||||
val targetIdentity = resolveTargetIdentity(context, session.targetPackage)
|
||||
val contentIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
notificationId(session.sessionId),
|
||||
Intent(context, SessionDetailActivity::class.java).apply {
|
||||
putExtra(SessionDetailActivity.EXTRA_SESSION_ID, session.sessionId)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
val contentText = notificationText.take(MAX_CONTENT_PREVIEW_CHARS)
|
||||
val builder = Notification.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(targetIdentity.icon)
|
||||
.setLargeIcon(targetIdentity.icon)
|
||||
.setContentTitle(buildNotificationTitle(session.state, targetIdentity.displayName))
|
||||
.setContentText(contentText)
|
||||
.setStyle(Notification.BigTextStyle().bigText(contentText))
|
||||
.setContentIntent(contentIntent)
|
||||
.setAutoCancel(false)
|
||||
.setOngoing(true)
|
||||
buildInlineReplyAction(
|
||||
context = context,
|
||||
session = session,
|
||||
notificationToken = notificationToken,
|
||||
)?.let { replyAction ->
|
||||
builder.addAction(replyAction)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
private fun buildNotificationTitle(
|
||||
state: Int,
|
||||
targetDisplayName: String,
|
||||
): String {
|
||||
return when (state) {
|
||||
AgentSessionInfo.STATE_WAITING_FOR_USER ->
|
||||
"Codex needs input for $targetDisplayName"
|
||||
AgentSessionInfo.STATE_RUNNING ->
|
||||
"Codex is working in $targetDisplayName"
|
||||
AgentSessionInfo.STATE_COMPLETED ->
|
||||
"Codex finished $targetDisplayName"
|
||||
AgentSessionInfo.STATE_FAILED ->
|
||||
"Codex hit an issue in $targetDisplayName"
|
||||
AgentSessionInfo.STATE_CANCELLED ->
|
||||
"Codex cancelled $targetDisplayName"
|
||||
AgentSessionInfo.STATE_CREATED,
|
||||
AgentSessionInfo.STATE_QUEUED,
|
||||
-> "Codex session for $targetDisplayName"
|
||||
else -> "Codex session for $targetDisplayName"
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildInlineReplyAction(
|
||||
context: Context,
|
||||
session: AgentSessionInfo,
|
||||
notificationToken: String,
|
||||
): Notification.Action? {
|
||||
if (session.state != AgentSessionInfo.STATE_WAITING_FOR_USER || notificationToken.isBlank()) {
|
||||
return null
|
||||
}
|
||||
val replyIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
notificationId(session.sessionId),
|
||||
Intent(context, AgentNotificationReplyReceiver::class.java).apply {
|
||||
action = ACTION_REPLY_FROM_NOTIFICATION
|
||||
putExtra(EXTRA_SESSION_ID, session.sessionId)
|
||||
putExtra(EXTRA_NOTIFICATION_TOKEN, notificationToken)
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE,
|
||||
)
|
||||
val remoteInput = RemoteInput.Builder(REMOTE_INPUT_KEY)
|
||||
.setLabel("Reply")
|
||||
.build()
|
||||
return Notification.Action.Builder(
|
||||
Icon.createWithResource(context, android.R.drawable.ic_menu_send),
|
||||
"Reply",
|
||||
replyIntent,
|
||||
)
|
||||
.addRemoteInput(remoteInput)
|
||||
.setAllowGeneratedReplies(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun ensureChannel(manager: NotificationManager) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return
|
||||
@@ -73,7 +216,102 @@ object AgentQuestionNotifier {
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
private fun activateNotificationToken(
|
||||
sessionId: String,
|
||||
notificationToken: String,
|
||||
): Boolean {
|
||||
if (notificationToken.isBlank()) {
|
||||
return false
|
||||
}
|
||||
synchronized(notificationStateLock) {
|
||||
if (retiredNotificationTokens[sessionId]?.contains(notificationToken) == true) {
|
||||
return false
|
||||
}
|
||||
activeNotificationTokens.put(sessionId, notificationToken)?.let { previousToken ->
|
||||
if (previousToken != notificationToken) {
|
||||
retiredNotificationTokens.getOrPut(sessionId, ::mutableSetOf)
|
||||
.add(previousToken)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearNotificationToken(sessionId: String) {
|
||||
synchronized(notificationStateLock) {
|
||||
activeNotificationTokens.remove(sessionId)
|
||||
retiredNotificationTokens.remove(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun retireNotificationToken(
|
||||
sessionId: String,
|
||||
notificationToken: String,
|
||||
): Boolean {
|
||||
if (notificationToken.isBlank()) {
|
||||
retireActiveNotificationToken(sessionId)
|
||||
return true
|
||||
}
|
||||
synchronized(notificationStateLock) {
|
||||
retiredNotificationTokens.getOrPut(sessionId, ::mutableSetOf)
|
||||
.add(notificationToken)
|
||||
if (activeNotificationTokens[sessionId] != notificationToken) {
|
||||
return false
|
||||
}
|
||||
activeNotificationTokens.remove(sessionId)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private fun retireActiveNotificationToken(sessionId: String) {
|
||||
synchronized(notificationStateLock) {
|
||||
activeNotificationTokens.remove(sessionId)?.let { notificationToken ->
|
||||
retiredNotificationTokens.getOrPut(sessionId, ::mutableSetOf)
|
||||
.add(notificationToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveTargetIdentity(
|
||||
context: Context,
|
||||
targetPackage: String?,
|
||||
): TargetIdentity {
|
||||
if (targetPackage.isNullOrBlank()) {
|
||||
return TargetIdentity(
|
||||
displayName = "Codex Agent",
|
||||
icon = Icon.createWithResource(context, android.R.drawable.ic_dialog_info),
|
||||
)
|
||||
}
|
||||
val packageManager = context.packageManager
|
||||
return runCatching {
|
||||
val appInfo = packageManager.getApplicationInfo(
|
||||
targetPackage,
|
||||
PackageManager.ApplicationInfoFlags.of(0),
|
||||
)
|
||||
val iconResId = appInfo.icon.takeIf { it != 0 }
|
||||
TargetIdentity(
|
||||
displayName = packageManager.getApplicationLabel(appInfo).toString()
|
||||
.ifBlank { targetPackage },
|
||||
icon = if (iconResId == null) {
|
||||
Icon.createWithResource(context, android.R.drawable.ic_dialog_info)
|
||||
} else {
|
||||
Icon.createWithResource(targetPackage, iconResId)
|
||||
},
|
||||
)
|
||||
}.getOrDefault(
|
||||
TargetIdentity(
|
||||
displayName = targetPackage,
|
||||
icon = Icon.createWithResource(context, android.R.drawable.ic_dialog_info),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun notificationId(sessionId: String): Int {
|
||||
return sessionId.hashCode()
|
||||
}
|
||||
|
||||
private data class TargetIdentity(
|
||||
val displayName: String,
|
||||
val icon: Icon,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -548,6 +548,26 @@ class AgentSessionController(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun ackSessionNotification(
|
||||
sessionId: String,
|
||||
notificationToken: String,
|
||||
) {
|
||||
requireAgentManager().ackSessionNotification(sessionId, notificationToken)
|
||||
}
|
||||
|
||||
fun answerQuestionFromNotification(
|
||||
sessionId: String,
|
||||
notificationToken: String,
|
||||
answer: String,
|
||||
parentSessionId: String?,
|
||||
) {
|
||||
val manager = requireAgentManager()
|
||||
manager.answerQuestionFromNotification(sessionId, notificationToken, answer)
|
||||
if (parentSessionId != null) {
|
||||
manager.publishTrace(parentSessionId, "Answered question for $sessionId: $answer")
|
||||
}
|
||||
}
|
||||
|
||||
fun isSessionWaitingForUser(sessionId: String): Boolean {
|
||||
val manager = agentManager ?: return false
|
||||
return manager.getSessions(currentUserId()).any { session ->
|
||||
|
||||
@@ -44,7 +44,6 @@ class CodexAgentService : AgentService() {
|
||||
AgentPlannerRuntimeManager.closeSession(session.sessionId)
|
||||
}
|
||||
if (session.state != AgentSessionInfo.STATE_WAITING_FOR_USER) {
|
||||
AgentQuestionNotifier.cancel(this, session.sessionId)
|
||||
return
|
||||
}
|
||||
if (!pendingQuestionLoads.add(session.sessionId)) {
|
||||
@@ -65,12 +64,37 @@ class CodexAgentService : AgentService() {
|
||||
AgentPlannerRuntimeManager.closeSession(sessionId)
|
||||
DesktopInspectionRegistry.removeSession(sessionId)
|
||||
AgentQuestionNotifier.cancel(this, sessionId)
|
||||
AgentQuestionNotifier.clearSessionState(sessionId)
|
||||
presentationPolicyStore.removePolicy(sessionId)
|
||||
handledGenieQuestions.removeIf { it.startsWith("$sessionId:") }
|
||||
handledBridgeRequests.removeIf { it.startsWith("$sessionId:") }
|
||||
pendingGenieQuestions.removeIf { it.startsWith("$sessionId:") }
|
||||
}
|
||||
|
||||
override fun onShowOrUpdateSessionNotification(
|
||||
session: AgentSessionInfo,
|
||||
notificationToken: String,
|
||||
notificationText: String,
|
||||
) {
|
||||
showOrUpdateSessionNotification(
|
||||
session = session,
|
||||
notificationToken = notificationToken,
|
||||
notificationText = notificationText,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCancelSessionNotification(
|
||||
sessionId: String,
|
||||
notificationToken: String,
|
||||
reason: Int,
|
||||
) {
|
||||
cancelSessionNotification(
|
||||
sessionId = sessionId,
|
||||
notificationToken = notificationToken,
|
||||
reason = reason,
|
||||
)
|
||||
}
|
||||
|
||||
private fun maybeRollUpParentSession(session: AgentSessionInfo) {
|
||||
val parentSessionId = when {
|
||||
!session.parentSessionId.isNullOrBlank() -> session.parentSessionId
|
||||
@@ -206,7 +230,7 @@ class CodexAgentService : AgentService() {
|
||||
val events = manager.getSessionEvents(session.sessionId)
|
||||
val question = findLatestQuestion(events) ?: return
|
||||
if (!isBridgeQuestion(question)) {
|
||||
AgentQuestionNotifier.cancel(this, session.sessionId)
|
||||
return
|
||||
}
|
||||
maybeAutoAnswerGenieQuestion(session, question)
|
||||
}
|
||||
@@ -236,6 +260,67 @@ class CodexAgentService : AgentService() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun showOrUpdateSessionNotification(
|
||||
session: AgentSessionInfo,
|
||||
notificationToken: String,
|
||||
notificationText: String,
|
||||
) {
|
||||
thread(name = "CodexAgentNotificationShow-${session.sessionId}") {
|
||||
val posted = runCatching {
|
||||
AgentQuestionNotifier.showOrUpdateDelegatedNotification(
|
||||
context = this,
|
||||
session = session,
|
||||
notificationToken = notificationToken,
|
||||
notificationText = notificationText,
|
||||
)
|
||||
}.onFailure { err ->
|
||||
Log.w(
|
||||
TAG,
|
||||
"Failed to post delegated notification sessionId=${session.sessionId} token=$notificationToken",
|
||||
err,
|
||||
)
|
||||
}.getOrDefault(false)
|
||||
if (!posted) {
|
||||
return@thread
|
||||
}
|
||||
runCatching {
|
||||
sessionController.ackSessionNotification(session.sessionId, notificationToken)
|
||||
}.onFailure { err ->
|
||||
Log.w(
|
||||
TAG,
|
||||
"Failed to ack delegated notification sessionId=${session.sessionId} token=$notificationToken",
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelSessionNotification(
|
||||
sessionId: String,
|
||||
notificationToken: String,
|
||||
reason: Int,
|
||||
) {
|
||||
thread(name = "CodexAgentNotificationCancel-$sessionId") {
|
||||
Log.i(
|
||||
TAG,
|
||||
"Cancelling delegated notification sessionId=$sessionId token=$notificationToken reason=${notificationCancelReasonToString(reason)}",
|
||||
)
|
||||
AgentQuestionNotifier.cancel(
|
||||
context = this,
|
||||
sessionId = sessionId,
|
||||
notificationToken = notificationToken,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notificationCancelReasonToString(reason: Int): String {
|
||||
return when (reason) {
|
||||
NOTIFICATION_CANCEL_REASON_SUPPRESSED -> "SUPPRESSED"
|
||||
NOTIFICATION_CANCEL_REASON_REMOVED -> "REMOVED"
|
||||
else -> reason.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun findLatestQuestion(events: List<AgentSessionEvent>): String? {
|
||||
return events.lastOrNull { event ->
|
||||
event.type == AgentSessionEvent.TYPE_QUESTION &&
|
||||
|
||||
@@ -3,15 +3,13 @@ package com.openai.codex.agent
|
||||
import android.content.Context
|
||||
|
||||
object SessionNotificationCoordinator {
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun acknowledgeSessionTree(
|
||||
context: Context,
|
||||
sessionController: AgentSessionController,
|
||||
topLevelSessionId: String,
|
||||
sessionIds: Collection<String>,
|
||||
) {
|
||||
sessionIds.forEach { sessionId ->
|
||||
AgentQuestionNotifier.cancel(context, sessionId)
|
||||
}
|
||||
sessionController.acknowledgeSessionUi(topLevelSessionId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,13 @@ The current repo now contains these implementation slices:
|
||||
still-live framework session.
|
||||
- The Agent now auto-answers only internal bridge questions. User-facing Genie
|
||||
questions remain user-facing and are surfaced through the normal framework
|
||||
question flow, notifications, and desktop/UI answer surfaces.
|
||||
question flow, desktop/UI answer surfaces, and framework-delegated
|
||||
AGENT-rendered notifications with target-app icon branding and inline reply.
|
||||
- Framework-owned notification lifecycle now delegates question/result/error
|
||||
notification rendering to the Agent app when available, while retaining a
|
||||
framework fallback path if the Agent renderer is unavailable or does not ACK
|
||||
in time. Inline reply actions route through
|
||||
`answerQuestionFromNotification(sessionId, notificationToken, response)`.
|
||||
- The Agent now records an explicit per-child final presentation policy
|
||||
(`ATTACHED`, `DETACHED_HIDDEN`, `DETACHED_SHOWN`, or `AGENT_CHOICE`) and
|
||||
uses the framework-authoritative `AgentSessionInfo.getTargetPresentation()`
|
||||
@@ -222,8 +228,10 @@ the Android Agent/Genie flow.
|
||||
- detached target ensure-hidden/show/hide/attach/close
|
||||
- typed detached frame capture with runtime-aware recovery
|
||||
- `request_user_input` bridged from hosted Codex back into AgentSDK questions
|
||||
- Framework-owned question notifications for Genie questions that need user
|
||||
input; the Agent app suppresses its former duplicate notification mirror
|
||||
- Framework-owned notification lifecycle with delegated Agent rendering for
|
||||
question/result/error notifications; the Agent app posts the visible
|
||||
notification, ACKs the tokenized callback, and falls back to plain
|
||||
framework-owned rendering if the new callback surface is unavailable
|
||||
- Detached-mode Genie sessions now request `showDetachedTarget` before entering
|
||||
`WAITING_FOR_USER`, so HOME can keep a visible live icon while the user
|
||||
answers the question
|
||||
@@ -249,7 +257,8 @@ the Android Agent/Genie flow.
|
||||
- `android/genie`
|
||||
- standalone Genie scaffold APK with hosted `codex app-server`
|
||||
- `android/app/src/main/java/com/openai/codex/agent/CodexAgentService.kt`
|
||||
- framework `AgentService`
|
||||
- framework `AgentService`, parent roll-up, bridge auto-answering, and
|
||||
delegated notification callbacks
|
||||
- `android/app/src/main/java/com/openai/codex/agent/AgentSessionController.kt`
|
||||
- Agent-side `AgentManager` orchestration helper
|
||||
- `android/app/src/main/java/com/openai/codex/agent/AgentFrameworkToolBridge.kt`
|
||||
@@ -258,6 +267,11 @@ the Android Agent/Genie flow.
|
||||
- Agent session UI, Agent clarification dialogs, and Agent-native auth controls
|
||||
- `android/app/src/main/java/com/openai/codex/agent/SessionDetailActivity.kt`
|
||||
- HOME/AGENT session inspection and question answering UI
|
||||
- `android/app/src/main/java/com/openai/codex/agent/AgentQuestionNotifier.kt`
|
||||
- token-aware Agent-side renderer for delegated framework notifications,
|
||||
including target-app branding and inline reply actions
|
||||
- `android/app/src/main/java/com/openai/codex/agent/AgentNotificationReplyReceiver.kt`
|
||||
- inline reply receiver for Agent-rendered question notifications
|
||||
- `android/app/src/main/java/com/openai/codex/agent/AgentUserInputPrompter.kt`
|
||||
- Android dialog bridge for hosted Agent `request_user_input` calls
|
||||
- `android/genie/src/main/java/com/openai/codex/genie/CodexGenieService.kt`
|
||||
|
||||
Reference in New Issue
Block a user