Skip to content

Commit a649933

Browse files
Merge remote-tracking branch 'upstream/main'
2 parents d335ac1 + afd5e5e commit a649933

3 files changed

Lines changed: 158 additions & 5 deletions

File tree

extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/browser/ContinueBrowser.kt

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.intellij.ui.jcef.*
1313
import org.cef.CefApp
1414
import org.cef.browser.CefBrowser
1515
import org.cef.handler.CefLoadHandlerAdapter
16+
import java.util.Base64
1617
import javax.swing.JComponent
1718

1819
class ContinueBrowser(
@@ -90,14 +91,39 @@ class ContinueBrowser(
9091
return
9192
}
9293
val json = gsonService.gson.toJson(BrowserMessage(messageType, messageId, data))
93-
val jsCode = """window.postMessage($json, "*");"""
9494
try {
95-
browser.cefBrowser.executeJavaScript(jsCode, getGuiUrl(), 0)
95+
if (json.length <= CHUNKED_MESSAGE_THRESHOLD) {
96+
browser.cefBrowser.executeJavaScript(
97+
"""window.postMessage($json, "*");""", getGuiUrl(), 0
98+
)
99+
} else {
100+
sendChunked(json, messageId)
101+
}
96102
} catch (error: Exception) {
97103
log.warn(error)
98104
}
99105
}
100106

