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
17 changes: 17 additions & 0 deletions app/src/main/java/dev/blazelight/p4oc/core/network/OpenCodeApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,30 @@ interface OpenCodeApi {
@Query("directory") directory: String?
): MessageWrapperDto

@GET("permission")
suspend fun listPermissions(
@Query("directory") directory: String?
): List<PermissionDto>

@POST("permission/{requestId}/reply")
suspend fun respondToPermission(
@Path("requestId") requestId: String,
@Body request: PermissionResponseRequest,
@Query("directory") directory: String?
): Boolean

@GET("api/session/{sessionId}/permission")
suspend fun listSessionPermissionsV2(
@Path("sessionId") sessionId: String
): PermissionV2RequestListResponseDto

@POST("api/session/{sessionId}/permission/{requestId}/reply")
suspend fun respondToPermissionV2(
@Path("sessionId") sessionId: String,
@Path("requestId") requestId: String,
@Body request: PermissionResponseRequest
): Response<Unit>

@POST("question/{requestId}/reply")
suspend fun respondToQuestion(
@Path("requestId") requestId: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,5 @@ internal class WorkspaceClientOfishAdapter(
)

override suspend fun respondToPermission(id: String, request: PermissionResponseRequest): Boolean =
workspaceClient.respondToPermission(id, request)
workspaceClient.respondToPermissionLegacy(id, request)
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,29 @@ data class PermissionToolDto(
@SerialName("callID") val callID: String
)

@Serializable
data class PermissionV2RequestListResponseDto(
val data: List<PermissionV2RequestDto>
)

@Serializable
data class PermissionV2RequestDto(
val id: String,
@SerialName("sessionID") val sessionID: String,
val action: String,
val resources: List<String>,
val save: List<String> = emptyList(),
val metadata: JsonObject? = null,
val source: PermissionV2SourceDto? = null
)

@Serializable
data class PermissionV2SourceDto(
val type: String,
@SerialName("messageID") val messageID: String,
@SerialName("callID") val callID: String
)

@Serializable
data class PermissionResponseRequest(
val reply: String,
Expand Down
47 changes: 34 additions & 13 deletions app/src/main/java/dev/blazelight/p4oc/data/remote/mapper/Mappers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,34 @@ object StatusMapper {
)
}

// ============================================================================
// Permission Mapper
// ============================================================================

object PermissionMapper {
fun mapToDomain(dto: PermissionDto): Permission = Permission(
id = dto.id,
type = dto.permission,
patterns = dto.patterns,
sessionID = dto.sessionID,
messageID = dto.tool?.messageID.orEmpty(),
callID = dto.tool?.callID,
metadata = dto.metadata,
always = dto.always,
)

fun mapV2ToDomain(dto: PermissionV2RequestDto): Permission = Permission(
id = dto.id,
type = dto.action,
patterns = dto.resources,
sessionID = dto.sessionID,
messageID = dto.source?.takeIf { it.type == "tool" }?.messageID.orEmpty(),
callID = dto.source?.takeIf { it.type == "tool" }?.callID,
metadata = dto.metadata ?: JsonObject(emptyMap()),
always = dto.save,
)
}

// ============================================================================
// Event Mapper
// ============================================================================
Expand Down Expand Up @@ -665,20 +693,13 @@ class EventMapper constructor(
}
"permission.asked" -> {
val permissionDto = json.decodeFromJsonElement<PermissionDto>(dto.properties)
OpenCodeEvent.PermissionRequested(
Permission(
id = permissionDto.id,
type = permissionDto.permission,
patterns = permissionDto.patterns,
sessionID = permissionDto.sessionID,
messageID = permissionDto.tool?.messageID ?: "",
callID = permissionDto.tool?.callID,
metadata = permissionDto.metadata,
always = permissionDto.always
)
)
OpenCodeEvent.PermissionRequested(PermissionMapper.mapToDomain(permissionDto))
}
"permission.v2.asked" -> {
val permissionDto = json.decodeFromJsonElement<PermissionV2RequestDto>(dto.properties)
OpenCodeEvent.PermissionRequested(PermissionMapper.mapV2ToDomain(permissionDto))
}
"permission.replied" -> {
"permission.replied", "permission.v2.replied" -> {
val props = json.decodeFromJsonElement<PermissionRepliedPropertiesDto>(dto.properties)
OpenCodeEvent.PermissionReplied(props.sessionID, props.requestID, props.reply)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import dev.blazelight.p4oc.data.remote.dto.QuestionRequestDto
import dev.blazelight.p4oc.data.remote.dto.SendMessageRequest
import dev.blazelight.p4oc.data.remote.dto.UpdateSessionRequest
import dev.blazelight.p4oc.data.remote.mapper.MessageMapper
import dev.blazelight.p4oc.data.remote.mapper.PermissionMapper
import dev.blazelight.p4oc.data.remote.mapper.SessionMapper
import dev.blazelight.p4oc.data.remote.mapper.mapQuestionRequestDtoToDomain
import dev.blazelight.p4oc.data.workspace.SessionWorkspaceClient
Expand Down Expand Up @@ -40,6 +41,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import java.util.concurrent.atomic.AtomicInteger
Expand Down Expand Up @@ -147,6 +149,7 @@ class SessionRepositoryImpl(
override fun acceptEvent(event: OpenCodeEvent) {
if (event is OpenCodeEvent.Connected) {
hydrateAfterReconnect()
scope.launch { reconcileObservedPendingPermissions() }
return
}

Expand Down Expand Up @@ -397,6 +400,7 @@ class SessionRepositoryImpl(
if (hasRunningQuestion) {
reconcilePendingQuestions()
}
reconcilePendingPermissions(sessionId.value)
}

override fun sendMessageAsync(sessionId: SessionId, request: SendMessageRequest): Deferred<Result<Unit>> = scope.async {
Expand Down Expand Up @@ -642,6 +646,34 @@ class SessionRepositoryImpl(
updateSession(ownerSessionId, transform)
}

private suspend fun reconcileObservedPendingPermissions() {
val sessionIds = synchronized(sessionUiStates) { sessionUiStates.keys.toList() }
sessionIds.forEach { sessionId -> reconcilePendingPermissions(sessionId) }
}

private suspend fun reconcilePendingPermissions(sessionId: String) {
val legacyPermissions = runCatching {
client.listPermissions()
.filter { permission -> permission.sessionID == sessionId }
.map(PermissionMapper::mapToDomain)
}.getOrNull()
val permissions = if (!legacyPermissions.isNullOrEmpty()) {
legacyPermissions
} else {
runCatching { client.listSessionPermissionsV2(sessionId).map(PermissionMapper::mapV2ToDomain) }
.getOrNull()
?: legacyPermissions
?: return
}
updateSession(sessionId) { state ->
val recovered = permissions.mapNotNull { permission ->
val callId = permission.callID ?: return@mapNotNull null
callId to permission
}.toMap()
state.copy(pendingPermissionsByCallId = recovered)
}
}

private fun mergeLoadedMessages(sessionId: String, loaded: List<MessageWithParts>) {
val state = messageState(sessionId)
state.update { current ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package dev.blazelight.p4oc.data.workspace

import dev.blazelight.p4oc.data.remote.dto.CreateSessionRequest
import dev.blazelight.p4oc.data.remote.dto.PermissionDto
import dev.blazelight.p4oc.data.remote.dto.PermissionV2RequestDto
import dev.blazelight.p4oc.data.remote.dto.ProjectDto
import dev.blazelight.p4oc.data.remote.dto.SendMessageRequest
import dev.blazelight.p4oc.data.remote.dto.SessionDto
Expand Down Expand Up @@ -40,5 +42,9 @@ interface SessionWorkspaceClient {

suspend fun sendMessageAsync(sessionId: String, request: SendMessageRequest)

suspend fun listSessionPermissionsV2(sessionId: String): List<PermissionV2RequestDto> = emptyList()

suspend fun listPermissions(): List<PermissionDto> = emptyList()

suspend fun abortSession(id: String): Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import dev.blazelight.p4oc.data.remote.dto.FileStatusDto
import dev.blazelight.p4oc.data.remote.dto.ForkSessionRequest
import dev.blazelight.p4oc.data.remote.dto.InitSessionRequest
import dev.blazelight.p4oc.data.remote.dto.MessageWrapperDto
import dev.blazelight.p4oc.data.remote.dto.PermissionDto
import dev.blazelight.p4oc.data.remote.dto.PermissionResponseRequest
import dev.blazelight.p4oc.data.remote.dto.PermissionV2RequestDto
import dev.blazelight.p4oc.data.remote.dto.ProjectDto
import dev.blazelight.p4oc.data.remote.dto.QuestionReplyRequest
import dev.blazelight.p4oc.data.remote.dto.QuestionRequestDto
Expand All @@ -27,6 +29,7 @@ import dev.blazelight.p4oc.data.remote.dto.VcsInfoDto
import dev.blazelight.p4oc.data.server.ActiveServerApiProvider
import dev.blazelight.p4oc.domain.server.ServerGeneration
import dev.blazelight.p4oc.domain.workspace.Workspace
import retrofit2.HttpException
import java.io.IOException

class WorkspaceClient(
Expand Down Expand Up @@ -101,7 +104,25 @@ class WorkspaceClient(
api.sendMessageAsync(sessionId, request, directory)
}

suspend fun respondToPermission(requestId: String, request: PermissionResponseRequest): Boolean =
override suspend fun listSessionPermissionsV2(sessionId: String): List<PermissionV2RequestDto> =
api.listSessionPermissionsV2(sessionId).data

override suspend fun listPermissions(): List<PermissionDto> = api.listPermissions(directory)

suspend fun respondToPermission(
sessionId: String,
requestId: String,
request: PermissionResponseRequest
): Boolean {
val response = api.respondToPermissionV2(sessionId, requestId, request)
val contentType = response.headers()["Content-Type"].orEmpty()
if (response.isSuccessful && !contentType.startsWith("text/html")) return true
if (response.code() != 404 && !contentType.startsWith("text/html")) throw HttpException(response)

return respondToPermissionLegacy(requestId, request)
}

suspend fun respondToPermissionLegacy(requestId: String, request: PermissionResponseRequest): Boolean =
api.respondToPermission(requestId, request, directory)

suspend fun respondToQuestion(requestId: String, request: QuestionReplyRequest): Boolean =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ private fun AssistantMessageContent(
ToolGroupWidget(
tools = group.tools,
defaultState = defaultToolWidgetState,
pendingPermissionCallIds = pendingPermissionsByCallId.keys,
pendingPermissionIdsByCallId = pendingPermissionsByCallId.mapValues { it.value.id },
onToolApprove = onToolApprove,
onToolDeny = onToolDeny,
onOpenSubSession = onOpenSubSession
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ fun BashWidgetExpanded(
tool: Part.Tool,
onClick: (() -> Unit)?,
showApprovalActions: Boolean,
approvalRequestId: String = tool.callID,
onToolApprove: (String) -> Unit,
onToolDeny: (String) -> Unit,
modifier: Modifier = Modifier
Expand Down Expand Up @@ -116,10 +117,10 @@ fun BashWidgetExpanded(
}

// Pending approval buttons
if (state is ToolState.Pending && showApprovalActions) {
if (showApprovalActions) {
PendingApprovalButtons(
onApprove = { onToolApprove(tool.callID) },
onDeny = { onToolDeny(tool.callID) }
onApprove = { onToolApprove(approvalRequestId) },
onDeny = { onToolDeny(approvalRequestId) }
)
}
}
Expand Down Expand Up @@ -342,6 +343,7 @@ fun DefaultWidgetExpanded(
tool: Part.Tool,
onClick: (() -> Unit)?,
showApprovalActions: Boolean,
approvalRequestId: String = tool.callID,
onToolApprove: (String) -> Unit,
onToolDeny: (String) -> Unit,
modifier: Modifier = Modifier
Expand Down Expand Up @@ -429,10 +431,10 @@ fun DefaultWidgetExpanded(
}

// Pending approval buttons
if (state is ToolState.Pending && showApprovalActions) {
if (showApprovalActions) {
PendingApprovalButtons(
onApprove = { onToolApprove(tool.callID) },
onDeny = { onToolDeny(tool.callID) }
onApprove = { onToolApprove(approvalRequestId) },
onDeny = { onToolDeny(approvalRequestId) }
)
}
}
Expand All @@ -447,6 +449,7 @@ fun TaskWidgetExpanded(
tool: Part.Tool,
onClick: (() -> Unit)?,
showApprovalActions: Boolean,
approvalRequestId: String = tool.callID,
onToolApprove: (String) -> Unit,
onToolDeny: (String) -> Unit,
onOpenSubSession: ((String) -> Unit)?,
Expand Down Expand Up @@ -565,10 +568,10 @@ fun TaskWidgetExpanded(
}

// Pending approval buttons
if (state is ToolState.Pending && showApprovalActions) {
if (showApprovalActions) {
PendingApprovalButtons(
onApprove = { onToolApprove(tool.callID) },
onDeny = { onToolDeny(tool.callID) }
onApprove = { onToolApprove(approvalRequestId) },
onDeny = { onToolDeny(approvalRequestId) }
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ fun ToolCallExpanded(
tool: Part.Tool,
onClick: (() -> Unit)?,
showApprovalActions: Boolean = true,
approvalRequestId: String = tool.callID,
onToolApprove: (String) -> Unit,
onToolDeny: (String) -> Unit,
onOpenSubSession: ((String) -> Unit)? = null,
Expand All @@ -216,6 +217,7 @@ fun ToolCallExpanded(
tool = tool,
onClick = onClick,
showApprovalActions = showApprovalActions,
approvalRequestId = approvalRequestId,
onToolApprove = onToolApprove,
onToolDeny = onToolDeny,
modifier = modifier
Expand All @@ -234,6 +236,7 @@ fun ToolCallExpanded(
tool = tool,
onClick = onClick,
showApprovalActions = showApprovalActions,
approvalRequestId = approvalRequestId,
onToolApprove = onToolApprove,
onToolDeny = onToolDeny,
onOpenSubSession = onOpenSubSession,
Expand All @@ -243,6 +246,7 @@ fun ToolCallExpanded(
tool = tool,
onClick = onClick,
showApprovalActions = showApprovalActions,
approvalRequestId = approvalRequestId,
onToolApprove = onToolApprove,
onToolDeny = onToolDeny,
modifier = modifier
Expand Down
Loading
Loading