M7: polish + E2E + docs (layout pass, theming, states, e2e driver, schema, setup.md, security)

This commit is contained in:
ARIA committed 2026-08-20 12:00:13 +02:00
1 parent 0cc8b7aafe
commit bf6bf7e8bd
26 files changed
+2225 -327

No files matched your search

@@ -3,6 +3,8 @@ package iris.state
import iris.data.ChatStore
import iris.data.ChannelStore
import iris.data.MediaItem
import iris.data.MessageItem
import iris.data.MsgStatus
import iris.data.SecureStore
import iris.media.MediaCache
import iris.media.kindFromMime
@@ -16,10 +18,13 @@ import iris.protocol.MediaOfferPayload
import iris.protocol.MessagePayload
import iris.protocol.MessageStopPayload
import iris.protocol.NotificationPayload
import iris.protocol.ReadReceiptPayload
import iris.protocol.ROLE_ASSISTANT
import iris.protocol.SearchHit
import iris.protocol.SearchResultsPayload
import iris.protocol.StatusPayload
import iris.protocol.SyncDonePayload
import iris.protocol.TYPE_ERROR
import iris.protocol.TYPE_NOTIFICATION
import iris.protocol.TYPE_CHANNEL_CREATED
import iris.protocol.TYPE_CHANNEL_DELETED
@@ -31,7 +36,9 @@ import iris.protocol.TYPE_MESSAGE
import iris.protocol.TYPE_MESSAGE_START
import iris.protocol.TYPE_MESSAGE_STOP
import iris.protocol.TYPE_MESSAGE_UPDATE
import iris.protocol.TYPE_READ_RECEIPT
import iris.protocol.TYPE_SEARCH_RESULTS
import iris.protocol.TYPE_STATUS
import iris.protocol.TYPE_SYNC_DONE
import iris.protocol.TYPE_TOOL_END
import iris.protocol.TYPE_TOOL_PROGRESS
@@ -89,6 +96,10 @@ class IrisController(
private val _homeChannel = MutableStateFlow("android:default")
val homeChannel: StateFlow<String> = _homeChannel.asStateFlow()
/** Gateway health state (M5: status frame; null = never received). */
private val _gatewayStatus = MutableStateFlow<String?>(null)
val gatewayStatus: StateFlow<String?> = _gatewayStatus.asStateFlow()
// ── M3: threads toggle (per-app for now; per-channel lands later) ─────
private val _threadsEnabled = MutableStateFlow(false)
val threadsEnabled: StateFlow<Boolean> = _threadsEnabled.asStateFlow()
@@ -259,6 +270,18 @@ class IrisController(
TYPE_TYPING -> {
frame.payloadAs<TypingPayload>()?.let { _typing.value = it.on }
}
TYPE_READ_RECEIPT -> {
frame.payloadAs<ReadReceiptPayload>()?.let { chat.markRead(it.messageId) }
}
TYPE_STATUS -> {
frame.payloadAs<StatusPayload>()?.let { _gatewayStatus.value = it.state }
}
TYPE_ERROR -> {
// M7: error frames are global, not per-message — fail
// any optimistic sends still in flight so they don't
// sit at "sending…" forever.
chat.failPending()
}
else -> Unit
}
}
@@ -375,6 +398,16 @@ class IrisController(
_attachments.value = emptyList()
}
/** M7: resend a failed user message (tap on the failed bubble). */
fun retrySend(messageId: String) {
val lane = chat.currentLane.value
val (chatId, threadId) = chat.parseLane(lane)
val item = chat.lanes.value[lane]?.firstOrNull { it.id == messageId } as? MessageItem ?: return
if (item.status != MsgStatus.Failed) return
chat.rearmForRetry(lane, messageId)
client.sendMessage(chatId, item.text, threadId, item.media.map { it.mediaId })
}
// ── M4: attachments (pick -> upload -> send) ──────────────────────────
/** Stage a picked file: upload it, then keep it as a pending attachment. */