107+
// Base64-encode and send in 512KB chunks to avoid JCEF freezing on large JS source strings
108+
private fun sendChunked(json: String, bufferId: String) {
109+
val scripts = buildChunkScripts(json, bufferId)
110+
val url = getGuiUrl()
111+
112+
browser.cefBrowser.executeJavaScript(scripts.init, url, 0)
113+
114+
try {
115+
for (chunkScript in scripts.chunks) {
116+
browser.cefBrowser.executeJavaScript(chunkScript, url, 0)
117+
}
118+
browser.cefBrowser.executeJavaScript(scripts.finalize, url, 0)
119+
} catch (e: Exception) {
120+
try {
121+
browser.cefBrowser.executeJavaScript(scripts.cleanup, url, 0)
122+
} catch (_: Exception) {}
123+
throw e
124+
}
125+
}
126+
101127
private fun executeJavaScript(myJSQueryOpenInBrowser: JBCefJSQuery) {
102128
val script = """
103129
window.postIntellijMessage = function(messageType, data, messageId) {
@@ -134,11 +160,40 @@ class ContinueBrowser(
134160
}
135161
}
136162

137-
private companion object {
163+
internal data class ChunkScripts(
164+
val init: String,
165+
val chunks: List<String>,
166+
val finalize: String,
167+
val cleanup: String,
168+
)
169+
170+
internal companion object {
171+
internal const val CHUNKED_MESSAGE_THRESHOLD = 1 * 1024 * 1024 // 1MB
172+
internal const val CHUNK_SIZE = 2 * 1024 * 1024 // 2MB
138173

139174
private fun getGuiUrl() =
140175
System.getenv("GUI_URL") ?: "http://continue/index.html"
141176

177+
internal fun buildChunkScripts(json: String, bufferId: String, chunkSize: Int = CHUNK_SIZE): ChunkScripts {
178+
val encoded = Base64.getEncoder().encodeToString(json.toByteArray(Charsets.UTF_8))
179+
val chunks = mutableListOf<String>()
180+
181+
var offset = 0
182+
while (offset < encoded.length) {
183+
val end = minOf(offset + chunkSize, encoded.length)
184+
val chunk = encoded.substring(offset, end)
185+
chunks.add("""window.__cc["$bufferId"].push("$chunk");""")
186+
offset = end
187+
}
188+
189+
return ChunkScripts(
190+
init = """window.__cc=window.__cc||{};window.__cc["$bufferId"]=[];""",
191+
chunks = chunks,
192+
finalize = """try{var b=atob(window.__cc["$bufferId"].join(""));window.postMessage(JSON.parse(new TextDecoder().decode(Uint8Array.from(b,function(c){return c.charCodeAt(0)}))),"*")}
193+
finally{delete window.__cc["$bufferId"]}""",
194+
cleanup = """delete window.__cc["$bufferId"];""",
195+
)
196+
}
142197
}
143198

144199
}

extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/continue/CoreMessenger.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,10 @@ class CoreMessenger(
6565
responseListeners[messageId]?.let { listener ->
6666
listener(data)
6767
@Suppress("UNCHECKED_CAST")
68-
val done = (data as? Map<String, Boolean>)?.get("done")
68+
val done = (data as? Map<String, Any>)?.get("done") as? Boolean
6969

70-
if (done == true) {
70+
// Remove unless explicitly streaming (done == false)
71+
if (done != false) {
7172
responseListeners.remove(messageId)
7273
}
7374
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package com.github.continuedev.continueintellijextension.unit
2+
3+
import com.github.continuedev.continueintellijextension.browser.ContinueBrowser
4+
import com.github.continuedev.continueintellijextension.browser.ContinueBrowser.Companion.buildChunkScripts
5+
import junit.framework.TestCase
6+
import java.util.Base64
7+
8+
class ContinueBrowserChunkTest : TestCase() {
9+
10+
fun `test small message produces single chunk`() {
11+
val json = """{"messageType":"test","data":"hello"}"""
12+
val scripts = buildChunkScripts(json, "buf1")
13+
14+
assertEquals(1, scripts.chunks.size)
15+
assertReassemblesTo(json, scripts.chunks, "buf1")
16+
}
17+
18+
fun `test message splits into expected number of chunks`() {
19+
val json = """{"data":"${"x".repeat(2_000_000)}"}"""
20+
val chunkSize = ContinueBrowser.CHUNK_SIZE
21+
val encoded = Base64.getEncoder().encodeToString(json.toByteArray(Charsets.UTF_8))
22+
val expectedChunks = (encoded.length + chunkSize - 1) / chunkSize
23+
24+
val scripts = buildChunkScripts(json, "buf2")
25+
26+
assertEquals(expectedChunks, scripts.chunks.size)
27+
assertReassemblesTo(json, scripts.chunks, "buf2")
28+
}
29+
30+
fun `test custom chunk size`() {
31+
val json = """{"data":"${"a".repeat(100)}"}"""
32+
val scripts = buildChunkScripts(json, "buf3", chunkSize = 32)
33+
34+
assertTrue(scripts.chunks.size > 1)
35+
assertReassemblesTo(json, scripts.chunks, "buf3")
36+
}
37+
38+
fun `test init script creates array buffer`() {
39+
val scripts = buildChunkScripts("{}", "myId")
40+
assertEquals("""window.__cc=window.__cc||{};window.__cc["myId"]=[];""", scripts.init)
41+
}
42+
43+
fun `test finalize script joins and decodes`() {
44+
val scripts = buildChunkScripts("{}", "myId")
45+
assertTrue(scripts.finalize.contains("""window.__cc["myId"].join("")"""))
46+
assertTrue(scripts.finalize.contains("atob"))
47+
assertTrue(scripts.finalize.contains("TextDecoder"))
48+
assertTrue(scripts.finalize.contains("JSON.parse"))
49+
assertTrue(scripts.finalize.contains("""delete window.__cc["myId"]"""))
50+
}
51+
52+
fun `test cleanup script deletes buffer`() {
53+
val scripts = buildChunkScripts("{}", "myId")
54+
assertEquals("""delete window.__cc["myId"];""", scripts.cleanup)
55+
}
56+
57+
fun `test special characters survive base64 round-trip`() {
58+
val json = """{"data":"héllo wörld 日本語 emoji: 🎉 quotes: \"nested\""}"""
59+
val scripts = buildChunkScripts(json, "buf4", chunkSize = 32)
60+
61+
assertReassemblesTo(json, scripts.chunks, "buf4")
62+
}
63+
64+
fun `test exact chunk boundary`() {
65+
// Create a JSON string whose Base64 encoding is exactly 2x chunkSize
66+
val chunkSize = 64
67+
val target = chunkSize * 2
68+
// Base64 output is ceil(input/3)*4, so we need input = target * 3 / 4 bytes
69+
val payloadSize = target * 3 / 4
70+
val json = "x".repeat(payloadSize)
71+
val encoded = Base64.getEncoder().encodeToString(json.toByteArray(Charsets.UTF_8))
72+
73+
assertEquals(target, encoded.length)
74+
75+
val scripts = buildChunkScripts(json, "buf5", chunkSize = chunkSize)
76+
assertEquals(2, scripts.chunks.size)
77+
assertReassemblesTo(json, scripts.chunks, "buf5")
78+
}
79+
80+
/**
81+
* Simulates the JS-side reassembly: extract push payloads, join, base64-decode,
82+
* and verify the result matches the original JSON.
83+
*/
84+
private fun assertReassemblesTo(expectedJson: String, chunkScripts: List<String>, bufferId: String) {
85+
val pushPrefix = """window.__cc["$bufferId"].push(""""
86+
val pushSuffix = """");"""
87+
88+
val reassembled = chunkScripts.joinToString("") { script ->
89+
assertTrue("Chunk should use array push", script.startsWith(pushPrefix))
90+
assertTrue(script.endsWith(pushSuffix))
91+
script.removePrefix(pushPrefix).removeSuffix(pushSuffix)
92+
}
93+
94+
val decoded = String(Base64.getDecoder().decode(reassembled), Charsets.UTF_8)
95+
assertEquals(expectedJson, decoded)
96+
}
97+
}

0 commit comments

Comments
 (0)