* fix(#724): inline host-path MEDIA: images on mobile (client-side) The previous approach re-fetched the REST session-messages endpoint, but that endpoint returns the raw MEDIA:<path> text (api_server._message_response does not inline, and the transcript stores raw content). The desktop app instead reads the file locally via readFileDataUrl because it is co-located with the host. Mirror that on mobile: on assistant message.complete/done, inline any MEDIA:<path> image into a base64 data:image/... markdown tag the renderer can show. Mobile-only, backend untouched. Unreachable tags are stripped (graceful degrade). Pure inlining logic extracted to MediaInline for unit testing. * fix: correct RegexOption name (IGNORE_CASE) in MediaInline * test: fix byte literal types in MediaInlineTest * fix(#724): rewrite host-path MEDIA: to gateway /api/files/download URL Pivot from the earlier base64-inline approach to the desktop-proven, remote-phone-working fix: rewrite MEDIA:<path> directives (delivered raw over the WS stream) into authenticated  markdown tags, mirroring desktop media.ts mediaExternalUrl. The mobile renderer (MarkdownText) shows http(s) ![]() via Coil, so agent-sent images now render on mobile too — including on a remote phone (real HTTP, unlike a host-local file read). Mobile-only, backend untouched. Pure rewrite logic in MediaUrlRewriter (replaces MediaInline) with unit tests. * feat(#724): full gateway-file support — attach MEDIA: files (not just images) Pivot the MEDIA: fix from image-only URL rewriting to real attachments: - Attachment: add gatewayUrl + AttachmentSource (LOCAL/GATEWAY) so gateway-hosted files are first-class and render via existing InlineAttachment. - HostMediaExtractor (replaces MediaUrlRewriter): parse ANY absolute MEDIA: directive (any extension) and strip them from message text. - ChatViewModel.attachHostMedia: build the authed /api/files/download URL via GatewayFileClient, classify via MediaTypes, attach as Attachment (images inline, every other file tappable). Idempotent. - InlineAttachment: tappable. Gateway files fetch via GatewayFileClient and share via a chooser Intent (FileProvider); images load the URL in Coil. - Hence agent-delivered images, audio, video, CSV, PDF and arbitrary files all work on mobile — including a remote phone. Backend untouched. Builds on #726 (GatewayFileClient). Rebased onto main after #726 merged. * fix(#724): resolve imports, bubble callback scope, and quoted media path regex * feat(chat): open attachment in-place on tap (#724) Tap opens LOCAL (content://) and GATEWAY (fetch->FileProvider) files via ACTION_VIEW; failures surface as a non-blocking snackbar via openError. * fix: remove redundant out-of-order io.mockk.* wildcard import (ktlint CI) ktlint import-ordering requires lexicographic order; the trailing wildcard 'import io.mockk.*' sorts before 'io.mockk.coEvery' and was redundant — every mockk symbol is already explicitly imported, and the single mockkConstructor call uses a fully-qualified io.mockk. prefix (needs no import). Fixes ktlint failure on PR #725. * fix(ci): mock android.jar Intent stub so openAttachment GATEWAY test passes android.jar unit-test stubs Intent.setDataAndType/addFlags to throw '... not mocked'. openWithView builds an Intent and calls them, so openBytes' runCatching swallowed the exception and startActivity never fired, failing the test. Mock the Intent constructor + setters and capture the constructed instance to assert the ACTION_VIEW contract. * fix(#724): resolve host media download URLs and deduplicate REST synced chat bubbles * fix(#724): add FLAG_ACTIVITY_NEW_TASK for file intents and update GatewayFileClientTest --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
6aee02c4da
commit
a1155e8015
14 files changed
+632
-35
No files matched your search
@@ -1,28 +1,40 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents a file attached to a chat message.
|
||||
*
|
||||
* @param uri Content URI or file path to the attachment
|
||||
* @param uri Content URI or file path to the attachment. For gateway-sourced
|
||||
* attachments this is the authenticated `/api/files/download?...` URL, which
|
||||
* Coil can load directly for images.
|
||||
* @param name Display name of the file
|
||||
* @param mimeType MIME type (e.g., "image/jpeg", "application/pdf")
|
||||
* @param size File size in bytes
|
||||
* @param localPath Optional local file system path for direct access
|
||||
* @param size File size in bytes (0 when unknown, e.g. gateway-hosted files)
|
||||
* @param gatewayUrl When non-null, the authenticated gateway download URL for a
|
||||
* file that lives on the *gateway host* (agent-delivered `MEDIA:` directive).
|
||||
* Null for locally-picked attachments.
|
||||
* @param source Where the attachment originated.
|
||||
*/
|
||||
@Serializable
|
||||
data class Attachment(
|
||||
val uri: String,
|
||||
val name: String,
|
||||
val mimeType: String,
|
||||
val size: Long,
|
||||
val size: Long = 0,
|
||||
val gatewayUrl: String? = null,
|
||||
val source: AttachmentSource = AttachmentSource.LOCAL,
|
||||
) {
|
||||
val isImage: Boolean
|
||||
get() = mimeType.startsWith("image/")
|
||||
|
||||
val isGateway: Boolean
|
||||
get() = source == AttachmentSource.GATEWAY
|
||||
|
||||
val formattedSize: String
|
||||
get() =
|
||||
when {
|
||||
size <= 0 -> "—"
|
||||
size < 1024 -> "$size B"
|
||||
size < 1024 * 1024 -> "${size / 1024} KB"
|
||||
else -> "%.1f MB".format(size.toDouble() / (1024 * 1024))
|
||||
@@ -31,3 +43,13 @@ data class Attachment(
|
||||
val fileExtension: String
|
||||
get() = name.substringAfterLast('.', "").lowercase()
|
||||
}
|
||||
|
||||
/** Origin of an [Attachment]. */
|
||||
@Serializable
|
||||
enum class AttachmentSource {
|
||||
/** Picked by the user on this device (content URI / local path). */
|
||||
LOCAL,
|
||||
|
||||
/** Delivered by the agent via a gateway-hosted `MEDIA:` directive. */
|
||||
GATEWAY,
|
||||
}
|
||||
Reference in new issue
Block a user