Skip to content

Commit 4a64e8a

Browse files
committed
🔖 [Release]: Prepare 2.3.0
1 parent 8071458 commit 4a64e8a

11 files changed

Lines changed: 164 additions & 54 deletions

File tree

ai/gradle.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
POM_ARTIFACT_ID=ai
2+
POM_NAME=Compose Hooks AI
3+
POM_DESCRIPTION=AI hooks and utilities for Compose Multiplatform.

ai/src/commonMain/kotlin/xyz/junerver/compose/ai/tokenstats/TokenUsageTracker.kt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,13 @@ data class WindowTokenUsage(
9494
*/
9595
@Stable
9696
class TokenUsageTracker {
97-
private val _statsState = mutableStateOf(TokenUsageStats.EMPTY)
97+
private val statsState = mutableStateOf(TokenUsageStats.EMPTY)
9898

9999
/**
100100
* Current aggregated token usage statistics.
101101
* This is a reactive state that will trigger recomposition when updated.
102102
*/
103-
val stats: TokenUsageStats get() = _statsState.value
103+
val stats: TokenUsageStats get() = statsState.value
104104

105105
/**
106106
* Record a new token usage from a completed request.
@@ -117,20 +117,20 @@ class TokenUsageTracker {
117117
model = model,
118118
usage = usage,
119119
)
120-
_statsState.value = _statsState.value.copy(
121-
totalPromptTokens = _statsState.value.totalPromptTokens + usage.promptTokens,
122-
totalCompletionTokens = _statsState.value.totalCompletionTokens + usage.completionTokens,
123-
totalTokens = _statsState.value.totalTokens + usage.totalTokens,
124-
requestCount = _statsState.value.requestCount + 1,
125-
records = (_statsState.value.records + record).toImmutableList(),
120+
statsState.value = statsState.value.copy(
121+
totalPromptTokens = statsState.value.totalPromptTokens + usage.promptTokens,
122+
totalCompletionTokens = statsState.value.totalCompletionTokens + usage.completionTokens,
123+
totalTokens = statsState.value.totalTokens + usage.totalTokens,
124+
requestCount = statsState.value.requestCount + 1,
125+
records = (statsState.value.records + record).toImmutableList(),
126126
)
127127
}
128128

129129
/**
130130
* Reset all accumulated statistics.
131131
*/
132132
fun reset() {
133-
_statsState.value = TokenUsageStats.EMPTY
133+
statsState.value = TokenUsageStats.EMPTY
134134
}
135135
}
136136

ai/src/commonMain/kotlin/xyz/junerver/compose/ai/useagent/useAgent.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ fun useAgent(optionsOf: AgentOptions.() -> Unit = {}): AgentHolder {
193193
requestId = response.message.id,
194194
provider = optionsRef.current.provider.name,
195195
model = optionsRef.current.effectiveModel,
196-
usage = response.usage!!,
196+
usage = response.usage,
197197
)
198198
}
199199
optionsRef.current.onFinish?.invoke(

ai/src/commonMain/kotlin/xyz/junerver/compose/ai/usechat/useChat.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ fun useChat(optionsOf: ChatOptions.() -> Unit = {}): ChatHolder {
405405
requestId = finalMessage.id,
406406
provider = optionsRef.current.provider.name,
407407
model = optionsRef.current.effectiveModel,
408-
usage = lastUsage!!,
408+
usage = lastUsage,
409409
)
410410
}
411411
optionsRef.current.onFinish?.invoke(
@@ -426,8 +426,7 @@ fun useChat(optionsOf: ChatOptions.() -> Unit = {}): ChatHolder {
426426
is StreamEvent.Multi -> Unit
427427
is StreamEvent.AudioDelta -> Unit // TTS audio events not used in chat
428428
}
429-
}
430-
?.collect()
429+
}.collect()
431430
} else {
432431
// Use multi-provider or single-provider client
433432
val result = if (useMultiProvider) {
@@ -454,7 +453,7 @@ fun useChat(optionsOf: ChatOptions.() -> Unit = {}): ChatHolder {
454453
requestId = finalMessage.id,
455454
provider = optionsRef.current.provider.name,
456455
model = optionsRef.current.effectiveModel,
457-
usage = result.usage!!,
456+
usage = result.usage,
458457
)
459458
}
460459
optionsRef.current.onFinish?.invoke(

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ mavenCentralAutomaticPublishing=true
5252

5353
GROUP=xyz.junerver.compose
5454
# use `-SNAPSHOT` can publish a snapshot to maven
55-
VERSION_NAME=2.2.2-beta-3
55+
VERSION_NAME=2.3.0
5656

5757
POM_NAME=hooks
5858

hooks/src/commonMain/kotlin/xyz/junerver/compose/hooks/usetable/features/filtering/FilteringFeature.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class FilteringFeature<T> : TableFeature<T> {
1313

1414
@Composable
1515
override fun initState(instance: TableInstance<T>) {
16-
// TODO: Register state and API
16+
// Filtering state and APIs are wired by useTable.
1717
}
1818

1919
override fun transform(

hooks/src/commonMain/kotlin/xyz/junerver/compose/hooks/usetable/features/rowselection/RowSelectionFeature.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class RowSelectionFeature<T> : TableFeature<T> {
1919

2020
@Composable
2121
override fun initState(instance: TableInstance<T>) {
22-
// TODO: Register API
22+
// Row selection state and APIs are wired by useTable.
2323
}
2424

2525
override fun transform(

hooks/src/commonMain/kotlin/xyz/junerver/compose/hooks/usetable/features/sorting/SortingFeature.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class SortingFeature<T> : TableFeature<T> {
1313

1414
@Composable
1515
override fun initState(instance: TableInstance<T>) {
16-
// TODO: Register state and API
16+
// Sorting state and APIs are wired by useTable.
1717
}
1818

1919
override fun transform(

hooks/src/desktopTest/kotlin/xyz/junerver/compose/hooks/test/UseIntervalDebugTest.kt

Lines changed: 13 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import androidx.compose.runtime.SideEffect
55
import androidx.compose.runtime.getValue
66
import androidx.compose.runtime.setValue
77
import androidx.compose.ui.test.ExperimentalTestApi
8-
import androidx.compose.ui.test.onNodeWithText
98
import androidx.compose.ui.test.runComposeUiTest
109
import kotlin.test.Test
10+
import kotlin.test.assertTrue
1111
import kotlin.time.Duration.Companion.milliseconds
1212
import xyz.junerver.compose.hooks.useInterval
1313
import xyz.junerver.compose.hooks.useState
@@ -25,6 +25,9 @@ class UseIntervalDebugTest {
2525
@OptIn(ExperimentalTestApi::class)
2626
@Test
2727
fun simple_interval_test() = runComposeUiTest {
28+
var observedCount = 0
29+
var observedActive = false
30+
2831
setContent {
2932
var count by useState(default = 0)
3033
var started by useState(default = false)
@@ -43,35 +46,21 @@ class UseIntervalDebugTest {
4346
}
4447
}
4548

49+
SideEffect {
50+
observedCount = count
51+
observedActive = isActive.value
52+
}
53+
4654
Text("count=$count active=${isActive.value} started=$started")
4755
}
4856

49-
// Initial state
50-
waitForIdle()
51-
println("Initial state checked")
52-
53-
// Wait and check multiple times
54-
for (i in 1..100) {
57+
repeat(100) {
5558
Thread.sleep(50)
5659
waitForIdle()
57-
val success = runCatching {
58-
onNodeWithText("count=1 active=true started=true").assertExists()
59-
}.isSuccess ||
60-
runCatching {
61-
onNodeWithText("count=2 active=true started=true").assertExists()
62-
}.isSuccess ||
63-
runCatching {
64-
onNodeWithText("count=3 active=true started=true").assertExists()
65-
}.isSuccess
66-
67-
if (success) {
68-
println("Found count >= 1 at iteration $i")
69-
return@runComposeUiTest
70-
}
60+
if (observedCount > 0 && observedActive) return@runComposeUiTest
7161
}
7262

73-
// If we get here, print what we actually see
74-
println("Test failed - checking final state")
75-
onNodeWithText("count=1 active=true started=true").assertExists()
63+
assertTrue(observedCount > 0, "Interval should execute at least once")
64+
assertTrue(observedActive, "Interval should remain active after resume")
7665
}
7766
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package xyz.junerver.compose.hooks.test
2+
3+
import androidx.compose.material3.Text
4+
import androidx.compose.runtime.SideEffect
5+
import androidx.compose.runtime.getValue
6+
import androidx.compose.runtime.setValue
7+
import androidx.compose.ui.test.ExperimentalTestApi
8+
import androidx.compose.ui.test.onNodeWithText
9+
import androidx.compose.ui.test.runComposeUiTest
10+
import kotlin.test.Test
11+
import xyz.junerver.compose.hooks.useState
12+
import xyz.junerver.compose.hooks.usetable.core.column
13+
import xyz.junerver.compose.hooks.usetable.useTable
14+
15+
/*
16+
Description: useTable desktop integration tests
17+
Author: Junerver
18+
Date: 2026/06/17
19+
Email: junerver@gmail.com
20+
Version: v1.0
21+
*/
22+
23+
@Suppress("DEPRECATION")
24+
class UseTableTest {
25+
private data class User(val name: String, val age: Int)
26+
27+
private val users = listOf(
28+
User("Charlie", 30),
29+
User("Alice", 25),
30+
User("Bob", 20),
31+
)
32+
33+
private val columns = listOf(
34+
column<User, String>("name") { it.name },
35+
column<User, Int>("age") { it.age },
36+
)
37+
38+
@OptIn(ExperimentalTestApi::class)
39+
@Test
40+
fun holder_api_updates_sorting_and_filtering_state() = runComposeUiTest {
41+
setContent {
42+
var phase by useState(default = 0)
43+
val table = useTable(
44+
data = users,
45+
columns = columns,
46+
optionsOf = {
47+
enableSorting = true
48+
enableFiltering = true
49+
},
50+
)
51+
52+
SideEffect {
53+
when (phase) {
54+
0 -> {
55+
table.toggleSorting("name", false)
56+
phase = 1
57+
}
58+
59+
1 -> {
60+
table.setGlobalFilter("o")
61+
phase = 2
62+
}
63+
}
64+
}
65+
66+
val rows = table.rowModel.value.rows.joinToString(",") { it.original.name }
67+
Text(
68+
text = "phase=$phase rows=$rows total=${table.rowModel.value.totalRows} " +
69+
"sorting=${table.state.value.sorting.sorting.size} filter=${table.state.value.filtering.globalFilter}",
70+
)
71+
}
72+
73+
waitForIdle()
74+
onNodeWithText("phase=2 rows=Bob total=1 sorting=1 filter=o").assertExists()
75+
}
76+
77+
@OptIn(ExperimentalTestApi::class)
78+
@Test
79+
fun holder_api_updates_pagination_and_row_selection_state() = runComposeUiTest {
80+
setContent {
81+
var phase by useState(default = 0)
82+
val table = useTable(
83+
data = users,
84+
columns = columns,
85+
optionsOf = {
86+
enablePagination = true
87+
enableRowSelection = true
88+
pageSize = 2
89+
},
90+
)
91+
92+
SideEffect {
93+
when (phase) {
94+
0 -> {
95+
table.setPageIndex(1)
96+
phase = 1
97+
}
98+
99+
1 -> {
100+
table.toggleRowSelection("2")
101+
phase = 2
102+
}
103+
}
104+
}
105+
106+
val rows = table.rowModel.value.rows.joinToString(",") { it.original.name }
107+
Text(
108+
text = "phase=$phase page=${table.state.value.pagination.pageIndex} rows=$rows " +
109+
"selected=${table.state.value.rowSelection.selectedRowIds.size}",
110+
)
111+
}
112+
113+
waitForIdle()
114+
onNodeWithText("phase=2 page=1 rows=Bob selected=1").assertExists()
115+
}
116+
}

0 commit comments

Comments
 (0)