feat(#1217): add compacted-session timeline navigation (#1227)

This commit is contained in:
m57 authored and GitHub committed 2026-09-22 18:29:06 +03:00
1 parent 2e6ae95fe0
commit 16967c7e91
12 files changed
+1124 -34

No files matched your search

@@ -42,6 +42,12 @@ data class SessionMessage(
val reasoning: JsonElement? = null,
val reasoning_text: JsonElement? = null,
val tool_call_id: String? = null,
/**
* User-visible projection supplied by the backend for rows whose physical
* [content] is model-facing scaffolding (notably compaction summaries).
* Prefer this for rendering whenever present.
*/
val display_content: JsonElement? = null,
/**
* Timeline-marker tag (backend NS-656 lineage, issue #904): markers like
* `model_switch` / `personality_switch` / `auto_continue` ride as
@@ -85,6 +91,14 @@ data class SessionMessage(
else -> content.toString()
}
val displayContentText: String?
get() =
when (display_content) {
is JsonPrimitive -> display_content.content
null -> null
else -> display_content.toString()
}
val reasoningText: String
get() =
when (val r = reasoning ?: reasoning_text) {
@@ -0,0 +1,53 @@
package com.m57.hermescontrol.data.model
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
/** Lightweight prompt index returned by GET /api/sessions/{id}/timeline. */
@Serializable
data class SessionTimelineResponse(
val session_id: String? = null,
val profile: String? = null,
val entries: List<SessionTimelineEntry> = emptyList(),
val pagination: SessionTimelinePagination = SessionTimelinePagination(),
)
@Serializable
data class SessionTimelineEntry(
/** Stable representative row id. Pass this exact value to /messages/around. */
val row_id: Int,
val preview: String = "",
val timestamp: JsonElement? = null,
)
@Serializable
data class SessionTimelinePagination(
val limit: Int? = null,
val after_row_id: Int? = null,
val returned: Int? = null,
val total: Int? = null,
val has_more: Boolean = false,
/** Backend cursor. It is not interchangeable with the last returned row_id. */
val next_cursor: Int? = null,
)
/** Bounded chronological display page returned by GET /api/sessions/{id}/messages/around. */
@Serializable
data class SessionMessagesAroundResponse(
val session_id: String? = null,
val profile: String? = null,
val messages: List<SessionMessage> = emptyList(),
val pagination: SessionMessagesAroundPagination = SessionMessagesAroundPagination(),
)
@Serializable
data class SessionMessagesAroundPagination(
val row_id: Int? = null,
val limit: Int? = null,
val returned: Int? = null,
val order: String? = null,
val offset: Int = 0,
val total: Int? = null,
val has_older: Boolean = false,
val has_newer: Boolean = false,
)
@@ -102,10 +102,12 @@ import com.m57.hermescontrol.data.model.ScanStatus
import com.m57.hermescontrol.data.model.SessionDetailResponse
import com.m57.hermescontrol.data.model.SessionInfo
import com.m57.hermescontrol.data.model.SessionListResponse
import com.m57.hermescontrol.data.model.SessionMessagesAroundResponse
import com.m57.hermescontrol.data.model.SessionMessagesResponse
import com.m57.hermescontrol.data.model.SessionRenameRequest
import com.m57.hermescontrol.data.model.SessionSearchResponse
import com.m57.hermescontrol.data.model.SessionStatsResponse
import com.m57.hermescontrol.data.model.SessionTimelineResponse
import com.m57.hermescontrol.data.model.SetActiveProfileRequest
import com.m57.hermescontrol.data.model.Skill
import com.m57.hermescontrol.data.model.SkillContentResponse
@@ -209,6 +211,22 @@ interface HermesApiService : KanbanApiService {
@Query("profile") profile: String? = null,
): Response<SessionMessagesResponse>
@GET("api/sessions/{id}/timeline")
suspend fun getSessionTimeline(
@Path("id", encoded = true) sessionId: String,
@Query("profile") profile: String? = null,
@Query("limit") limit: Int = 500,
@Query("after_row_id") afterRowId: Int = 0,
): Response<SessionTimelineResponse>
@GET("api/sessions/{id}/messages/around")
suspend fun getSessionMessagesAround(
@Path("id", encoded = true) sessionId: String,
@Query("row_id") rowId: Int,
@Query("profile") profile: String? = null,
@Query("limit") limit: Int = 120,
): Response<SessionMessagesAroundResponse>
@GET("api/sessions/stats")
suspend fun getSessionStats(): Response<SessionStatsResponse>