Skip to content
This repository was archived by the owner on Feb 25, 2026. It is now read-only.

Commit c96b670

Browse files
authored
Merge pull request #440 from Kilo-Org/kevinvandijk/kilo-opencode-v1.1.65
OpenCode v1.1.65
2 parents 6e48aab + b15b554 commit c96b670

35 files changed

Lines changed: 882 additions & 572 deletions

packages/app/src/app.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function UiI18nBridge(props: ParentProps) {
5757

5858
declare global {
5959
interface Window {
60-
__KILO__?: { updaterEnabled?: boolean; serverPassword?: string; deepLinks?: string[] }
60+
__KILO__?: { updaterEnabled?: boolean; serverPassword?: string; deepLinks?: string[]; wsl?: boolean }
6161
}
6262
}
6363

packages/app/src/components/dialog-manage-models.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Dialog } from "@opencode-ai/ui/dialog"
22
import { List } from "@opencode-ai/ui/list"
33
import { Switch } from "@opencode-ai/ui/switch"
4+
import { Tooltip } from "@opencode-ai/ui/tooltip"
45
import { Button } from "@opencode-ai/ui/button"
56
import type { Component } from "solid-js"
67
import { useLocal } from "@/context/local"
@@ -18,6 +19,14 @@ export const DialogManageModels: Component = () => {
1819
dialog.show(() => <DialogSelectProvider />)
1920
}
2021
const providerRank = (id: string) => popularProviders.indexOf(id)
22+
const providerList = (providerID: string) => local.model.list().filter((x) => x.provider.id === providerID)
23+
const providerVisible = (providerID: string) =>
24+
providerList(providerID).every((x) => local.model.visible({ modelID: x.id, providerID: x.provider.id }))
25+
const setProviderVisibility = (providerID: string, checked: boolean) => {
26+
providerList(providerID).forEach((x) => {
27+
local.model.setVisibility({ modelID: x.id, providerID: x.provider.id }, checked)
28+
})
29+
}
2130

2231
return (
2332
<Dialog
@@ -36,7 +45,28 @@ export const DialogManageModels: Component = () => {
3645
items={local.model.list()}
3746
filterKeys={["provider.name", "name", "id"]}
3847
sortBy={(a, b) => a.name.localeCompare(b.name)}
39-
groupBy={(x) => x.provider.name}
48+
groupBy={(x) => x.provider.id}
49+
groupHeader={(group) => {
50+
const provider = group.items[0].provider
51+
return (
52+
<>
53+
<span>{provider.name}</span>
54+
<Tooltip
55+
placement="top"
56+
value={language.t("dialog.model.manage.provider.toggle", { provider: provider.name })}
57+
>
58+
<Switch
59+
class="-mr-1"
60+
checked={providerVisible(provider.id)}
61+
onChange={(checked) => setProviderVisibility(provider.id, checked)}
62+
hideLabel
63+
>
64+
{provider.name}
65+
</Switch>
66+
</Tooltip>
67+
</>
68+
)
69+
}}
4070
sortGroupsBy={(a, b) => {
4171
const aRank = providerRank(a.items[0].provider.id)
4272
const bRank = providerRank(b.items[0].provider.id)

packages/app/src/components/prompt-input.tsx

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@ import { useLanguage } from "@/context/language"
3838
import { usePlatform } from "@/context/platform"
3939
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
4040
import { createPromptAttachments, ACCEPTED_FILE_TYPES } from "./prompt-input/attachments"
41-
import { navigatePromptHistory, prependHistoryEntry, promptLength } from "./prompt-input/history"
41+
import {
42+
canNavigateHistoryAtCursor,
43+
navigatePromptHistory,
44+
prependHistoryEntry,
45+
promptLength,
46+
} from "./prompt-input/history"
4247
import { createPromptSubmit } from "./prompt-input/submit"
4348
import { PromptPopover, type AtOption, type SlashCommand } from "./prompt-input/slash-popover"
4449
import { PromptContextItems } from "./prompt-input/context-items"
@@ -473,10 +478,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
473478
const prev = node.previousSibling
474479
const next = node.nextSibling
475480
const prevIsBr = prev?.nodeType === Node.ELEMENT_NODE && (prev as HTMLElement).tagName === "BR"
476-
const nextIsBr = next?.nodeType === Node.ELEMENT_NODE && (next as HTMLElement).tagName === "BR"
477-
if (!prevIsBr && !nextIsBr) return false
478-
if (nextIsBr && !prevIsBr && prev) return false
479-
return true
481+
return !!prevIsBr && !next
480482
}
481483
if (node.nodeType !== Node.ELEMENT_NODE) return false
482484
const el = node as HTMLElement
@@ -496,6 +498,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
496498
editorRef.appendChild(createPill(part))
497499
}
498500
}
501+
502+
const last = editorRef.lastChild
503+
if (last?.nodeType === Node.ELEMENT_NODE && (last as HTMLElement).tagName === "BR") {
504+
editorRef.appendChild(document.createTextNode("\u200B"))
505+
}
499506
}
500507

