Skip to content

Commit 29e3ba4

Browse files
author
Faruk Brbovic
committed
Merge remote-tracking branch 'upstream/dev' into dev
2 parents 2587815 + e5d0c63 commit 29e3ba4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+5413
-3116
lines changed

.github/workflows/pr-title.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: PR Title Validation
2+
3+
on:
4+
pull_request:
5+
types: [opened, edited, synchronize]
6+
7+
jobs:
8+
validate-title:
9+
if: |
10+
github.event.pull_request.user.login != 'actions-user' &&
11+
github.event.pull_request.user.login != 'opencode' &&
12+
github.event.pull_request.user.login != 'rekram1-node' &&
13+
github.event.pull_request.user.login != 'thdxr' &&
14+
github.event.pull_request.user.login != 'kommander' &&
15+
github.event.pull_request.user.login != 'jayair' &&
16+
github.event.pull_request.user.login != 'fwang' &&
17+
github.event.pull_request.user.login != 'adamdotdevin' &&
18+
github.event.pull_request.user.login != 'iamdavidhill' &&
19+
github.event.pull_request.user.login != 'opencode-agent[bot]'
20+
runs-on: ubuntu-latest
21+
permissions:
22+
pull-requests: write
23+
steps:
24+
- name: Validate PR title
25+
uses: actions/github-script@v7
26+
with:
27+
script: |
28+
const title = context.payload.pull_request.title;
29+
const validPrefixes = ['feat:', 'fix:', 'docs:', 'chore:', 'refactor:', 'test:'];
30+
const isValid = validPrefixes.some(prefix => title.startsWith(prefix));
31+
32+
if (!isValid) {
33+
const body = `👋 Thanks for opening this PR!
34+
35+
Your PR title \`${title}\` doesn't follow our conventional commit format.
36+
37+
Please update it to start with one of these prefixes:
38+
- \`feat:\` new feature or functionality
39+
- \`fix:\` bug fix
40+
- \`docs:\` documentation or README changes
41+
- \`chore:\` maintenance tasks, dependency updates, etc.
42+
- \`refactor:\` code refactoring without changing behavior
43+
- \`test:\` adding or updating tests
44+
45+
**Examples:**
46+
- \`docs: update contributing guidelines\`
47+
- \`fix: resolve crash on startup\`
48+
- \`feat: add dark mode support\`
49+
50+
See [CONTRIBUTING.md](../blob/dev/CONTRIBUTING.md#pr-titles) for more details.`;
51+
52+
await github.rest.issues.createComment({
53+
owner: context.repo.owner,
54+
repo: context.repo.repo,
55+
issue_number: context.payload.pull_request.number,
56+
body: body
57+
});
58+
59+
core.setFailed('PR title does not follow conventional commit format');
60+
} else {
61+
console.log('PR title is valid:', title);
62+
}

CONTRIBUTING.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,24 @@ With that said, you may want to try these methods, as they might work for you.
155155
- Avoid having verbose LLM generated PR descriptions
156156
- Before adding new functions or functionality, ensure that such behavior doesn't already exist elsewhere in the codebase.
157157

158+
### PR Titles
159+
160+
PR titles should follow conventional commit standards:
161+
162+
- `feat:` new feature or functionality
163+
- `fix:` bug fix
164+
- `docs:` documentation or README changes
165+
- `chore:` maintenance tasks, dependency updates, etc.
166+
- `refactor:` code refactoring without changing behavior
167+
- `test:` adding or updating tests
168+
169+
Examples:
170+
171+
- `docs: update contributing guidelines`
172+
- `fix: resolve crash on startup`
173+
- `feat: add dark mode support`
174+
- `chore: bump dependency versions`
175+
158176
### Style Preferences
159177

160178
These are not strictly enforced, they are just general guidelines:

STATS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,4 @@
194194
| 2026-01-05 | 1,738,171 (+65,515) | 1,353,043 (+13,160) | 3,091,214 (+78,675) |
195195
| 2026-01-06 | 1,960,988 (+222,817) | 1,377,377 (+24,334) | 3,338,365 (+247,151) |
196196
| 2026-01-07 | 2,123,239 (+162,251) | 1,398,648 (+21,271) | 3,521,887 (+183,522) |
197+
| 2026-01-08 | 2,272,630 (+149,391) | 1,432,480 (+33,832) | 3,705,110 (+183,223) |

STYLE_GUIDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
## Style Guide
22

33
- Try to keep things in one function unless composable or reusable
4-
- AVOID unnecessary destructuring of variables
4+
- AVOID unnecessary destructuring of variables. instead of doing `const { a, b }
5+
= obj` just reference it as obj.a and obj.b. this preserves context
56
- AVOID `try`/`catch` where possible
67
- AVOID `else` statements
78
- AVOID using `any` type

packages/desktop/src-tauri/src/lib.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use tauri::{
1515
};
1616
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
1717
use tauri_plugin_shell::ShellExt;
18+
use tauri_plugin_store::StoreExt;
1819
use tokio::net::TcpSocket;
1920

