diff --git a/CHANGELOG.md b/CHANGELOG.md index 213a15c5f..fc34e857a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Changelog **Unreleased** -------------- +- Change `slack.allowWarnings` property to `sgp.kotlin.allowWarnings`. +- Disallow warnings in Kotlin test compilations by default, add `sgp.kotlin.allowWarningsInTests` property to opt-out. + 0.19.3 ------ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6f7738a21..5b49b69ec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] agp = "8.6.0" -agpAlpha = "8.6.0" +agpAlpha = "8.6.1" anvil = "2.5.0-beta11" bugsnagGradle = "8.1.0" circuit = "0.23.1" @@ -13,7 +13,7 @@ errorproneGradle = "3.0.1" jdk = "22" jvmTarget = "17" jewel = "0.15.2.2" -jna = "5.14.0" +jna = "5.15.0" kaml = "0.61.0" kotlin = "2.0.20" ksp = "2.0.20-1.0.25" @@ -64,7 +64,7 @@ agpAlpha = { module = "com.android.tools.build:gradle", version.ref = "agpAlpha" autoService-annotations = "com.google.auto.service:auto-service-annotations:1.1.1" autoService-ksp = "dev.zacsweers.autoservice:auto-service-ksp:1.2.0" bugsnag = "com.bugsnag:bugsnag:3.7.2" -clikt = "com.github.ajalt.clikt:clikt:4.4.0" +clikt = "com.github.ajalt.clikt:clikt:5.0.0" circuit-foundation = { module = "com.slack.circuit:circuit-foundation", version.ref = "circuit" } commonsText = "org.apache.commons:commons-text:1.12.0" composeLints = "com.slack.lint.compose:compose-lint-checks:1.3.1" @@ -117,9 +117,10 @@ okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttp okhttp-loggingInterceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } okio = { module = "com.squareup.okio:okio", version.ref = "okio" } okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okio" } -oshi = "com.github.oshi:oshi-core:6.6.3" +oshi = "com.github.oshi:oshi-core:6.6.4" retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-converters-wire = { module = "com.squareup.retrofit2:converter-wire", version.ref = "retrofit" } +retrofit-converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit"} rxjava = "io.reactivex.rxjava3:rxjava:3.1.9" slackLints-checks = { module = "com.slack.lint:slack-lint-checks", version.ref = "slack-lint" } slackLints-annotations = { module = "com.slack.lint:slack-lint-annotations", version.ref = "slack-lint" } diff --git a/skate-plugin/project-gen/build.gradle.kts b/skate-plugin/project-gen/build.gradle.kts index fb6aa6de6..5cc8f15d8 100644 --- a/skate-plugin/project-gen/build.gradle.kts +++ b/skate-plugin/project-gen/build.gradle.kts @@ -45,6 +45,12 @@ kotlin { implementation(libs.jewel.bridge232) implementation(libs.kotlin.poet) implementation(libs.markdown) + + implementation(libs.kaml) + implementation(libs.okhttp) + implementation(libs.okhttp.loggingInterceptor) + implementation(libs.retrofit) + implementation(libs.retrofit.converter.gson) } } } diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt new file mode 100644 index 000000000..89170dfc8 --- /dev/null +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatBotActionService.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2024 Slack Technologies, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package slack.tooling.aibot + +import com.google.gson.Gson +import com.google.gson.JsonObject +import java.io.BufferedReader +import java.io.File +import java.io.InputStreamReader +import java.util.concurrent.TimeUnit +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jetbrains.annotations.VisibleForTesting + +class ChatBotActionService { + suspend fun executeCommand(question: String): String { + val jsonInput = createJsonInput(question) + val scriptContent = createScriptContent(jsonInput) + val tempScript = createTempScript(scriptContent) + val output = runScript(tempScript) + tempScript.delete() + return parseOutput(output) + } + + @VisibleForTesting + private fun createJsonInput(question: String): String { + val gsonInput = Gson() + val jsonObjectInput = + Content( + messages = listOf(Message(role = "user", question)), + source = "curl", + max_tokens = 2048, + ) + + val content = gsonInput.toJson(jsonObjectInput) + + println("jsonContent $content") + + return content + } + + @VisibleForTesting + private fun createScriptContent(jsonInput: String): String { + val scriptContent = + """ + temp + """ + .trimIndent() + return scriptContent + } + + @VisibleForTesting + private suspend fun createTempScript(scriptContent: String): File { + val tempScript = withContext(Dispatchers.IO) { File.createTempFile("run_command", ".sh") } + tempScript.writeText(scriptContent) + tempScript.setExecutable(true) + return tempScript + } + + @VisibleForTesting + private fun runScript(tempScript: File): String { + + val processBuilder = ProcessBuilder("/bin/bash", tempScript.absolutePath) + processBuilder.redirectErrorStream(true) + + val process = processBuilder.start() + val output = StringBuilder() + + BufferedReader(InputStreamReader(process.inputStream)).use { reader -> + var line: String? + while (reader.readLine().also { line = it } != null) { + output.append(line).append("\n") + } + } + + val completed = process.waitFor(600, TimeUnit.SECONDS) + if (!completed) { + process.destroyForcibly() + throw RuntimeException("Process timed out after 600 seconds") + } + + tempScript.delete() + return output.toString() + } + + @VisibleForTesting + private fun parseOutput(output: String): String { + println("output: $output") + val regex = """\{.*\}""".toRegex(RegexOption.DOT_MATCHES_ALL) + val result = regex.find(output.toString())?.value ?: "{}" + val gson = Gson() + val jsonObject = gson.fromJson(result, JsonObject::class.java) + val contentArray = jsonObject.getAsJsonArray("content") + val contentObject = contentArray.get(0).asJsonObject + val actualContent = contentObject.get("content").asString + + println("actual content $actualContent") + + return actualContent + } + + data class Content( + val messages: List, + val source: String = "curl", + val max_tokens: Int = 512, + ) +} diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatColors.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatColors.kt index e7e085bfe..b2d9207d9 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatColors.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatColors.kt @@ -15,21 +15,18 @@ */ package slack.tooling.aibot -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.graphics.Color -import org.jetbrains.jewel.foundation.theme.JewelTheme object ChatColors { val promptBackground = Color(0xFF45494A) // Color(0xFF2d2f30) responseBackground - val responseBackground: Color - @Composable @ReadOnlyComposable get() = JewelTheme.globalColors.infoContent + val responseBackground = Color(0xFF2d2f30) + // @Composable @ReadOnlyComposable get() = JewelTheme.globalColors.infoContent // Color(0xFFEAEEF7) userTextColor - val userTextColor: Color - @Composable @ReadOnlyComposable get() = JewelTheme.globalColors.infoContent + val userTextColor = Color(0xFFEAEEF7) + // @Composable @ReadOnlyComposable get() = JewelTheme.globalColors.infoContent val responseTextColor = Color(0xFFE0EEF7) } diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt index 83eaf348b..8cf9c2439 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatPresenter.kt @@ -21,8 +21,15 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import com.slack.circuit.runtime.presenter.Presenter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch class ChatPresenter : Presenter { + val user = "user" + val bot = "bot" + private val chatBotActionService = ChatBotActionService() + @Composable override fun present(): ChatScreen.State { var messages by remember { mutableStateOf(emptyList()) } @@ -30,18 +37,15 @@ class ChatPresenter : Presenter { return ChatScreen.State(messages = messages) { event -> when (event) { is ChatScreen.Event.SendMessage -> { - val newMessage = Message(event.message, isMe = true) + val newMessage = Message(role = user, event.message) messages = messages + newMessage - val response = Message(callApi(event.message), isMe = false) - messages = messages + response + + CoroutineScope(Dispatchers.IO).launch { + val response = chatBotActionService.executeCommand(event.message) + messages = messages + Message(role = bot, response) + } } } } } - - private fun callApi(message: String): String { - // function set up to call the DevXP API in the future. - // right now, just sends back the user input message - return ("I am a bot. You said \"${message}\"") - } } diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatWindowUi.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatWindowUi.kt index e72aec314..70d5955e3 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatWindowUi.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/ChatWindowUi.kt @@ -63,9 +63,10 @@ fun ChatWindowUi(state: ChatScreen.State, modifier: Modifier = Modifier) { Column(modifier = modifier.fillMaxSize().background(JewelTheme.globalColors.paneBackground)) { LazyColumn(modifier = Modifier.weight(1f), reverseLayout = true) { items(state.messages.reversed()) { message -> + val isMe = message.role == "user" Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = if (message.isMe) Arrangement.End else Arrangement.Start, + horizontalArrangement = if (isMe) Arrangement.End else Arrangement.Start, ) { ChatBubble(message) } @@ -148,18 +149,17 @@ private fun ConversationField(modifier: Modifier = Modifier, onSendMessage: (Str @Composable private fun ChatBubble(message: Message, modifier: Modifier = Modifier) { + val isMe = message.role == "user" Box( Modifier.wrapContentWidth() .padding(8.dp) .shadow(elevation = 0.5.dp, shape = RoundedCornerShape(25.dp), clip = true) - .background( - color = if (message.isMe) ChatColors.promptBackground else ChatColors.responseBackground - ) + .background(color = if (isMe) ChatColors.promptBackground else ChatColors.responseBackground) .padding(8.dp) ) { Text( - text = message.text, - color = if (message.isMe) ChatColors.userTextColor else ChatColors.responseTextColor, + text = message.content, + color = if (isMe) ChatColors.userTextColor else ChatColors.responseTextColor, modifier = modifier.padding(8.dp), fontFamily = FontFamily.SansSerif, ) diff --git a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/Message.kt b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/Message.kt index 2b2da9499..3c184dfba 100644 --- a/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/Message.kt +++ b/skate-plugin/project-gen/src/jvmMain/kotlin/slack/tooling/aibot/Message.kt @@ -17,4 +17,4 @@ package slack.tooling.aibot import androidx.compose.runtime.Immutable -@Immutable data class Message(val text: String, val isMe: Boolean) +@Immutable data class Message(var role: String, val content: String) diff --git a/skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt b/skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt new file mode 100644 index 000000000..8c59e0efa --- /dev/null +++ b/skate-plugin/src/test/kotlin/slack/tooling/aibot/ChatBotActionServiceTest.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2024 Slack Technologies, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package slack.tooling.aibot + +import com.google.common.truth.Truth.assertThat +import com.google.gson.Gson +import com.google.gson.JsonObject +import junit.framework.TestCase.assertEquals +import org.junit.Test + +class ChatBotActionServiceTest { + @Test + fun `createJsonInput with simple input`() { + val question = "Why is the sky blue?" + + val result = createJsonInput(question) + + val expectedJson = + """ + { + "messages": [ + { + "role": "user", + "content": "Why is the sky blue?" + } + ], + "source": "curl", + "max_tokens": 512 + } + """ + .trimIndent() + + val trimmedExpected = expectedJson.replace(Regex("\\s"), "") + val trimmedResult = result.replace(Regex("\\s"), "") + println("expected is $trimmedExpected") + println("actual is $trimmedResult") + + assertThat(trimmedResult).isEqualTo(trimmedExpected) + } + + @Test + fun `createJsonInput with long strings`() { + val question = "A".repeat(10000) + val result = createJsonInput(question) + println("result $result") + val jsonObject = Gson().fromJson(result, JsonObject::class.java) + println(jsonObject) + assertEquals( + question, + jsonObject.get("messages").asJsonArray[0].asJsonObject.get("content").asString, + ) + } + + @Test + fun `createJsonInput with special characters`() { + val question = "What about \n, \t, and \"quotes\"? and \'apostrophes" + val result = createJsonInput(question) + println("result $result") + val jsonObject = Gson().fromJson(result, JsonObject::class.java) + assertEquals( + question, + jsonObject.get("messages").asJsonArray[0].asJsonObject.get("content").asString, + ) + } + + private fun createJsonInput(question: String): String { + val user = "user" + val gsonInput = Gson() + val content = + Content(messages = listOf(Message(role = user, question)), source = "curl", max_tokens = 512) + + val jsonContent = gsonInput.toJson(content).toString() + return jsonContent + } + + data class Content( + val messages: List, + val source: String = "curl", + val max_tokens: Int = 512, + ) +} diff --git a/skippy/src/main/kotlin/com/slack/skippy/CliktSgpLogger.kt b/skippy/src/main/kotlin/com/slack/skippy/CliktSgpLogger.kt index 7ccd1bfe7..5b0e73d81 100644 --- a/skippy/src/main/kotlin/com/slack/skippy/CliktSgpLogger.kt +++ b/skippy/src/main/kotlin/com/slack/skippy/CliktSgpLogger.kt @@ -15,12 +15,13 @@ */ package com.slack.skippy -import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.BaseCliktCommand import com.slack.sgp.common.SgpLogger -internal fun SgpLogger.Companion.clikt(command: CliktCommand): SgpLogger = CliktSgpLogger(command) +internal fun SgpLogger.Companion.clikt(command: BaseCliktCommand<*>): SgpLogger = + CliktSgpLogger(command) -private class CliktSgpLogger(private val command: CliktCommand) : SgpLogger { +private class CliktSgpLogger(private val command: BaseCliktCommand<*>) : SgpLogger { override fun debug(message: String) { command.echo(message) } diff --git a/skippy/src/main/kotlin/com/slack/skippy/ComputeAffectedProjectsCli.kt b/skippy/src/main/kotlin/com/slack/skippy/ComputeAffectedProjectsCli.kt index c4f2aa220..614056c89 100644 --- a/skippy/src/main/kotlin/com/slack/skippy/ComputeAffectedProjectsCli.kt +++ b/skippy/src/main/kotlin/com/slack/skippy/ComputeAffectedProjectsCli.kt @@ -15,7 +15,8 @@ */ package com.slack.skippy -import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.command.SuspendingCliktCommand +import com.github.ajalt.clikt.core.Context import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.options.required @@ -33,7 +34,6 @@ import kotlin.io.path.readText import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.newFixedThreadPoolContext -import kotlinx.coroutines.runBlocking import okio.FileSystem import okio.Path.Companion.toOkioPath @@ -41,10 +41,10 @@ import okio.Path.Companion.toOkioPath * @see AffectedProjectsComputer for most of the salient docs! The inputs in this CLI more or less * match 1:1 to the properties of that class. */ -public class ComputeAffectedProjectsCli : - CliktCommand( - help = "Computes affected projects and writes output files to an output directory." - ) { +public class ComputeAffectedProjectsCli : SuspendingCliktCommand() { + + override fun help(context: Context): String = + "Computes affected projects and writes output files to an output directory." private val debug: Boolean by option("--debug", help = "Enable debug logging.").flag(default = false) @@ -102,7 +102,7 @@ public class ComputeAffectedProjectsCli : private val logger = SgpLogger.clikt(this) @OptIn(DelicateCoroutinesApi::class) - override fun run() { + override suspend fun run() { val moshi = Moshi.Builder().build() val dependencyGraph = ObjectInputStream(serializedDependencyGraph.inputStream()).use { @@ -136,15 +136,11 @@ public class ComputeAffectedProjectsCli : .run(context) } - runBlocking { - if (parallelism == 1) { - body(Dispatchers.Unconfined) - } else { - logger.lifecycle("Running $parallelism configs in parallel") - newFixedThreadPoolContext(3, "computeAffectedProjects").use { dispatcher -> - body(dispatcher) - } - } + if (parallelism == 1) { + body(Dispatchers.Unconfined) + } else { + logger.lifecycle("Running $parallelism configs in parallel") + newFixedThreadPoolContext(3, "computeAffectedProjects").use { dispatcher -> body(dispatcher) } } } } diff --git a/slack-plugin/src/main/kotlin/slack/gradle/GradleExt.kt b/slack-plugin/src/main/kotlin/slack/gradle/GradleExt.kt index 025a825b5..12903ee07 100644 --- a/slack-plugin/src/main/kotlin/slack/gradle/GradleExt.kt +++ b/slack-plugin/src/main/kotlin/slack/gradle/GradleExt.kt @@ -35,6 +35,7 @@ import org.gradle.api.plugins.PluginManager import org.gradle.api.provider.ListProperty import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Property +import org.gradle.api.provider.Provider import org.gradle.api.provider.SetProperty import org.gradle.api.reflect.TypeOf import org.gradle.api.tasks.TaskContainer @@ -282,3 +283,6 @@ internal inline fun TaskContainer.registerOrConfigure( in names -> named(taskName) as TaskProvider else -> register(taskName, T::class.java) }.apply { configure { configureAction() } } + +/** Returns a provider that is the inverse of this. */ +internal fun Provider.not(): Provider = map { !it } diff --git a/slack-plugin/src/main/kotlin/slack/gradle/SlackProperties.kt b/slack-plugin/src/main/kotlin/slack/gradle/SlackProperties.kt index 9f52744aa..59db8ef88 100644 --- a/slack-plugin/src/main/kotlin/slack/gradle/SlackProperties.kt +++ b/slack-plugin/src/main/kotlin/slack/gradle/SlackProperties.kt @@ -199,9 +199,13 @@ internal constructor( public val robolectricIVersion: Int get() = intProperty("slack.robolectricIVersion") - /** Opt out for -Werror, should only be used for prototype projects. */ - public val allowWarnings: Boolean - get() = booleanProperty("slack.allowWarnings") + /** Opt out for -Werror. */ + public val allowWarnings: Provider + get() = resolver.booleanProvider("sgp.kotlin.allowWarnings", defaultValue = false) + + /** Opt out for -Werror in tests. */ + public val allowWarningsInTests: Provider + get() = resolver.booleanProvider("sgp.kotlin.allowWarningsInTests", defaultValue = false) /** * Anvil generator projects that should always be included when Anvil is enabled. diff --git a/slack-plugin/src/main/kotlin/slack/gradle/kgp/KgpTasks.kt b/slack-plugin/src/main/kotlin/slack/gradle/kgp/KgpTasks.kt index 139e4140a..b37ba98ee 100644 --- a/slack-plugin/src/main/kotlin/slack/gradle/kgp/KgpTasks.kt +++ b/slack-plugin/src/main/kotlin/slack/gradle/kgp/KgpTasks.kt @@ -37,8 +37,10 @@ import slack.gradle.SlackTools import slack.gradle.asProvider import slack.gradle.configure import slack.gradle.lint.DetektTasks +import slack.gradle.not import slack.gradle.onFirst import slack.gradle.util.configureKotlinCompilationTask +import slack.gradle.util.setDisallowChanges /** Common configuration for Kotlin projects. */ internal object KgpTasks { @@ -179,11 +181,10 @@ internal object KgpTasks { slackProperties.kotlinLanguageVersionOverride.map(KotlinVersion::fromVersion) ) } - if ( - !slackProperties.allowWarnings && - !this@configureKotlinCompilationTask.name.contains("test", ignoreCase = true) - ) { - allWarningsAsErrors.set(true) + if (this@configureKotlinCompilationTask.name.contains("test", ignoreCase = true)) { + allWarningsAsErrors.setDisallowChanges(slackProperties.allowWarningsInTests.not()) + } else { + allWarningsAsErrors.setDisallowChanges(slackProperties.allowWarnings.not()) } freeCompilerArgs.addAll(slackProperties.kotlinFreeArgs)