summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-11-28 21:13:13 -0500
committerDax Raad <[email protected]>2025-11-28 21:13:13 -0500
commit204a31b6bbad951e1e03d985c2edc62165062a4e (patch)
treeb386540eda1ebe6bd679d12f888cf02f181c4435
parent813d287a09b3bdffecdb7e79226b39b4cb8ee211 (diff)
parent4dd9f33ebaa887f906387a2fb8700beff170648f (diff)
downloadopencode-204a31b6bbad951e1e03d985c2edc62165062a4e.tar.gz
opencode-204a31b6bbad951e1e03d985c2edc62165062a4e.zip
Merge remote-tracking branch 'origin/dev' into dev
-rw-r--r--bun.lock24
-rw-r--r--flake.lock6
-rw-r--r--nix/hashes.json2
-rw-r--r--packages/desktop/index.html3
-rw-r--r--packages/enterprise/src/entry-server.tsx3
-rw-r--r--packages/opencode/package.json6
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/session/index.tsx156
-rw-r--r--packages/ui/src/components/diff.tsx2
8 files changed, 67 insertions, 135 deletions
diff --git a/bun.lock b/bun.lock
index 12349e37c..a71653d29 100644
--- a/bun.lock
+++ b/bun.lock
@@ -236,8 +236,8 @@
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.2.8",
- "@opentui/core": "0.1.50",
- "@opentui/solid": "0.1.50",
+ "@opentui/core": "0.1.51",
+ "@opentui/solid": "0.1.51",
"@parcel/watcher": "2.5.1",
"@pierre/precision-diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
@@ -256,7 +256,7 @@
"jsonc-parser": "3.3.1",
"minimatch": "10.0.3",
"open": "10.1.2",
- "opentui-spinner": "0.0.5",
+ "opentui-spinner": "0.0.6",
"partial-json": "0.1.7",
"remeda": "catalog:",
"solid-js": "catalog:",
@@ -1085,21 +1085,21 @@
"@opentelemetry/api": ["@opentelemetry/[email protected]", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
- "@opentui/core": ["@opentui/[email protected]", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.50", "@opentui/core-darwin-x64": "0.1.50", "@opentui/core-linux-arm64": "0.1.50", "@opentui/core-linux-x64": "0.1.50", "@opentui/core-win32-arm64": "0.1.50", "@opentui/core-win32-x64": "0.1.50", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-QhjwT2f8AIQj0gbL/WQ2M93sl2/qp9+Kqxyh4dOhp8z3qnTc5D7J105VrMyeWZW7/P27ubgbFAqqWXrZ4FsuLw=="],
+ "@opentui/core": ["@opentui/[email protected]", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.51", "@opentui/core-darwin-x64": "0.1.51", "@opentui/core-linux-arm64": "0.1.51", "@opentui/core-linux-x64": "0.1.51", "@opentui/core-win32-arm64": "0.1.51", "@opentui/core-win32-x64": "0.1.51", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-9w9vg2nYC4eTKdh5en7WpBB44Nrib3uMtcPNXr2JxftjzDXU5Qmcv3vbKbxHqgzgk7FYm2Z9OFAOk7innbnplA=="],
- "@opentui/core-darwin-arm64": ["@opentui/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FKqTDOsZl9TXF7KN2SdZKoRHQNvqKSY27AG3jhKCoiyLGdaNCAsaeBWqAmpnL4E4kMkV3aiQSCrKTrYsaevvOg=="],
+ "@opentui/core-darwin-arm64": ["@opentui/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5EicEs5JQiMmr3rzKdGfHnsRXJ7cv/pxz0/C2Xcg+jKMSUmUvS7LE3Mi3HBY05GjZyB/EZ3bYyHqXB4nanYTNg=="],
- "@opentui/core-darwin-x64": ["@opentui/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-GczVNqqpM/HtsgeBB08K6zL1B7oc6Y5G2cMklo06LrYRdDkFdDtY5fNNnJR2/psZWzTrI3M+sLnKWgUGD5CxUQ=="],
+ "@opentui/core-darwin-x64": ["@opentui/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-mG9WlKdv0yWCIldFEAzgnm1NCyOtfiVG8zVZLiGaLhsL7Jii+f4RpOmlvFlb2sSSTxjC3HxRzWPC7WBSzk4txA=="],
- "@opentui/core-linux-arm64": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-+CKMhweEXH0tLGM6qqaqk6DyCEmwrTVubTtez/pSM3GgcROSXIBui9TEZpIlPgSCVmjbotGS6eSIg4oU+p9o7w=="],
+ "@opentui/core-linux-arm64": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-CNUx8nvkKRCsoLg/z7W8tD0hBFEUE9yEpqgyACc/ODdaaRLrUPQkBgUYWM/XGV9OnoqnpqLSKf95+DoUaTZ0lA=="],
- "@opentui/core-linux-x64": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-yv5KWiMohAK9bsi1gth9DDZDpoJA1EDHexjhThsPT8EH82g13T088dnJZuJWUE9dr1OwTCQG8DyorNxX3ViEGQ=="],
+ "@opentui/core-linux-x64": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-GpZ8vqX2dPyvBKvCRkBJp5x7zsmbixNCsCfzSVG9VIAVyT6qhylT2wrTOTGJLzDfc12ugdeL/1WUIR80YvwARA=="],
- "@opentui/core-win32-arm64": ["@opentui/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-6/6pURTRNTLFKF8IhYVi7U+T/HGMeURav9LIYw7yfcOibd0kLMthmemhS0Lzyk5dmtp0T4V4NmRmtlq/fIzyjQ=="],
+ "@opentui/core-win32-arm64": ["@opentui/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-eIc13B9dmoJ2x0EEsd4JWKCLzgVPBv+6Gn6vbjaSL9T/+14XRKMQuEdnQmU6+/KruAumfK+qSXlFdmjhFKrDOA=="],
- "@opentui/core-win32-x64": ["@opentui/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-EME8GBFq9uCLbH5js8fH7/xY4ZtLIZlt3bkYKT6lPiCNdaf/6ebg+F/ObPXFkJrc8VeV1ql2bXhQ6RLi7izvAA=="],
+ "@opentui/core-win32-x64": ["@opentui/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-ytpWgA3oNLehI5s9pxLAtg5kVO7Npq6onGPTZUikM5LlH71bX3Bm4PP8qLkD0AG9zDiO7ndp93ZFuT9yGqKPiQ=="],
- "@opentui/solid": ["@opentui/[email protected]", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.50", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-q778kp/eksh8UOPSQO2h8h9CGGDqepTf9u2WYTS2HYHRAI2SRtUWpN9L7Euyt3BtG9L/wpsIOHK/ufPhQH1X6A=="],
+ "@opentui/solid": ["@opentui/[email protected]", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.51", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-CBohbgFjUVG2P6/iAN52OCGaK0s5Wc2VWKyNrs7Fd9mFDMrC/IfrGAeaDeJXdQ8T0YhZ0PiUVijzFILCi2yOUw=="],
"@oslojs/asn1": ["@oslojs/[email protected]", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@@ -2969,7 +2969,7 @@
"openid-client": ["[email protected]", "", { "dependencies": { "jose": "^4.15.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA=="],
- "opentui-spinner": ["[email protected]", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.1.49", "@opentui/react": "^0.1.49", "@opentui/solid": "^0.1.49", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-abSWzWA7eyuD0PjerAWbBznLmOQn+8xRDaLGCVIs4ctETi2laNFr5KwicYnPXsHZpPc2neV7WtQm+diCEfOhLA=="],
+ "opentui-spinner": ["[email protected]", "", { "dependencies": { "cli-spinners": "^3.3.0" }, "peerDependencies": { "@opentui/core": "^0.1.49", "@opentui/react": "^0.1.49", "@opentui/solid": "^0.1.49", "typescript": "^5" }, "optionalPeers": ["@opentui/react", "@opentui/solid"] }, "sha512-xupLOeVQEAXEvVJCvHkfX6fChDWmJIPHe5jyUrVb8+n4XVTX8mBNhitFfB9v2ZbkC1H2UwPab/ElePHoW37NcA=="],
"own-keys": ["[email protected]", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
diff --git a/flake.lock b/flake.lock
index b67bb39a7..33aae3812 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
- "lastModified": 1764230294,
- "narHash": "sha256-Z63xl5Scj3Y/zRBPAWq1eT68n2wBWGCIEF4waZ0bQBE=",
+ "lastModified": 1764290847,
+ "narHash": "sha256-VwPgoDgnd628GdE3KyLqTyPF1WWh0VwT5UoKygoi8sg=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "0d59e0290eefe0f12512043842d7096c4070f30e",
+ "rev": "cd5fedfc384cb98d9fd3827b55f4522f49efda42",
"type": "github"
},
"original": {
diff --git a/nix/hashes.json b/nix/hashes.json
index 39148a4c9..1a2d21588 100644
--- a/nix/hashes.json
+++ b/nix/hashes.json
@@ -1,3 +1,3 @@
{
- "nodeModules": "sha256-/z9CLf4nR7FK56erTRpcexoageOloKX6tcrJtOCXjF4="
+ "nodeModules": "sha256-4OrnnZy44edgShLMUrYFsO+07gAnL//pGKO/ehWu5P4="
}
diff --git a/packages/desktop/index.html b/packages/desktop/index.html
index 6e67e6d47..0ac3d566d 100644
--- a/packages/desktop/index.html
+++ b/packages/desktop/index.html
@@ -9,7 +9,8 @@
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
- <meta name="theme-color" content="var(--background-base)" />
+ <meta name="theme-color" content="#F8F7F7" />
+ <meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
<meta property="og:image" content="/social-share.png" />
<meta property="twitter:image" content="/social-share.png" />
</head>
diff --git a/packages/enterprise/src/entry-server.tsx b/packages/enterprise/src/entry-server.tsx
index 85d69e2e0..68f4325c8 100644
--- a/packages/enterprise/src/entry-server.tsx
+++ b/packages/enterprise/src/entry-server.tsx
@@ -9,7 +9,8 @@ export default createHandler(() => (
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OpenCode</title>
- <meta name="theme-color" content="var(--background-base)" />
+ <meta name="theme-color" content="#F8F7F7" />
+ <meta name="theme-color" content="#131010" media="(prefers-color-scheme: dark)" />
<meta property="og:image" content="/social-share.png" />
<meta property="twitter:image" content="/social-share.png" />
{assets}
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index 968e9539b..e8e79f36f 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -62,8 +62,8 @@
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.2.8",
- "@opentui/core": "0.1.50",
- "@opentui/solid": "0.1.50",
+ "@opentui/core": "0.1.51",
+ "@opentui/solid": "0.1.51",
"@parcel/watcher": "2.5.1",
"@pierre/precision-diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
@@ -82,7 +82,7 @@
"jsonc-parser": "3.3.1",
"minimatch": "10.0.3",
"open": "10.1.2",
- "opentui-spinner": "0.0.5",
+ "opentui-spinner": "0.0.6",
"partial-json": "0.1.7",
"remeda": "catalog:",
"solid-js": "catalog:",
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
index 3d14db149..fb0e07a86 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -81,6 +81,7 @@ const context = createContext<{
conceal: () => boolean
showThinking: () => boolean
showTimestamps: () => boolean
+ diffWrapMode: () => "word" | "none"
sync: ReturnType<typeof useSync>
}>()
@@ -113,6 +114,7 @@ export function Session() {
const [conceal, setConceal] = createSignal(true)
const [showThinking, setShowThinking] = createSignal(kv.get("thinking_visibility", true))
const [showTimestamps, setShowTimestamps] = createSignal(kv.get("timestamps", "hide") === "show")
+ const [diffWrapMode, setDiffWrapMode] = createSignal<"word" | "none">("word")
const wide = createMemo(() => dimensions().width > 120)
const sidebarVisible = createMemo(() => {
@@ -443,6 +445,15 @@ export function Session() {
},
},
{
+ title: "Toggle diff wrapping",
+ value: "session.toggle.diffwrap",
+ category: "Session",
+ onSelect: (dialog) => {
+ setDiffWrapMode((prev) => (prev === "word" ? "none" : "word"))
+ dialog.clear()
+ },
+ },
+ {
title: "Page up",
value: "session.page.up",
keybind: "messages_page_up",
@@ -743,6 +754,7 @@ export function Session() {
conceal,
showThinking,
showTimestamps,
+ diffWrapMode,
sync,
}}
>
@@ -1302,21 +1314,9 @@ ToolRegistry.register<typeof WriteTool>({
container: "block",
render(props) {
const { theme, syntax } = useTheme()
- const lines = createMemo(
- () => (typeof props.input.content === "string" ? props.input.content.split("\n") : []),
- [] as string[],
- )
const code = createMemo(() => {
if (!props.input.content) return ""
- const text = props.input.content
- return text
- })
-
- const numbers = createMemo(() => {
- const pad = lines().length.toString().length
- return lines()
- .map((_, index) => index + 1)
- .map((x) => x.toString().padStart(pad, " "))
+ return props.input.content
})
const diagnostics = createMemo(() => props.metadata.diagnostics?.[props.input.filePath ?? ""] ?? [])
@@ -1326,14 +1326,9 @@ ToolRegistry.register<typeof WriteTool>({
<ToolTitle icon="←" fallback="Preparing write..." when={props.input.filePath}>
Wrote {props.input.filePath}
</ToolTitle>
- <box flexDirection="row">
- <box flexShrink={0}>
- <For each={numbers()}>{(value) => <text style={{ fg: theme.textMuted }}>{value}</text>}</For>
- </box>
- <box paddingLeft={1} flexGrow={1}>
- <code fg={theme.text} filetype={filetype(props.input.filePath!)} syntaxStyle={syntax()} content={code()} />
- </box>
- </box>
+ <line_number fg={theme.textMuted} minWidth={3} paddingRight={1}>
+ <code fg={theme.text} filetype={filetype(props.input.filePath!)} syntaxStyle={syntax()} content={code()} />
+ </line_number>
<Show when={diagnostics().length}>
<For each={diagnostics()}>
{(diagnostic) => (
@@ -1475,84 +1470,17 @@ ToolRegistry.register<typeof EditTool>({
const ctx = use()
const { theme, syntax } = useTheme()
- const style = createMemo(() => {
+ const view = createMemo(() => {
const diffStyle = ctx.sync.data.config.tui?.diff_style
- if (diffStyle === "stacked") return "stacked"
+ if (diffStyle === "stacked") return "unified"
// Default to "auto" behavior
- return ctx.width > 120 ? "split" : "stacked"
- })
-
- const diff = createMemo(() => {
- const diff = props.metadata.diff ?? props.permission["diff"]
- if (!diff) return null
-
- try {
- const patches = parsePatch(diff)
- if (patches.length === 0) return null
-
- const patch = patches[0]
- const oldLines: string[] = []
- const newLines: string[] = []
-
- for (const hunk of patch.hunks) {
- let i = 0
- while (i < hunk.lines.length) {
- const line = hunk.lines[i]
-
- if (line.startsWith("-")) {
- const removedLines: string[] = []
- while (i < hunk.lines.length && hunk.lines[i].startsWith("-")) {
- removedLines.push("- " + hunk.lines[i].slice(1))
- i++
- }
-
- const addedLines: string[] = []
- while (i < hunk.lines.length && hunk.lines[i].startsWith("+")) {
- addedLines.push("+ " + hunk.lines[i].slice(1))
- i++
- }
-
- const maxLen = Math.max(removedLines.length, addedLines.length)
- for (let j = 0; j < maxLen; j++) {
- oldLines.push(removedLines[j] ?? "")
- newLines.push(addedLines[j] ?? "")
- }
- } else if (line.startsWith("+")) {
- const addedLines: string[] = []
- while (i < hunk.lines.length && hunk.lines[i].startsWith("+")) {
- addedLines.push("+ " + hunk.lines[i].slice(1))
- i++
- }
-
- for (const added of addedLines) {
- oldLines.push("")
- newLines.push(added)
- }
- } else {
- oldLines.push(" " + line.slice(1))
- newLines.push(" " + line.slice(1))
- i++
- }
- }
- }
-
- return {
- oldContent: oldLines.join("\n"),
- newContent: newLines.join("\n"),
- }
- } catch (error) {
- return null
- }
- })
-
- const code = createMemo(() => {
- if (!props.metadata.diff) return ""
- const text = props.metadata.diff.split("\n").slice(5).join("\n")
- return text.trim()
+ return ctx.width > 120 ? "split" : "unified"
})
const ft = createMemo(() => filetype(props.input.filePath))
+ const diffContent = createMemo(() => props.metadata.diff ?? props.permission["diff"])
+
const diagnostics = createMemo(() => {
const arr = props.metadata.diagnostics?.[props.input.filePath ?? ""] ?? []
return arr.filter((x) => x.severity === 1).slice(0, 3)
@@ -1566,26 +1494,28 @@ ToolRegistry.register<typeof EditTool>({
replaceAll: props.input.replaceAll,
})}
</ToolTitle>
- <Switch>
- <Match when={props.permission["diff"]}>
- <text fg={theme.text}>{props.permission["diff"]?.trim()}</text>
- </Match>
- <Match when={diff() && style() === "split"}>
- <box paddingLeft={1} flexDirection="row" gap={2}>
- <box flexGrow={1} flexBasis={0}>
- <code fg={theme.text} filetype={ft()} syntaxStyle={syntax()} content={diff()!.oldContent} />
- </box>
- <box flexGrow={1} flexBasis={0}>
- <code fg={theme.text} filetype={ft()} syntaxStyle={syntax()} content={diff()!.newContent} />
- </box>
- </box>
- </Match>
- <Match when={code()}>
- <box paddingLeft={1}>
- <code fg={theme.text} filetype={ft()} syntaxStyle={syntax()} content={code()} />
- </box>
- </Match>
- </Switch>
+ <Show when={diffContent()}>
+ <box paddingLeft={1}>
+ <diff
+ diff={diffContent()}
+ view={view()}
+ filetype={ft()}
+ syntaxStyle={syntax()}
+ showLineNumbers={true}
+ width="100%"
+ wrapMode={ctx.diffWrapMode()}
+ addedBg={theme.diffAddedBg}
+ removedBg={theme.diffRemovedBg}
+ contextBg={theme.diffContextBg}
+ addedSignColor={theme.diffHighlightAdded}
+ removedSignColor={theme.diffHighlightRemoved}
+ lineNumberFg={theme.diffLineNumber}
+ lineNumberBg={theme.diffContextBg}
+ addedLineNumberBg={theme.diffAddedLineNumberBg}
+ removedLineNumberBg={theme.diffRemovedLineNumberBg}
+ />
+ </box>
+ </Show>
<Show when={diagnostics().length}>
<box>
<For each={diagnostics()}>
diff --git a/packages/ui/src/components/diff.tsx b/packages/ui/src/components/diff.tsx
index c895a6e29..bd2134515 100644
--- a/packages/ui/src/components/diff.tsx
+++ b/packages/ui/src/components/diff.tsx
@@ -46,7 +46,7 @@ export function Diff<T>(props: DiffProps<T>) {
})
onMount(() => {
- if (isServer) return
+ if (isServer || !props.preloadedDiff) return
fileDiffInstance = new FileDiff<T>({
...createDefaultOptions(props.diffStyle),
...others,