501508
createEffect(
@@ -729,7 +736,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
729736
}
730737
}
731738
if (last.nodeType !== Node.TEXT_NODE) {
732-
range.setStartAfter(last)
739+
const isBreak = last.nodeType === Node.ELEMENT_NODE && (last as HTMLElement).tagName === "BR"
740+
const next = last.nextSibling
741+
const emptyText = next?.nodeType === Node.TEXT_NODE && (next.textContent ?? "") === ""
742+
if (isBreak && (!next || emptyText)) {
743+
const placeholder = next && emptyText ? next : document.createTextNode("\u200B")
744+
if (!next) last.parentNode?.insertBefore(placeholder, null)
745+
placeholder.textContent = "\u200B"
746+
range.setStart(placeholder, 0)
747+
} else {
748+
range.setStartAfter(last)
749+
}
733750
}
734751
}
735752
range.collapse(true)
@@ -899,6 +916,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
899916
.current()
900917
.map((part) => ("content" in part ? part.content : ""))
901918
.join("")
919+
const direction = event.key === "ArrowUp" ? "up" : "down"
920+
if (!canNavigateHistoryAtCursor(direction, textContent, cursorPosition)) return
902921
const isEmpty = textContent.trim() === "" || textLength <= 1
903922
const hasNewlines = textContent.includes("\n")
904923
const inHistory = store.historyIndex >= 0
@@ -907,7 +926,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
907926
const allowUp = isEmpty || atStart || (!hasNewlines && !inHistory) || (inHistory && atEnd)
908927
const allowDown = isEmpty || atEnd || (!hasNewlines && !inHistory) || (inHistory && atStart)
909928

