Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
395 changes: 395 additions & 0 deletions index.html

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/main/kotlin/com/github/codeplangui/ChatService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,8 @@ $selection
if (session.getMessages().any { it.role != MessageRole.SYSTEM }) {
return
}
val ttlDays = PluginSettings.getInstance().state.sessionTtlDays
sessionStore.evictExpiredSessions(ttlDays)
val data = sessionStore.loadSession() ?: return
session = ChatSession(data.threadId)
data.messages.forEach { session.add(it) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ data class SettingsState(
var memoryText: String = "",
var commandExecutionEnabled: Boolean = true,
var commandWhitelist: MutableList<String> = ShellPlatform.current().defaultWhitelist().toMutableList(),
var commandTimeoutSeconds: Int = 30
var commandTimeoutSeconds: Int = 30,
var sessionTtlDays: Int = 30
)

@State(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class PluginSettingsConfigurable : Configurable {
private lateinit var commandTimeoutSpinner: JSpinner
private lateinit var commandWhitelistModel: DefaultListModel<String>
private lateinit var commandWhitelistList: JList<String>
private lateinit var sessionTtlDaysSpinner: JSpinner
private val client = OkHttpSseClient()
private val pendingApiKeyUpdates = linkedMapOf<String, String?>()

Expand Down Expand Up @@ -208,6 +209,8 @@ class PluginSettingsConfigurable : Configurable {
wrapStyleWord = true
}

sessionTtlDaysSpinner = JSpinner(SpinnerNumberModel(settings.sessionTtlDays, 1, 365, 1))

val chatCommitPanel = FormBuilder.createFormBuilder()
.addLabeledComponent("Temperature:", temperatureSpinner)
.addLabeledComponent("Max Tokens:", maxTokensSpinner)
Expand Down Expand Up @@ -240,6 +243,10 @@ class PluginSettingsConfigurable : Configurable {
JBLabel("AI 记忆(注入所有对话的系统提示词):"),
JScrollPane(memoryTextArea)
)
.addLabeledComponent(
"Session 过期天数 (0 = 永不过期):",
sessionTtlDaysSpinner
)
.panel

val execSettings = SettingsFormState.fromSettingsState(PluginSettings.getInstance().getState())
Expand Down Expand Up @@ -345,6 +352,7 @@ class PluginSettingsConfigurable : Configurable {
commitMaxFilesSpinner.value = settings.commitMaxFiles
commitDiffLineLimitSpinner.value = settings.commitDiffLineLimit
memoryTextArea.text = settings.memoryText
sessionTtlDaysSpinner.value = settings.sessionTtlDays
val execState = SettingsFormState.fromSettingsState(PluginSettings.getInstance().getState())
commandExecutionCheckbox.isSelected = execState.commandExecutionEnabled
commandTimeoutSpinner.value = execState.commandTimeoutSeconds
Expand Down Expand Up @@ -375,6 +383,7 @@ class PluginSettingsConfigurable : Configurable {
commandWhitelistModel.getElementAt(it)
}.toMutableList(),
commandTimeoutSeconds = (commandTimeoutSpinner.value as Number).toInt(),
sessionTtlDays = (sessionTtlDaysSpinner.value as Number).toInt(),
)

private fun selectedProviderId(): String? =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ data class SettingsFormState(
var memoryText: String = "",
var commandExecutionEnabled: Boolean = true,
var commandWhitelist: MutableList<String> = ShellPlatform.current().defaultWhitelist().toMutableList(),
var commandTimeoutSeconds: Int = 30
var commandTimeoutSeconds: Int = 30,
var sessionTtlDays: Int = 30
) {
fun toSettingsState(): SettingsState = SettingsState(
providers = providers.toMutableList(),
Expand All @@ -35,6 +36,7 @@ data class SettingsFormState(
commandExecutionEnabled = commandExecutionEnabled,
commandWhitelist = commandWhitelist.toMutableList(),
commandTimeoutSeconds = commandTimeoutSeconds,
sessionTtlDays = sessionTtlDays,
)

companion object {
Expand All @@ -54,6 +56,7 @@ data class SettingsFormState(
commandExecutionEnabled = state.commandExecutionEnabled,
commandWhitelist = state.commandWhitelist.toMutableList(),
commandTimeoutSeconds = state.commandTimeoutSeconds,
sessionTtlDays = state.sessionTtlDays,
)
}
}
Expand Down
39 changes: 37 additions & 2 deletions src/main/kotlin/com/github/codeplangui/storage/SessionStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.security.MessageDigest
import java.time.Instant
import java.time.temporal.ChronoUnit

@Serializable
data class SessionData(
val threadId: String,
val messages: List<Message>
val messages: List<Message>,
val lastAccessedAt: Long = System.currentTimeMillis()
)

class SessionStore(private val projectId: String) {
Expand All @@ -41,7 +44,7 @@ class SessionStore(private val projectId: String) {

fun saveSession(threadId: String, messages: List<Message>) {
try {
val data = SessionData(threadId, messages)
val data = SessionData(threadId, messages, lastAccessedAt = System.currentTimeMillis())
val content = json.encodeToString(data)
Files.writeString(tempFile, content)
try {
Expand All @@ -66,6 +69,38 @@ class SessionStore(private val projectId: String) {
}
}

/**
* Evict sessions that haven't been accessed within [ttlDays] days.
* Scans all project session directories under the sessions root.
*/
fun evictExpiredSessions(ttlDays: Int) {
if (ttlDays <= 0) return
try {
val sessionsRoot = dataDir.parent
if (!Files.isDirectory(sessionsRoot)) return
val cutoff = Instant.now().minus(ttlDays.toLong(), ChronoUnit.DAYS)
Files.list(sessionsRoot).use { dirs ->
dirs.filter { Files.isDirectory(it) }.forEach { projectDir ->
val file = projectDir.resolve("session.json")
if (!Files.exists(file)) return@forEach
try {
val content = Files.readString(file)
val data = json.decodeFromString<SessionData>(content)
val lastAccess = Instant.ofEpochMilli(data.lastAccessedAt)
if (lastAccess.isBefore(cutoff)) {
Files.deleteIfExists(file)
logger.warn("Evicted expired session: $projectDir (last accessed $lastAccess)")
}
} catch (e: Exception) {
logger.warn("Failed to check session expiry for $projectDir", e)
}
}
}
} catch (e: Exception) {
logger.warn("Failed to evict expired sessions", e)
}
}

fun clearSession() {
try {
Files.deleteIfExists(sessionFile)
Expand Down
Loading