Skip to content
Open
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
1 change: 1 addition & 0 deletions STATS.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,4 @@
| 2026-01-13 | 3,297,078 (+243,484) | 1,595,062 (+41,391) | 4,892,140 (+284,875) |
| 2026-01-14 | 3,568,928 (+271,850) | 1,645,362 (+50,300) | 5,214,290 (+322,150) |
| 2026-01-16 | 4,121,550 (+552,622) | 1,754,418 (+109,056) | 5,875,968 (+661,678) |
| 2026-01-17 | 4,389,558 (+268,008) | 1,805,315 (+50,897) | 6,194,873 (+318,905) |
2 changes: 2 additions & 0 deletions github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,10 @@ This will walk you through installing the GitHub app, creating the workflow, and
uses: anomalyco/opencode/github@latest
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
model: anthropic/claude-sonnet-4-20250514
use_github_token: true
```
3. Store the API keys in secrets. In your organization or project **settings**, expand **Secrets and variables** on the left and select **Actions**. Add the required API keys.
Expand Down
4 changes: 3 additions & 1 deletion packages/app/src/context/command.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createMemo, createSignal, onCleanup, onMount, type Accessor } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useDialog } from "@opencode-ai/ui/context/dialog"

const IS_MAC = typeof navigator === "object" && /(Mac|iPod|iPhone|iPad)/.test(navigator.platform)

Expand Down Expand Up @@ -122,6 +123,7 @@ export function formatKeybind(config: string): string {
export const { use: useCommand, provider: CommandProvider } = createSimpleContext({
name: "Command",
init: () => {
const dialog = useDialog()
const [registrations, setRegistrations] = createSignal<Accessor<CommandOption[]>[]>([])
const [suspendCount, setSuspendCount] = createSignal(0)

Expand Down Expand Up @@ -165,7 +167,7 @@ export const { use: useCommand, provider: CommandProvider } = createSimpleContex
}

const handleKeyDown = (event: KeyboardEvent) => {
if (suspended()) return
if (suspended() || dialog.active) return

const paletteKeybinds = parseKeybind("mod+shift+p")
if (matchKeybind(paletteKeybinds, event)) {
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/context/global-sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,8 @@ function createGlobalSync() {
}),
)
}
if (event.properties.info.parentID) break
setStore("sessionTotal", (value) => Math.max(0, value - 1))
break
}
if (result.found) {
Expand Down
12 changes: 6 additions & 6 deletions packages/app/src/pages/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,7 @@ export default function Layout(props: ParentProps) {
const [dirStore] = globalSync.child(dir)
const dirSessions = dirStore.session
.filter((session) => session.directory === dirStore.path.directory)
.filter((session) => !session.parentID)
.filter((session) => !session.parentID && !session.time?.archived)
.toSorted(sortSessions)
result.push(...dirSessions)
}
Expand All @@ -510,7 +510,7 @@ export default function Layout(props: ParentProps) {
const [projectStore] = globalSync.child(project.worktree)
return projectStore.session
.filter((session) => session.directory === projectStore.path.directory)
.filter((session) => !session.parentID)
.filter((session) => !session.parentID && !session.time?.archived)
.toSorted(sortSessions)
})

Expand Down Expand Up @@ -1203,7 +1203,7 @@ export default function Layout(props: ParentProps) {
const sessions = createMemo(() =>
workspaceStore.session
.filter((session) => session.directory === workspaceStore.path.directory)
.filter((session) => !session.parentID)
.filter((session) => !session.parentID && !session.time?.archived)
.toSorted(sortSessions),
)
const local = createMemo(() => props.directory === props.project.worktree)
Expand Down Expand Up @@ -1349,7 +1349,7 @@ export default function Layout(props: ParentProps) {
const [data] = globalSync.child(directory)
return data.session
.filter((session) => session.directory === data.path.directory)
.filter((session) => !session.parentID)
.filter((session) => !session.parentID && !session.time?.archived)
.toSorted(sortSessions)
.slice(0, 2)
}
Expand All @@ -1358,7 +1358,7 @@ export default function Layout(props: ParentProps) {
const [data] = globalSync.child(props.project.worktree)
return data.session
.filter((session) => session.directory === data.path.directory)
.filter((session) => !session.parentID)
.filter((session) => !session.parentID && !session.time?.archived)
.toSorted(sortSessions)
.slice(0, 2)
}
Expand Down Expand Up @@ -1445,7 +1445,7 @@ export default function Layout(props: ParentProps) {
const sessions = createMemo(() =>
workspaceStore.session
.filter((session) => session.directory === workspaceStore.path.directory)
.filter((session) => !session.parentID)
.filter((session) => !session.parentID && !session.time?.archived)
.toSorted(sortSessions),
)
const loading = createMemo(() => workspaceStore.status !== "complete" && sessions().length === 0)
Expand Down
12 changes: 12 additions & 0 deletions packages/desktop/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
)
}

const isWindows = ostype() === "windows"
if (isWindows) {
const originalGetComputedStyle = window.getComputedStyle
window.getComputedStyle = ((elt: Element, pseudoElt?: string | null) => {
if (!(elt instanceof Element)) {
// WebView2 can call into Floating UI with non-elements; fall back to a safe element.
return originalGetComputedStyle(document.documentElement, pseudoElt ?? undefined)
}
return originalGetComputedStyle(elt, pseudoElt ?? undefined)
}) as typeof window.getComputedStyle
}

let update: Update | null = null

const createPlatform = (password: Accessor<string | null>): Platform => ({
Expand Down
9 changes: 9 additions & 0 deletions packages/opencode/src/cli/cmd/tui/context/local.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,16 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})

const file = Bun.file(path.join(Global.Path.state, "model.json"))
const state = {
pending: false,
}

function save() {
if (!modelStore.ready) {
state.pending = true
return
}
state.pending = false
Bun.write(
file,
JSON.stringify({
Expand All @@ -135,6 +143,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
.catch(() => {})
.finally(() => {
setModelStore("ready", true)
if (state.pending) save()
})

const args = useArgs()
Expand Down
13 changes: 13 additions & 0 deletions packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ export function Session() {
const [showScrollbar, setShowScrollbar] = kv.signal("scrollbar_visible", false)
const [diffWrapMode, setDiffWrapMode] = createSignal<"word" | "none">("word")
const [animationsEnabled, setAnimationsEnabled] = kv.signal("animations_enabled", true)
const [compactionMethod, setCompactionMethod] = kv.signal<"standard" | "collapse">(
"compaction_method",
sync.data.config.compaction?.method ?? "standard",
)

const wide = createMemo(() => dimensions().width > 120)
const sidebarVisible = createMemo(() => {
Expand Down Expand Up @@ -392,6 +396,15 @@ export function Session() {
dialog.clear()
},
},
{
title: compactionMethod() === "collapse" ? "Use standard compaction" : "Use collapse compaction",
value: "session.toggle.compaction_method",
category: "Session",
onSelect: (dialog) => {
setCompactionMethod((prev) => (prev === "standard" ? "collapse" : "standard"))
dialog.clear()
},
},
{
title: "Unshare session",
value: "session.unshare",
Expand Down
6 changes: 6 additions & 0 deletions packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
<text fg={theme.text}>
<b>Context</b>
</text>
<text fg={theme.textMuted}>
compact{" "}
{sync.data.config.compaction?.auto === false
? "disabled"
: kv.get("compaction_method", sync.data.config.compaction?.method ?? "standard")}
</text>
<text fg={theme.textMuted}>{context()?.tokens ?? 0} tokens</text>
<text fg={theme.textMuted}>{context()?.percentage ?? 0}% used</text>
<text fg={theme.textMuted}>{cost()} spent</text>
Expand Down
6 changes: 5 additions & 1 deletion packages/opencode/src/cli/cmd/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ export const WebCommand = cmd({
}

if (opts.mdns) {
UI.println(UI.Style.TEXT_INFO_BOLD + " mDNS: ", UI.Style.TEXT_NORMAL, "opencode.local")
UI.println(
UI.Style.TEXT_INFO_BOLD + " mDNS: ",
UI.Style.TEXT_NORMAL,
`opencode.local:${server.port}`,
)
}

// Open localhost in browser
Expand Down
42 changes: 42 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,48 @@ export namespace Config {
.object({
auto: z.boolean().optional().describe("Enable automatic compaction when context is full (default: true)"),
prune: z.boolean().optional().describe("Enable pruning of old tool outputs (default: true)"),
method: z
.enum(["standard", "collapse"])
.optional()
.describe(
"Compaction method: 'standard' summarizes entire conversation, 'collapse' extracts oldest messages and creates summary at breakpoint (default: standard)",
),
trigger: z
.number()
.min(0)
.max(1)
.optional()
.describe("Trigger compaction at this fraction of total context (default: 0.85 = 85%)"),
extractRatio: z
.number()
.min(0)
.max(1)
.optional()
.describe("For collapse mode: fraction of oldest tokens to extract and summarize (default: 0.65)"),
recentRatio: z
.number()
.min(0)
.max(1)
.optional()
.describe("For collapse mode: fraction of newest tokens to use as reference context (default: 0.15)"),
summaryMaxTokens: z
.number()
.min(1000)
.max(50000)
.optional()
.describe("For collapse mode: target token count for the summary output (default: 10000)"),
previousSummaries: z
.number()
.min(0)
.max(10)
.optional()
.describe("For collapse mode: number of previous summaries to include for context merging (default: 3)"),
insertTriggers: z
.boolean()
.optional()
.describe(
"Whether to insert compaction trigger messages in the stream. Standard compaction needs triggers (default: true), collapse compaction does not (default: false)",
),
})
.optional(),
experimental: z
Expand Down
63 changes: 59 additions & 4 deletions packages/opencode/src/id/id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export namespace Identifier {
}

const LENGTH = 26
const TIME_BYTES = 6

// State for monotonic ID generation
let lastTimestamp = 0
Expand Down Expand Up @@ -65,12 +66,12 @@ export namespace Identifier {

now = descending ? ~now : now

const timeBytes = Buffer.alloc(6)
for (let i = 0; i < 6; i++) {
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
const timeBytes = Buffer.alloc(TIME_BYTES)
for (let i = 0; i < TIME_BYTES; i++) {
timeBytes[i] = Number((now >> BigInt((TIME_BYTES - 1 - i) * 8)) & BigInt(0xff))
}

return prefixes[prefix] + "_" + timeBytes.toString("hex") + randomBase62(LENGTH - 12)
return prefixes[prefix] + "_" + timeBytes.toString("hex") + randomBase62(LENGTH - TIME_BYTES * 2)
}

/** Extract timestamp from an ascending ID. Does not work with descending IDs. */
Expand All @@ -80,4 +81,58 @@ export namespace Identifier {
const encoded = BigInt("0x" + hex)
return Number(encoded / BigInt(0x1000))
}

/**
* Insert an ID that sorts after afterId, and optionally before beforeId.
*
* If beforeId is provided and there's a gap, the new ID will sort between them.
* Otherwise, the new ID will sort immediately after afterId.
*
* @param afterId - The ID that the new ID must sort AFTER
* @param beforeId - Optional ID that the new ID should sort BEFORE (if gap exists)
* @param prefix - The prefix for the new ID (e.g., "message", "part")
*/
export function insert(afterId: string, beforeId: string | undefined, prefix: keyof typeof prefixes): string {
const underscoreIndex = afterId.indexOf("_")
if (underscoreIndex === -1) {
throw new Error(`Invalid afterId: ${afterId}`)
}

const afterHex = afterId.slice(underscoreIndex + 1, underscoreIndex + 1 + TIME_BYTES * 2)
const afterValue = BigInt("0x" + afterHex)

let newValue: bigint

if (beforeId) {
const beforeUnderscoreIndex = beforeId.indexOf("_")
if (beforeUnderscoreIndex !== -1) {
const beforeHex = beforeId.slice(beforeUnderscoreIndex + 1, beforeUnderscoreIndex + 1 + TIME_BYTES * 2)
if (/^[0-9a-f]+$/i.test(beforeHex)) {
const beforeValue = BigInt("0x" + beforeHex)
const gap = beforeValue - afterValue
if (gap > BigInt(1)) {
// Insert in the middle of the gap
newValue = afterValue + gap / BigInt(2)
} else {
// Gap too small, create after afterId
newValue = afterValue + BigInt(0x1000) + BigInt(1)
}
} else {
newValue = afterValue + BigInt(0x1000) + BigInt(1)
}
} else {
newValue = afterValue + BigInt(0x1000) + BigInt(1)
}
} else {
// No beforeId, create after afterId
newValue = afterValue + BigInt(0x1000) + BigInt(1)
}

const timeBytes = Buffer.alloc(TIME_BYTES)
for (let i = 0; i < TIME_BYTES; i++) {
timeBytes[i] = Number((newValue >> BigInt((TIME_BYTES - 1 - i) * 8)) & BigInt(0xff))
}

return prefixes[prefix] + "_" + timeBytes.toString("hex") + randomBase62(LENGTH - TIME_BYTES * 2)
}
}
4 changes: 3 additions & 1 deletion packages/opencode/src/server/mdns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ export namespace MDNS {
let bonjour: Bonjour | undefined
let currentPort: number | undefined

export function publish(port: number, name = "opencode") {
export function publish(port: number) {
if (currentPort === port) return
if (bonjour) unpublish()

try {
const name = `opencode-${port}`
bonjour = new Bonjour()
const service = bonjour.publish({
name,
type: "http",
host: "opencode.local",
port,
txt: { path: "/" },
})
Expand Down
2 changes: 1 addition & 1 deletion packages/opencode/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ export namespace Server {
opts.hostname !== "localhost" &&
opts.hostname !== "::1"
if (shouldPublishMDNS) {
MDNS.publish(server.port!, `opencode-${server.port!}`)
MDNS.publish(server.port!)
} else if (opts.mdns) {
log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
}
Expand Down
Loading