Skip to content

Commit cfb1ca6

Browse files
committed
feat(codereview): add issue cache with refresh and age display #463
Implement persistent caching for issue info with timestamps, cache age indicators in UI, and manual refresh support to reduce API calls and improve transparency.
1 parent 236157e commit cfb1ca6

6 files changed

Lines changed: 392 additions & 55 deletions

File tree

mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tracker/IssueTracker.kt

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package cc.unitmesh.agent.tracker
22

3+
import kotlinx.datetime.Clock
4+
import kotlinx.datetime.Instant
5+
36
/**
47
* Issue information from issue tracker
58
*/
@@ -12,8 +15,46 @@ data class IssueInfo(
1215
val author: String? = null,
1316
val assignees: List<String> = emptyList(),
1417
val createdAt: String? = null,
15-
val updatedAt: String? = null
16-
)
18+
val updatedAt: String? = null,
19+
/**
20+
* When this issue info was fetched/cached (null if freshly fetched)
21+
*/
22+
val cachedAt: Instant? = null
23+
) {
24+
/**
25+
* Check if this issue info is from cache
26+
*/
27+
val isFromCache: Boolean get() = cachedAt != null
28+
29+
/**
30+
* Get cache age in minutes (null if not cached)
31+
*/
32+
fun getCacheAgeMinutes(): Long? {
33+
return cachedAt?.let {
34+
(Clock.System.now() - it).inWholeMinutes
35+
}
36+
}
37+
38+
/**
39+
* Get human-readable cache age
40+
*/
41+
fun getCacheAgeDisplay(): String? {
42+
val minutes = getCacheAgeMinutes() ?: return null
43+
return when {
44+
minutes < 1 -> "just now"
45+
minutes < 60 -> "${minutes}m ago"
46+
minutes < 1440 -> "${minutes / 60}h ago"
47+
else -> "${minutes / 1440}d ago"
48+
}
49+
}
50+
51+
/**
52+
* Create a cached copy with current timestamp
53+
*/
54+
fun withCacheTimestamp(timestamp: Instant = Clock.System.now()): IssueInfo {
55+
return copy(cachedAt = timestamp)
56+
}
57+
}
1758

1859
/**
1960
* Abstract interface for issue tracking systems

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/codereview/CodeReviewModels.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,5 +135,7 @@ data class CommitInfo(
135135
val message: String,
136136
val issueInfo: IssueInfo? = null, // Issue information extracted from commit message
137137
val isLoadingIssue: Boolean = false, // Whether issue info is being loaded
138-
val issueLoadError: String? = null // Error message if issue loading failed (e.g., needs token)
138+
val issueLoadError: String? = null, // Error message if issue loading failed (e.g., needs token)
139+
val issueFromCache: Boolean = false, // Whether issue info was loaded from cache
140+
val issueCacheAge: String? = null // Human-readable cache age (e.g., "5m ago")
139141
)

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/codereview/CodeReviewSideBySideView.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,15 @@ private fun ThreeColumnLayout(
158158
val workspaceRoot = viewModel.workspace.rootPath
159159
val onConfigureToken = onShowConfigDialog
160160

161+
// Map selected commit indices for refresh callback
162+
val selectedIndicesList = state.selectedCommitIndices.toList().sorted()
163+
val onRefreshIssue: (Int) -> Unit = { relativeIndex ->
164+
// Map relative index in selectedCommits to actual commitHistory index
165+
selectedIndicesList.getOrNull(relativeIndex)?.let { actualIndex ->
166+
viewModel.refreshIssueForCommit(actualIndex)
167+
}
168+
}
169+
161170
DiffCenterView(
162171
diffFiles = state.diffFiles,
163172
selectedCommits = state.selectedCommitIndices.mapNotNull { state.commitHistory.getOrNull(it) },
@@ -166,6 +175,7 @@ private fun ThreeColumnLayout(
166175
workspaceRoot = workspaceRoot,
167176
isLoadingDiff = state.isLoadingDiff,
168177
onConfigureToken = onConfigureToken,
178+
onRefreshIssue = onRefreshIssue,
169179
relatedTests = state.relatedTests,
170180
isLoadingTests = state.isLoadingTests
171181
)

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/codereview/CodeReviewViewModel.kt

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,12 +1081,13 @@ open class CodeReviewViewModel(
10811081
* Load issue information for a specific commit asynchronously
10821082
*
10831083
* @param commitIndex Index of the commit in commitHistory
1084+
* @param forceRefresh If true, bypass cache and fetch fresh data
10841085
*/
1085-
private fun loadIssueForCommit(commitIndex: Int) {
1086+
private fun loadIssueForCommit(commitIndex: Int, forceRefresh: Boolean = false) {
10861087
val commit = currentState.commitHistory.getOrNull(commitIndex) ?: return
10871088

1088-
// Skip if already loaded or loading
1089-
if (commit.issueInfo != null || commit.isLoadingIssue) {
1089+
// Skip if already loaded or loading (unless force refresh)
1090+
if (!forceRefresh && (commit.issueInfo != null || commit.isLoadingIssue)) {
10901091
return
10911092
}
10921093

@@ -1096,17 +1097,25 @@ open class CodeReviewViewModel(
10961097
// Load issue asynchronously
10971098
scope.launch {
10981099
try {
1099-
val issueDeferred = issueService.getIssueAsync(commit.hash, commit.message)
1100+
val issueDeferred = issueService.getIssueAsync(commit.hash, commit.message, forceRefresh)
11001101
val result = issueDeferred.await()
11011102

11021103
// Update commit with issue info or error
11031104
updateCommitAtIndex(commitIndex) {
11041105
it.copy(
11051106
issueInfo = result.issueInfo,
11061107
isLoadingIssue = false,
1107-
issueLoadError = result.error
1108+
issueLoadError = result.error,
1109+
issueFromCache = result.fromCache,
1110+
issueCacheAge = result.cacheAgeDisplay
11081111
)
11091112
}
1113+
1114+
if (result.fromCache) {
1115+
AutoDevLogger.debug("CodeReviewViewModel") {
1116+
"Loaded cached issue for commit ${commit.shortHash} (cached ${result.cacheAgeDisplay})"
1117+
}
1118+
}
11101119
} catch (e: Exception) {
11111120
AutoDevLogger.error("CodeReviewViewModel") {
11121121
"Failed to load issue for commit ${commit.shortHash}: ${e.message}"
@@ -1120,6 +1129,13 @@ open class CodeReviewViewModel(
11201129
}
11211130
}
11221131
}
1132+
1133+
/**
1134+
* Force refresh issue for a specific commit (bypasses cache)
1135+
*/
1136+
fun refreshIssueForCommit(commitIndex: Int) {
1137+
loadIssueForCommit(commitIndex, forceRefresh = true)
1138+
}
11231139

11241140
/**
11251141
* Update a commit at a specific index

0 commit comments

Comments
 (0)