opencode-tweaks 0.0.0-stage → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +222 -2
- package/package.json +62 -4
- package/src/index.ts +20 -0
- package/src/quota/format.ts +97 -0
- package/src/quota/pace.ts +180 -0
- package/src/quota/parse.ts +101 -0
- package/src/quota/service.ts +224 -0
- package/src/rpc.ts +84 -0
- package/src/tui.tsx +66 -0
- package/src/tweaks/menu-dialog.tsx +424 -0
- package/src/tweaks/menu-model.ts +55 -0
- package/src/tweaks/menu.ts +1 -0
- package/src/tweaks/quota-panel.tsx +514 -0
- package/src/tweaks/quota.ts +163 -0
- package/src/tweaks/registry.ts +10 -0
- package/src/tweaks/settings.ts +73 -0
- package/src/tweaks/token-inject.ts +724 -0
- package/src/tweaks/token-slots.tsx +327 -0
- package/src/tweaks/tokens-model.ts +213 -0
- package/src/tweaks/tokens.ts +113 -0
- package/src/tweaks/types.ts +187 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { QuotaWindowKey, QuotaWindowSnapshot } from "../rpc"
|
|
2
|
+
|
|
3
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function text(value: unknown): string | undefined {
|
|
8
|
+
return typeof value === "string" && value.length > 0 ? value : undefined
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function numeric(value: unknown): number | null {
|
|
12
|
+
const parsed =
|
|
13
|
+
typeof value === "string" ? Number(value) : typeof value === "number" ? value : Number.NaN
|
|
14
|
+
return Number.isFinite(parsed) ? parsed : null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resetIso(value: unknown): string | undefined {
|
|
18
|
+
const raw = text(value)
|
|
19
|
+
if (!raw) return undefined
|
|
20
|
+
|
|
21
|
+
const time = Date.parse(raw)
|
|
22
|
+
return Number.isFinite(time) ? new Date(time).toISOString() : undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function consoleWindow(
|
|
26
|
+
key: QuotaWindowKey,
|
|
27
|
+
value: unknown,
|
|
28
|
+
fallbackReset: string | undefined,
|
|
29
|
+
): QuotaWindowSnapshot | undefined {
|
|
30
|
+
if (!isRecord(value)) return undefined
|
|
31
|
+
|
|
32
|
+
const limit = numeric(value.limitMicroCents)
|
|
33
|
+
const used = numeric(value.usedMicroCents)
|
|
34
|
+
if (limit === null || used === null || limit < 0 || used < 0) return undefined
|
|
35
|
+
|
|
36
|
+
const usedShare =
|
|
37
|
+
limit === 0 ? (used > 0 ? 100 : 0) : Math.min(100, Math.max(0, (used / limit) * 100))
|
|
38
|
+
const percentUsed = Math.round(usedShare)
|
|
39
|
+
const resetsAt = resetIso(value.resetsAt) ?? resetIso(fallbackReset)
|
|
40
|
+
if (!resetsAt) return undefined
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
key,
|
|
44
|
+
percentRemaining: 100 - percentUsed,
|
|
45
|
+
percentRemainingExact: 100 - usedShare,
|
|
46
|
+
resetsAt,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function keyWindow(key: QuotaWindowKey, value: unknown): QuotaWindowSnapshot | undefined {
|
|
51
|
+
if (!isRecord(value)) return undefined
|
|
52
|
+
|
|
53
|
+
const percent = numeric(value.percent)
|
|
54
|
+
if (percent === null || percent < 0 || percent > 100) return undefined
|
|
55
|
+
|
|
56
|
+
const resetsAt = resetIso(value.resetsAt)
|
|
57
|
+
if (!resetsAt) return undefined
|
|
58
|
+
|
|
59
|
+
const remaining = value.status === "rate-limited" ? 0 : 100 - percent
|
|
60
|
+
return {
|
|
61
|
+
key,
|
|
62
|
+
percentRemaining: remaining,
|
|
63
|
+
percentRemainingExact: remaining,
|
|
64
|
+
resetsAt,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The Console `access.meters` response, in the order the sidebar shows. */
|
|
69
|
+
export function consoleWindows(
|
|
70
|
+
meters: Record<string, unknown>,
|
|
71
|
+
endsAt: string | undefined,
|
|
72
|
+
): QuotaWindowSnapshot[] {
|
|
73
|
+
const windows: QuotaWindowSnapshot[] = []
|
|
74
|
+
|
|
75
|
+
for (const [key, meter] of [
|
|
76
|
+
["5h", "fiveHour"],
|
|
77
|
+
["7d", "week"],
|
|
78
|
+
["30d", "month"],
|
|
79
|
+
] as const) {
|
|
80
|
+
const window = consoleWindow(key, meters[meter], endsAt)
|
|
81
|
+
if (window) windows.push(window)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return windows
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The Zen `/v1/usage` response, in the order the sidebar shows. */
|
|
88
|
+
export function keyWindows(usage: Record<string, unknown>): QuotaWindowSnapshot[] {
|
|
89
|
+
const windows: QuotaWindowSnapshot[] = []
|
|
90
|
+
|
|
91
|
+
for (const [key, meter] of [
|
|
92
|
+
["5h", "rolling"],
|
|
93
|
+
["7d", "weekly"],
|
|
94
|
+
["30d", "monthly"],
|
|
95
|
+
] as const) {
|
|
96
|
+
const window = keyWindow(key, usage[meter])
|
|
97
|
+
if (window) windows.push(window)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return windows
|
|
101
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode/plugin"
|
|
2
|
+
import type { QuotaSnapshot, QuotaWindowSnapshot } from "../rpc"
|
|
3
|
+
import { errorText } from "./format"
|
|
4
|
+
import { consoleWindows, isRecord, keyWindows, text } from "./parse"
|
|
5
|
+
|
|
6
|
+
const PROVIDER = "OpenCode Go"
|
|
7
|
+
const CONSOLE_URL = "https://opencode.ai/console"
|
|
8
|
+
const ZEN_USAGE_URL = "https://opencode.ai/zen/go/v1/usage"
|
|
9
|
+
const REQUEST_TIMEOUT_MS = 15_000
|
|
10
|
+
|
|
11
|
+
type ConsoleAuth = {
|
|
12
|
+
readonly kind: "console"
|
|
13
|
+
readonly access: string
|
|
14
|
+
readonly server: string
|
|
15
|
+
readonly orgID?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type KeyAuth = {
|
|
19
|
+
readonly kind: "key"
|
|
20
|
+
readonly key: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Auth = ConsoleAuth | KeyAuth
|
|
24
|
+
|
|
25
|
+
type Connection = NonNullable<
|
|
26
|
+
Awaited<ReturnType<Plugin.Context["integration"]["connection"]["active"]>>
|
|
27
|
+
>
|
|
28
|
+
type Credential = NonNullable<
|
|
29
|
+
Awaited<ReturnType<Plugin.Context["integration"]["connection"]["resolve"]>>
|
|
30
|
+
>
|
|
31
|
+
|
|
32
|
+
async function activeConnection(
|
|
33
|
+
context: Plugin.Context,
|
|
34
|
+
integrationID: string,
|
|
35
|
+
): Promise<Connection | undefined> {
|
|
36
|
+
try {
|
|
37
|
+
return await context.integration.connection.active(integrationID)
|
|
38
|
+
} catch {
|
|
39
|
+
return undefined
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function resolveConnection(
|
|
44
|
+
context: Plugin.Context,
|
|
45
|
+
connection: Connection,
|
|
46
|
+
): Promise<Credential | undefined> {
|
|
47
|
+
try {
|
|
48
|
+
return await context.integration.connection.resolve(connection)
|
|
49
|
+
} catch {
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The active OpenCode Console OAuth login authorizes the Go status endpoint.
|
|
56
|
+
* Without one, an OpenCode Go or OpenCode API key uses the public usage
|
|
57
|
+
* endpoint. The environment variable stays as a last fallback.
|
|
58
|
+
*/
|
|
59
|
+
async function readAuth(context: Plugin.Context): Promise<Auth | null> {
|
|
60
|
+
const consoleConnection = await activeConnection(context, "opencode")
|
|
61
|
+
const consoleValue = consoleConnection
|
|
62
|
+
? await resolveConnection(context, consoleConnection)
|
|
63
|
+
: undefined
|
|
64
|
+
|
|
65
|
+
if (consoleValue?.type === "oauth") {
|
|
66
|
+
const access = text(consoleValue.access)
|
|
67
|
+
const metadata = isRecord(consoleValue.metadata) ? consoleValue.metadata : undefined
|
|
68
|
+
if (access) {
|
|
69
|
+
return {
|
|
70
|
+
kind: "console",
|
|
71
|
+
access,
|
|
72
|
+
server: text(metadata?.server) ?? CONSOLE_URL,
|
|
73
|
+
orgID: text(metadata?.orgID),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const integrationID of ["opencode-go", "opencode"]) {
|
|
79
|
+
const connection = await activeConnection(context, integrationID)
|
|
80
|
+
const value = connection ? await resolveConnection(context, connection) : undefined
|
|
81
|
+
if (value?.type !== "key") continue
|
|
82
|
+
|
|
83
|
+
const key = value.key.trim()
|
|
84
|
+
if (key) return { kind: "key", key }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const envKey = text(process.env.OPENCODE_API_KEY)?.trim()
|
|
88
|
+
return envKey ? { kind: "key", key: envKey } : null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function withTimeout<T>(
|
|
92
|
+
signal: AbortSignal,
|
|
93
|
+
run: (signal: AbortSignal) => Promise<T>,
|
|
94
|
+
): Promise<T> {
|
|
95
|
+
const controller = new AbortController()
|
|
96
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS)
|
|
97
|
+
const abort = () => controller.abort()
|
|
98
|
+
|
|
99
|
+
if (signal.aborted) controller.abort()
|
|
100
|
+
else signal.addEventListener("abort", abort, { once: true })
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
return await run(controller.signal)
|
|
104
|
+
} finally {
|
|
105
|
+
clearTimeout(timer)
|
|
106
|
+
signal.removeEventListener("abort", abort)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function readConsole(auth: ConsoleAuth, signal: AbortSignal) {
|
|
111
|
+
const headers: Record<string, string> = {
|
|
112
|
+
Authorization: `Bearer ${auth.access}`,
|
|
113
|
+
Accept: "application/json",
|
|
114
|
+
}
|
|
115
|
+
if (auth.orgID) headers["x-org-id"] = auth.orgID
|
|
116
|
+
|
|
117
|
+
const response = await fetch(`${auth.server.replace(/\/+$/u, "")}/api/go/status`, {
|
|
118
|
+
headers,
|
|
119
|
+
signal,
|
|
120
|
+
})
|
|
121
|
+
if (response.status === 404) throw new Error("OpenCode Go is not active for this account")
|
|
122
|
+
if (!response.ok) throw new Error(`OpenCode Console API error ${response.status}`)
|
|
123
|
+
|
|
124
|
+
const payload: unknown = await response.json()
|
|
125
|
+
const access = isRecord(payload) && isRecord(payload.access) ? payload.access : undefined
|
|
126
|
+
const meters = access && isRecord(access.meters) ? access.meters : undefined
|
|
127
|
+
if (!access || !meters) throw new Error("OpenCode Go is not active for this account")
|
|
128
|
+
|
|
129
|
+
return consoleWindows(meters, text(access.endsAt))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function readKey(auth: KeyAuth, signal: AbortSignal) {
|
|
133
|
+
const response = await fetch(ZEN_USAGE_URL, {
|
|
134
|
+
headers: {
|
|
135
|
+
Authorization: `Bearer ${auth.key}`,
|
|
136
|
+
Accept: "application/json",
|
|
137
|
+
},
|
|
138
|
+
signal,
|
|
139
|
+
})
|
|
140
|
+
if (response.status === 403) throw new Error("OpenCode Go is not active for this key")
|
|
141
|
+
if (!response.ok) throw new Error(`OpenCode Go API error ${response.status}`)
|
|
142
|
+
|
|
143
|
+
const payload: unknown = await response.json()
|
|
144
|
+
const usage = isRecord(payload) && isRecord(payload.usage) ? payload.usage : undefined
|
|
145
|
+
if (!usage) throw new Error("OpenCode Go usage is unavailable")
|
|
146
|
+
|
|
147
|
+
return keyWindows(usage)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Reads the active OpenCode Go quota and always returns a snapshot, including failures. */
|
|
151
|
+
async function readGo(
|
|
152
|
+
context: Plugin.Context,
|
|
153
|
+
signal: AbortSignal,
|
|
154
|
+
): Promise<QuotaWindowSnapshot[]> {
|
|
155
|
+
const auth = await readAuth(context)
|
|
156
|
+
if (!auth) throw new Error("OpenCode Go is not signed in")
|
|
157
|
+
|
|
158
|
+
return withTimeout(signal, (requestSignal) =>
|
|
159
|
+
auth.kind === "console" ? readConsole(auth, requestSignal) : readKey(auth, requestSignal),
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
interface QuotaSource {
|
|
164
|
+
/** Display name, and the credential the source knows how to read. */
|
|
165
|
+
readonly name: string
|
|
166
|
+
read(context: Plugin.Context, signal: AbortSignal): Promise<QuotaWindowSnapshot[]>
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Providers whose quota this plugin can read, keyed by the model catalog ID so
|
|
171
|
+
* the TUI never names a provider. Adding one is a reader plus an entry here.
|
|
172
|
+
* Providers missing from the map publish no quota, and the panel stays hidden
|
|
173
|
+
* for them instead of showing an empty box.
|
|
174
|
+
*/
|
|
175
|
+
const SOURCES: Readonly<Record<string, QuotaSource>> = {
|
|
176
|
+
"opencode-go": { name: PROVIDER, read: readGo },
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The provider IDs this plugin can read a quota for. */
|
|
180
|
+
export function supportedProviders(): readonly string[] {
|
|
181
|
+
return Object.keys(SOURCES)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Reads one provider's quota, or every provider that has a source when no ID is
|
|
186
|
+
* given. Unknown IDs are skipped so an unsupported provider renders nothing.
|
|
187
|
+
*/
|
|
188
|
+
export async function readQuotas(
|
|
189
|
+
context: Plugin.Context,
|
|
190
|
+
providerID: string | undefined,
|
|
191
|
+
signal: AbortSignal,
|
|
192
|
+
): Promise<QuotaSnapshot[]> {
|
|
193
|
+
const ids = providerID ? [providerID] : supportedProviders()
|
|
194
|
+
const snapshots: QuotaSnapshot[] = []
|
|
195
|
+
|
|
196
|
+
for (const id of ids) {
|
|
197
|
+
const source = SOURCES[id]
|
|
198
|
+
if (source) snapshots.push(await readSource(context, id, source, signal))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return snapshots
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function readSource(
|
|
205
|
+
context: Plugin.Context,
|
|
206
|
+
providerID: string,
|
|
207
|
+
source: QuotaSource,
|
|
208
|
+
signal: AbortSignal,
|
|
209
|
+
): Promise<QuotaSnapshot> {
|
|
210
|
+
try {
|
|
211
|
+
const windows = await source.read(context, signal)
|
|
212
|
+
if (windows.length === 0) throw new Error(`${source.name} returned no quota windows`)
|
|
213
|
+
|
|
214
|
+
return { providerID, provider: source.name, capturedAt: Date.now(), windows }
|
|
215
|
+
} catch (error) {
|
|
216
|
+
return {
|
|
217
|
+
providerID,
|
|
218
|
+
provider: source.name,
|
|
219
|
+
capturedAt: Date.now(),
|
|
220
|
+
windows: [],
|
|
221
|
+
error: errorText(error),
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
package/src/rpc.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Rpc } from "@opencode/plugin/rpc"
|
|
2
|
+
|
|
3
|
+
export type QuotaWindowKey = "5h" | "7d" | "30d"
|
|
4
|
+
|
|
5
|
+
export interface QuotaWindowSnapshot {
|
|
6
|
+
readonly key: QuotaWindowKey
|
|
7
|
+
readonly percentRemaining: number
|
|
8
|
+
/**
|
|
9
|
+
* The same share without the rounding shown in the panel. Pacing reads it so
|
|
10
|
+
* the daily bar moves with real spending instead of jumping a whole percent
|
|
11
|
+
* at a time. Providers that only publish a whole percent return the same
|
|
12
|
+
* value as `percentRemaining`.
|
|
13
|
+
*/
|
|
14
|
+
readonly percentRemainingExact?: number
|
|
15
|
+
readonly resetsAt: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* One provider's quota. `providerID` is the model catalog ID (`opencode-go`),
|
|
20
|
+
* so the panel can follow the selected model without knowing any provider name.
|
|
21
|
+
*/
|
|
22
|
+
export interface QuotaSnapshot {
|
|
23
|
+
readonly providerID: string
|
|
24
|
+
readonly provider: string
|
|
25
|
+
readonly capturedAt: number
|
|
26
|
+
readonly windows: readonly QuotaWindowSnapshot[]
|
|
27
|
+
readonly error?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const windowSchema = {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
key: { type: "string", enum: ["5h", "7d", "30d"] },
|
|
34
|
+
percentRemaining: { type: "number" },
|
|
35
|
+
percentRemainingExact: { type: "number" },
|
|
36
|
+
resetsAt: { type: "string" },
|
|
37
|
+
},
|
|
38
|
+
required: ["key", "percentRemaining", "resetsAt"],
|
|
39
|
+
additionalProperties: false,
|
|
40
|
+
} as const
|
|
41
|
+
|
|
42
|
+
const snapshotSchema = {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
providerID: { type: "string" },
|
|
46
|
+
provider: { type: "string" },
|
|
47
|
+
capturedAt: { type: "number" },
|
|
48
|
+
windows: {
|
|
49
|
+
type: "array",
|
|
50
|
+
items: windowSchema,
|
|
51
|
+
},
|
|
52
|
+
error: { type: "string" },
|
|
53
|
+
},
|
|
54
|
+
required: ["providerID", "provider", "capturedAt", "windows"],
|
|
55
|
+
additionalProperties: false,
|
|
56
|
+
} as const
|
|
57
|
+
|
|
58
|
+
export const Quota = Rpc.define({
|
|
59
|
+
id: "opencode-tweaks.quota",
|
|
60
|
+
methods: {
|
|
61
|
+
snapshot: {
|
|
62
|
+
/** A provider ID reads that provider; omitting it reads every source. */
|
|
63
|
+
input: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
providerID: { type: "string" },
|
|
67
|
+
},
|
|
68
|
+
additionalProperties: false,
|
|
69
|
+
} as const,
|
|
70
|
+
output: {
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: {
|
|
73
|
+
snapshots: {
|
|
74
|
+
type: "array",
|
|
75
|
+
items: snapshotSchema,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
required: ["snapshots"],
|
|
79
|
+
additionalProperties: false,
|
|
80
|
+
} as const,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
events: {},
|
|
84
|
+
} as const)
|
package/src/tui.tsx
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Plugin } from "@opencode/plugin/tui"
|
|
2
|
+
import { openTweaksMenu } from "./tweaks/menu"
|
|
3
|
+
import { QuotaPanel } from "./tweaks/quota-panel"
|
|
4
|
+
import { createSettingsStore } from "./tweaks/settings"
|
|
5
|
+
import { createTokenInjector } from "./tweaks/token-inject"
|
|
6
|
+
import { TokenInline, TokenStatus } from "./tweaks/token-slots"
|
|
7
|
+
|
|
8
|
+
export default Plugin.define({
|
|
9
|
+
id: "opencode-tweaks.tui",
|
|
10
|
+
setup(context) {
|
|
11
|
+
const store = createSettingsStore(context)
|
|
12
|
+
const tokens = createTokenInjector(context)
|
|
13
|
+
let menuOpen = false
|
|
14
|
+
|
|
15
|
+
context.ui.slot({
|
|
16
|
+
append: "sidebar.content",
|
|
17
|
+
render: () => <QuotaPanel context={context} store={store} />,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
context.ui.slot({
|
|
21
|
+
append: "session.composer.top",
|
|
22
|
+
render: (input) => (
|
|
23
|
+
<TokenInline context={context} input={input} store={store} injector={tokens} />
|
|
24
|
+
),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
context.ui.slot({
|
|
28
|
+
after: "prompt.footer.status",
|
|
29
|
+
render: (input) => (
|
|
30
|
+
<TokenStatus context={context} input={input} store={store} injector={tokens} />
|
|
31
|
+
),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
context.keymap.layer(() => ({
|
|
35
|
+
mode: "global",
|
|
36
|
+
commands: [
|
|
37
|
+
{
|
|
38
|
+
id: "opencode-tweaks.tweaks",
|
|
39
|
+
title: "Tweaks",
|
|
40
|
+
description: "Open the opencode-tweaks settings menu",
|
|
41
|
+
group: "Tweaks",
|
|
42
|
+
palette: true,
|
|
43
|
+
slash: { name: "tweaks" },
|
|
44
|
+
run: async () => {
|
|
45
|
+
if (menuOpen) return
|
|
46
|
+
|
|
47
|
+
menuOpen = true
|
|
48
|
+
try {
|
|
49
|
+
await openTweaksMenu(context, store)
|
|
50
|
+
} catch (error) {
|
|
51
|
+
context.ui.toast.show({
|
|
52
|
+
variant: "error",
|
|
53
|
+
title: "Tweaks",
|
|
54
|
+
message: error instanceof Error ? error.message : String(error),
|
|
55
|
+
})
|
|
56
|
+
} finally {
|
|
57
|
+
menuOpen = false
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
}))
|
|
63
|
+
|
|
64
|
+
return () => tokens.dispose()
|
|
65
|
+
},
|
|
66
|
+
})
|