Skip to content

Commit 924fc9e

Browse files
committed
wip(app): settings
1 parent df094a1 commit 924fc9e

9 files changed

Lines changed: 490 additions & 104 deletions

File tree

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
255255

256256
createEffect(() => {
257257
params.id
258-
editorRef.focus()
259258
if (params.id) return
260259
const interval = setInterval(() => {
261260
setStore("placeholder", (prev) => (prev + 1) % PLACEHOLDERS.length)
Lines changed: 146 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,153 @@
1-
import { Component } from "solid-js"
1+
import { Select } from "@opencode-ai/ui/select"
2+
import { showToast } from "@opencode-ai/ui/toast"
3+
import { Component, For, createMemo, type JSX } from "solid-js"
4+
import { useGlobalSync } from "@/context/global-sync"
5+
6+
type PermissionAction = "allow" | "ask" | "deny"
7+
8+
type PermissionObject = Record<string, PermissionAction>
9+
type PermissionValue = PermissionAction | PermissionObject | string[] | undefined
10+
type PermissionMap = Record<string, PermissionValue>
11+
12+
type PermissionItem = {
13+
id: string
14+
title: string
15+
description: string
16+
}
17+
18+
const ACTIONS: Array<{ value: PermissionAction; label: string }> = [
19+
{ value: "allow", label: "Allow" },
20+
{ value: "ask", label: "Ask" },
21+
{ value: "deny", label: "Deny" },
22+
]
23+
24+
const ITEMS: PermissionItem[] = [
25+
{ id: "read", title: "Read", description: "Reading a file (matches the file path)" },
26+
{ id: "edit", title: "Edit", description: "Modify files, including edits, writes, patches, and multi-edits" },
27+
{ id: "glob", title: "Glob", description: "Match files using glob patterns" },
28+
{ id: "grep", title: "Grep", description: "Search file contents using regular expressions" },
29+
{ id: "list", title: "List", description: "List files within a directory" },
30+
{ id: "bash", title: "Bash", description: "Run shell commands" },
31+
{ id: "task", title: "Task", description: "Launch sub-agents" },
32+
{ id: "skill", title: "Skill", description: "Load a skill by name" },
33+
{ id: "lsp", title: "LSP", description: "Run language server queries" },
34+
{ id: "todoread", title: "Todo Read", description: "Read the todo list" },
35+
{ id: "todowrite", title: "Todo Write", description: "Update the todo list" },
36+
{ id: "webfetch", title: "Web Fetch", description: "Fetch content from a URL" },
37+
{ id: "websearch", title: "Web Search", description: "Search the web" },
38+
{ id: "codesearch", title: "Code Search", description: "Search code on the web" },
39+
{ id: "external_directory", title: "External Directory", description: "Access files outside the project directory" },
40+
{ id: "doom_loop", title: "Doom Loop", description: "Detect repeated tool calls with identical input" },
41+
]
42+
43+
const VALID_ACTIONS = new Set<PermissionAction>(["allow", "ask", "deny"])
44+
45+
function toMap(value: unknown): PermissionMap {
46+
if (value && typeof value === "object" && !Array.isArray(value)) return value as PermissionMap
47+
48+
const action = getAction(value)
49+
if (action) return { "*": action }
50+
51+
return {}
52+
}
53+
54+
function getAction(value: unknown): PermissionAction | undefined {
55+
if (typeof value === "string" && VALID_ACTIONS.has(value as PermissionAction)) return value as PermissionAction
56+
return
57+
}
58+
59+
function getRuleDefault(value: unknown): PermissionAction | undefined {
60+
const action = getAction(value)
61+
if (action) return action
62+
63+
if (!value || typeof value !== "object" || Array.isArray(value)) return
64+
65+
return getAction((value as Record<string, unknown>)["*"])
66+
}
267

368
export const SettingsPermissions: Component = () => {
69+
const globalSync = useGlobalSync()
70+
71+
const permission = createMemo(() => {
72+
return toMap(globalSync.data.config.permission)
73+
})
74+
75+
const actionFor = (id: string): PermissionAction => {
76+
const value = permission()[id]
77+
const direct = getRuleDefault(value)
78+
if (direct) return direct
79+
80+
const wildcard = getRuleDefault(permission()["*"])
81+
if (wildcard) return wildcard
82+
83+
return "allow"
84+
}
85+
86+
const setPermission = async (id: string, action: PermissionAction) => {
87+
const before = globalSync.data.config.permission
88+
const map = toMap(before)
89+
const existing = map[id]
90+
91+
const nextValue =
92+
existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing, "*": action } : action
93+
94+
globalSync.set("config", "permission", { ...map, [id]: nextValue })
95+
globalSync.updateConfig({ permission: { [id]: nextValue } }).catch((err: unknown) => {
96+
globalSync.set("config", "permission", before)
97+
const message = err instanceof Error ? err.message : String(err)
98+
showToast({ title: "Failed to update permissions", description: message })
99+
})
100+
}
101+
102+
return (
103+
<div class="flex flex-col h-full overflow-y-auto no-scrollbar">
104+
<div class="sticky top-0 z-10 bg-background-base border-b border-border-weak-base">
105+
<div class="flex flex-col gap-1 p-8 max-w-[720px]">
106+
<h2 class="text-16-medium text-text-strong">Permissions</h2>
107+
<p class="text-14-regular text-text-weak">Control what tools the server can use by default.</p>
108+
</div>
109+
</div>
110+
111+
<div class="flex flex-col gap-6 p-8 pt-6 max-w-[720px]">
112+
<div class="flex flex-col gap-2">
113+
<h3 class="text-14-medium text-text-strong">Appearance</h3>
114+
<div class="border border-border-weak-base rounded-lg overflow-hidden">
115+
<For each={ITEMS}>
116+
{(item) => (
117+
<SettingsRow title={item.title} description={item.description}>
118+
<Select
119+
options={ACTIONS}
120+
current={ACTIONS.find((o) => o.value === actionFor(item.id))}
121+
value={(o) => o.value}
122+
label={(o) => o.label}
123+
onSelect={(option) => option && setPermission(item.id, option.value)}
124+
variant="secondary"
125+
size="small"
126+
/>
127+
</SettingsRow>
128+
)}
129+
</For>
130+
</div>
131+
</div>
132+
</div>
133+
</div>
134+
)
135+
}
136+
137+
interface SettingsRowProps {
138+
title: string
139+
description: string
140+
children: JSX.Element
141+
}
142+
143+
const SettingsRow: Component<SettingsRowProps> = (props) => {
4144
return (
5-
<div class="flex flex-col h-full overflow-y-auto">
6-
<div class="flex flex-col gap-6 p-6 max-w-[600px]">
7-
<h2 class="text-16-medium text-text-strong">Permissions</h2>
8-
<p class="text-14-regular text-text-weak">Permission settings will be configurable here.</p>
145+
<div class="flex items-center justify-between gap-4 px-4 py-3 border-b border-border-weak-base last:border-none">
146+
<div class="flex flex-col gap-0.5">
147+
<span class="text-14-medium text-text-strong">{props.title}</span>
148+
<span class="text-12-regular text-text-weak">{props.description}</span>
9149
</div>
150+
<div class="flex-shrink-0">{props.children}</div>
10151
</div>
11152
)
12153
}

0 commit comments

Comments
 (0)