2021
use crate::window_customizer::PinchZoomDisablePlugin;
@@ -45,6 +46,65 @@ impl ServerState {
4546
struct LogState(Arc<Mutex<VecDeque<String>>>);
4647

4748
const MAX_LOG_ENTRIES: usize = 200;
49+
const GLOBAL_STORAGE: &str = "opencode.global.dat";
50+
51+
/// Check if a URL's origin matches any configured server in the store.
52+
/// Returns true if the URL should be allowed for internal navigation.
53+
fn is_allowed_server(app: &AppHandle, url: &tauri::Url) -> bool {
54+
// Always allow localhost and 127.0.0.1
55+
if let Some(host) = url.host_str() {
56+
if host == "localhost" || host == "127.0.0.1" {
57+
return true;
58+
}
59+
}
60+
61+
// Try to read the server list from the store
62+
let Ok(store) = app.store(GLOBAL_STORAGE) else {
63+
return false;
64+
};
65+
66+
let Some(server_data) = store.get("server") else {
67+
return false;
68+
};
69+
70+
// Parse the server list from the stored JSON
71+
let Some(list) = server_data.get("list").and_then(|v| v.as_array()) else {
72+
return false;
73+
};
74+
75+
// Get the origin of the navigation URL (scheme + host + port)
76+
let url_origin = format!(
77+
"{}://{}{}",
78+
url.scheme(),
79+
url.host_str().unwrap_or(""),
80+
url.port().map(|p| format!(":{}", p)).unwrap_or_default()
81+
);
82+
83+
// Check if any configured server matches the URL's origin
84+
for server in list {
85+
let Some(server_url) = server.as_str() else {
86+
continue;
87+
};
88+
89+
// Parse the server URL to extract its origin
90+
let Ok(parsed) = tauri::Url::parse(server_url) else {
91+
continue;
92+
};
93+
94+
let server_origin = format!(
95+
"{}://{}{}",
96+
parsed.scheme(),
97+
parsed.host_str().unwrap_or(""),
98+
parsed.port().map(|p| format!(":{}", p)).unwrap_or_default()
99+
);
100+
101+
if url_origin == server_origin {
102+
return true;
103+
}
104+
}
105+
106+
false
107+
}
48108

49109
#[tauri::command]
50110
fn kill_sidecar(app: AppHandle) {
@@ -236,13 +296,30 @@ pub fn run() {
236296
.unwrap_or(LogicalSize::new(1920, 1080));
237297

238298
// Create window immediately with serverReady = false
299+
let app_for_nav = app.clone();
239300
let mut window_builder =
240301
WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into()))
241302
.title("OpenCode")
242303
.inner_size(size.width as f64, size.height as f64)
243304
.decorations(true)
244305
.zoom_hotkeys_enabled(true)
245306
.disable_drag_drop_handler()
307+
.on_navigation(move |url| {
308+
// Allow internal navigation (tauri:// scheme)
309+
if url.scheme() == "tauri" {
310+
return true;
311+
}
312+
// Allow navigation to configured servers (localhost, 127.0.0.1, or remote)
313+
if is_allowed_server(&app_for_nav, url) {
314+
return true;
315+
}
316+
// Open external http/https URLs in default browser
317+
if url.scheme() == "http" || url.scheme() == "https" {
318+
let _ = app_for_nav.shell().open(url.as_str(), None);
319+
return false; // Cancel internal navigation
320+
}
321+
true
322+
})
246323
.initialization_script(format!(
247324
r#"
248325
window.__OPENCODE__ ??= {{}};

packages/opencode/src/agent/agent.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Provider } from "../provider/provider"
44
import { generateObject, type ModelMessage } from "ai"
55
import { SystemPrompt } from "../session/system"
66
import { Instance } from "../project/instance"
7+
import { Truncate } from "../tool/truncation"
78

89
import PROMPT_GENERATE from "./generate.txt"
910
import PROMPT_COMPACTION from "./prompt/compaction.txt"
@@ -46,7 +47,11 @@ export namespace Agent {
4647
const defaults = PermissionNext.fromConfig({
4748
"*": "allow",
4849
doom_loop: "ask",
49-
external_directory: "ask",
50+
external_directory: {
51+
"*": "ask",
52+
[Truncate.DIR]: "allow",
53+
},
54+
question: "deny",
5055
// mirrors github.com/github/gitignore Node.gitignore pattern for .env files
5156
read: {
5257
"*": "allow",
@@ -61,7 +66,13 @@ export namespace Agent {
6166
build: {
6267
name: "build",
6368
options: {},
64-
permission: PermissionNext.merge(defaults, user),
69+
permission: PermissionNext.merge(
70+
defaults,
71+
PermissionNext.fromConfig({
72+
question: "allow",
73+
}),
74+
user,
75+
),
6576
mode: "primary",
6677
native: true,
6778
},
@@ -71,6 +82,7 @@ export namespace Agent {
7182
permission: PermissionNext.merge(
7283
defaults,
7384
PermissionNext.fromConfig({
85+
question: "allow",
7486
edit: {
7587
"*": "deny",
7688
".opencode/plan/*.md": "allow",
@@ -110,6 +122,9 @@ export namespace Agent {
110122
websearch: "allow",
111123
codesearch: "allow",
112124
read: "allow",
125+
external_directory: {
126+
[Truncate.DIR]: "allow",
127+
},
113128
}),
114129
user,
115130
),
@@ -194,6 +209,21 @@ export namespace Agent {
194209
item.options = mergeDeep(item.options, value.options ?? {})
195210
item.permission = PermissionNext.merge(item.permission, PermissionNext.fromConfig(value.permission ?? {}))
196211
}
212+
213+
// Ensure Truncate.DIR is allowed unless explicitly configured
214+
for (const name in result) {
215+
const agent = result[name]
216+
const explicit = agent.permission.some(
217+
(r) => r.permission === "external_directory" && r.pattern === Truncate.DIR && r.action === "deny",
218+
)
219+
if (explicit) continue
220+
221+
result[name].permission = PermissionNext.merge(
222+
result[name].permission,
223+
PermissionNext.fromConfig({ external_directory: { [Truncate.DIR]: "allow" } }),
224+
)
225+
}
226+
197227
return result
198228
})
199229

packages/opencode/src/cli/cmd/debug/agent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { cmd } from "../cmd"
66

77
export const AgentCommand = cmd({
88
command: "agent <name>",
9+
describe: "show agent configuration details",
910
builder: (yargs) =>
1011
yargs.positional("name", {
1112
type: "string",

packages/opencode/src/cli/cmd/debug/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { cmd } from "../cmd"
55

66
export const ConfigCommand = cmd({
77
command: "config",
8+
describe: "show resolved configuration",
89
builder: (yargs) => yargs,
910
async handler() {
1011
await bootstrap(process.cwd(), async () => {

packages/opencode/src/cli/cmd/debug/file.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Ripgrep } from "@/file/ripgrep"
66

77
const FileSearchCommand = cmd({
88
command: "search <query>",
9+
describe: "search files by query",
910
builder: (yargs) =>
1011
yargs.positional("query", {
1112
type: "string",
@@ -22,6 +23,7 @@ const FileSearchCommand = cmd({
2223

2324
const FileReadCommand = cmd({
2425
command: "read <path>",
26+
describe: "read file contents as JSON",
2527
builder: (yargs) =>
2628
yargs.positional("path", {
2729
type: "string",
@@ -38,6 +40,7 @@ const FileReadCommand = cmd({
3840

3941
const FileStatusCommand = cmd({
4042
command: "status",
43+
describe: "show file status information",
4144
builder: (yargs) => yargs,
4245
async handler() {
4346
await bootstrap(process.cwd(), async () => {
@@ -49,6 +52,7 @@ const FileStatusCommand = cmd({
4952

5053
const FileListCommand = cmd({
5154
command: "list <path>",
55+
describe: "list files in a directory",
5256
builder: (yargs) =>
5357
yargs.positional("path", {
5458
type: "string",
@@ -65,6 +69,7 @@ const FileListCommand = cmd({
6569

6670
const FileTreeCommand = cmd({
6771
command: "tree [dir]",
72+
describe: "show directory tree",
6873
builder: (yargs) =>
6974
yargs.positional("dir", {
7075
type: "string",
@@ -79,6 +84,7 @@ const FileTreeCommand = cmd({
7984

8085
export const FileCommand = cmd({
8186
command: "file",
87+
describe: "file system debugging utilities",
8288
builder: (yargs) =>
8389
yargs
8490
.command(FileReadCommand)

packages/opencode/src/cli/cmd/debug/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { AgentCommand } from "./agent"
1212

1313
export const DebugCommand = cmd({
1414
command: "debug",
15+
describe: "debugging and troubleshooting tools",
1516
builder: (yargs) =>
1617
yargs
1718
.command(ConfigCommand)
@@ -25,6 +26,7 @@ export const DebugCommand = cmd({
2526
.command(PathsCommand)
2627
.command({
2728
command: "wait",
29+
describe: "wait indefinitely (for debugging)",
2830
async handler() {
2931
await bootstrap(process.cwd(), async () => {
3032
await new Promise((resolve) => setTimeout(resolve, 1_000 * 60 * 60 * 24))
@@ -37,6 +39,7 @@ export const DebugCommand = cmd({
3739

3840
const PathsCommand = cmd({
3941
command: "paths",
42+
describe: "show global paths (data, config, cache, state)",
4043
handler() {
4144
for (const [key, value] of Object.entries(Global.Path)) {
4245
console.log(key.padEnd(10), value)

0 commit comments

Comments
 (0)