910-
if (event.key === "ArrowUp") {
929+
if (direction === "up") {
911930
if (!allowUp) return
912931
if (navigateHistory("up")) {
913932
event.preventDefault()

packages/app/src/components/prompt-input/editor-dom.test.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,26 @@ import { describe, expect, test } from "bun:test"
22
import { createTextFragment, getCursorPosition, getNodeLength, getTextLength, setCursorPosition } from "./editor-dom"
33

44
describe("prompt-input editor dom", () => {
5-
test("createTextFragment preserves newlines with br and zero-width placeholders", () => {
5+
test("createTextFragment preserves newlines with consecutive br nodes", () => {
66
const fragment = createTextFragment("foo\n\nbar")
77
const container = document.createElement("div")
88
container.appendChild(fragment)
99

10-
expect(container.childNodes.length).toBe(5)
10+
expect(container.childNodes.length).toBe(4)
11+
expect(container.childNodes[0]?.textContent).toBe("foo")
12+
expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR")
13+
expect((container.childNodes[2] as HTMLElement).tagName).toBe("BR")
14+
expect(container.childNodes[3]?.textContent).toBe("bar")
15+
})
16+
17+
test("createTextFragment keeps trailing newline as terminal break", () => {
18+
const fragment = createTextFragment("foo\n")
19+
const container = document.createElement("div")
20+
container.appendChild(fragment)
21+
22+
expect(container.childNodes.length).toBe(2)
1123
expect(container.childNodes[0]?.textContent).toBe("foo")
1224
expect((container.childNodes[1] as HTMLElement).tagName).toBe("BR")
13-
expect(container.childNodes[2]?.textContent).toBe("\u200B")
14-
expect((container.childNodes[3] as HTMLElement).tagName).toBe("BR")
15-
expect(container.childNodes[4]?.textContent).toBe("bar")
1625
})
1726

1827
test("length helpers treat breaks as one char and ignore zero-width chars", () => {
@@ -48,4 +57,21 @@ describe("prompt-input editor dom", () => {
4857

4958
container.remove()
5059
})
60+
61+
test("setCursorPosition and getCursorPosition round-trip across blank lines", () => {
62+
const container = document.createElement("div")
63+
container.appendChild(document.createTextNode("a"))
64+
container.appendChild(document.createElement("br"))
65+
container.appendChild(document.createElement("br"))
66+
container.appendChild(document.createTextNode("b"))
67+
document.body.appendChild(container)
68+
69+
setCursorPosition(container, 2)
70+
expect(getCursorPosition(container)).toBe(2)
71+
72+
setCursorPosition(container, 3)
73+
expect(getCursorPosition(container)).toBe(3)
74+
75+
container.remove()
76+
})
5177
})

packages/app/src/components/prompt-input/editor-dom.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ export function createTextFragment(content: string): DocumentFragment {
44
segments.forEach((segment, index) => {
55
if (segment) {
66
fragment.appendChild(document.createTextNode(segment))
7-
} else if (segments.length > 1) {
8-
fragment.appendChild(document.createTextNode("\u200B"))
97
}
108
if (index < segments.length - 1) {
119
fragment.appendChild(document.createElement("br"))

packages/app/src/components/prompt-input/history.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { describe, expect, test } from "bun:test"
22
import type { Prompt } from "@/context/prompt"
3-
import { clonePromptParts, navigatePromptHistory, prependHistoryEntry, promptLength } from "./history"
3+
import {
4+
canNavigateHistoryAtCursor,
5+
clonePromptParts,
6+
navigatePromptHistory,
7+
prependHistoryEntry,
8+
promptLength,
9+
} from "./history"
410

511
const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
612

@@ -66,4 +72,20 @@ describe("prompt-input history", () => {
6672
if (original[1]?.type !== "file") throw new Error("expected file")
6773
expect(original[1].selection?.startLine).toBe(1)
6874
})
75+
76+
test("canNavigateHistoryAtCursor only allows multiline boundaries", () => {
77+
const value = "a\nb\nc"
78+
79+
expect(canNavigateHistoryAtCursor("up", value, 0)).toBe(true)
80+
expect(canNavigateHistoryAtCursor("down", value, 0)).toBe(false)
81+
82+
expect(canNavigateHistoryAtCursor("up", value, 2)).toBe(false)
83+
expect(canNavigateHistoryAtCursor("down", value, 2)).toBe(false)
84+
85+
expect(canNavigateHistoryAtCursor("up", value, 5)).toBe(false)
86+
expect(canNavigateHistoryAtCursor("down", value, 5)).toBe(true)
87+
88+
expect(canNavigateHistoryAtCursor("up", "abc", 1)).toBe(true)
89+
expect(canNavigateHistoryAtCursor("down", "abc", 1)).toBe(true)
90+
})
6991
})

packages/app/src/components/prompt-input/history.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
44

55
export const MAX_HISTORY = 100
66

7+
export function canNavigateHistoryAtCursor(direction: "up" | "down", text: string, cursor: number) {
8+
if (!text.includes("\n")) return true
9+
const position = Math.max(0, Math.min(cursor, text.length))
10+
if (direction === "up") return !text.slice(0, position).includes("\n")
11+
return !text.slice(position).includes("\n")
12+
}
13+
714
export function clonePromptParts(prompt: Prompt): Prompt {
815
return prompt.map((part) => {
916
if (part.type === "text") return { ...part }

packages/app/src/context/global-sdk.tsx

Lines changed: 46 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
4646
type Queued = { directory: string; payload: Event }
4747
const FLUSH_FRAME_MS = 16
4848
const STREAM_YIELD_MS = 8
49+
const RECONNECT_DELAY_MS = 250
4950

5051
let queue: Queued[] = []
5152
let buffer: Queued[] = []
@@ -91,50 +92,58 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
9192
}
9293

9394
let streamErrorLogged = false
95+
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))
9496

9597
void (async () => {
96-
const events = await eventSdk.global.event({
97-
onSseError: (error) => {
98-
if (streamErrorLogged) return
99-
streamErrorLogged = true
100-
console.error("[global-sdk] event stream error", {
101-
url: server.url,
102-
fetch: eventFetch ? "platform" : "webview",
103-
error,
98+
while (!abort.signal.aborted) {
99+
try {
100+
const events = await eventSdk.global.event({
101+
onSseError: (error) => {
102+
if (streamErrorLogged) return
103+
streamErrorLogged = true
104+
console.error("[global-sdk] event stream error", {
105+
url: server.url,
106+
fetch: eventFetch ? "platform" : "webview",
107+
error,
108+
})
109+
},
104110
})
105-
},
106-
})
107-
let yielded = Date.now()
108-
for await (const event of events.stream) {
109-
const directory = event.directory ?? "global"
110-
const payload = event.payload
111-
const k = key(directory, payload)
112-
if (k) {
113-
const i = coalesced.get(k)
114-
if (i !== undefined) {
115-
queue[i] = { directory, payload }
116-
continue
111+
let yielded = Date.now()
112+
for await (const event of events.stream) {
113+
streamErrorLogged = false
114+
const directory = event.directory ?? "global"
115+
const payload = event.payload
116+
const k = key(directory, payload)
117+
if (k) {
118+
const i = coalesced.get(k)
119+
if (i !== undefined) {
120+
queue[i] = { directory, payload }
121+
continue
122+
}
123+
coalesced.set(k, queue.length)
124+
}
125+
queue.push({ directory, payload })
126+
schedule()
127+
128+
if (Date.now() - yielded < STREAM_YIELD_MS) continue
129+
yielded = Date.now()
130+
await wait(0)
131+
}
132+
} catch (error) {
133+
if (!streamErrorLogged) {
134+
streamErrorLogged = true
135+
console.error("[global-sdk] event stream failed", {
136+
url: server.url,
137+
fetch: eventFetch ? "platform" : "webview",
138+
error,
139+
})
117140
}
118-
coalesced.set(k, queue.length)
119141
}
120-
queue.push({ directory, payload })
121-
schedule()
122142

123-
if (Date.now() - yielded < STREAM_YIELD_MS) continue
124-
yielded = Date.now()
125-
await new Promise<void>((resolve) => setTimeout(resolve, 0))
143+
if (abort.signal.aborted) return
144+
await wait(RECONNECT_DELAY_MS)
126145
}
127-
})()
128-
.finally(flush)
129-
.catch((error) => {
130-
if (streamErrorLogged) return
131-
streamErrorLogged = true
132-
console.error("[global-sdk] event stream failed", {
133-
url: server.url,
134-
fetch: eventFetch ? "platform" : "webview",
135-
error,
136-
})
137-
})
146+
})().finally(flush)
138147

139148
onCleanup(() => {
140149
abort.abort()
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { describe, expect, test } from "bun:test"
2+
import { createRoot, getOwner } from "solid-js"
3+
import { createStore } from "solid-js/store"
4+
import type { State } from "./types"
5+
import { createChildStoreManager } from "./child-store"
6+
7+
const child = () => createStore({} as State)
8+
9+
describe("createChildStoreManager", () => {
10+
test("does not evict the active directory during mark", () => {
11+
const owner = createRoot((dispose) => {
12+
const current = getOwner()
13+
dispose()
14+
return current
15+
})
16+
if (!owner) throw new Error("owner required")
17+
18+
const manager = createChildStoreManager({
19+
owner,
20+
markStats() {},
21+
incrementEvictions() {},
22+
isBooting: () => false,
23+
isLoadingSessions: () => false,
24+
onBootstrap() {},
25+
onDispose() {},
26+
})
27+
28+
Array.from({ length: 30 }, (_, index) => `/pinned-${index}`).forEach((directory) => {
29+
manager.children[directory] = child()
30+
manager.pin(directory)
31+
})
32+
33+
const directory = "/active"
34+
manager.children[directory] = child()
35+
manager.mark(directory)
36+
37+
expect(manager.children[directory]).toBeDefined()
38+
})
39+
})

0 commit comments

Comments
 (0)