opencode-deepseek-peak 0.1.0 → 0.2.0
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/README.md +19 -2
- package/dist/tui-v1.js +48096 -0
- package/dist/tui-v1.js.map +7 -0
- package/install.mjs +12 -8
- package/package.json +3 -1
- package/src/v1/tui.tsx +275 -0
- package/tui/index.js +7 -2
package/install.mjs
CHANGED
|
@@ -24,28 +24,32 @@ async function install() {
|
|
|
24
24
|
await mkdir(directory, { recursive: true })
|
|
25
25
|
const jsoncPath = join(directory, "opencode.jsonc")
|
|
26
26
|
const jsonc = await readOptional(jsoncPath)
|
|
27
|
+
const tuiJsoncPath = join(directory, "tui.jsonc")
|
|
28
|
+
const tuiJsonc = await readOptional(tuiJsoncPath)
|
|
29
|
+
// [path, $schema, list key]: V1 TUI plugins live in tui.json under `plugin`.
|
|
27
30
|
const targets = [
|
|
28
|
-
[jsonc !== undefined ? jsoncPath : join(directory, "opencode.json"), "https://opencode.ai/config.json"],
|
|
29
|
-
[join(directory, "cli.json"), "https://opencode.ai/v2/cli.json"],
|
|
31
|
+
[jsonc !== undefined ? jsoncPath : join(directory, "opencode.json"), "https://opencode.ai/config.json", "plugins"],
|
|
32
|
+
[join(directory, "cli.json"), "https://opencode.ai/v2/cli.json", "plugins"],
|
|
33
|
+
[tuiJsonc !== undefined ? tuiJsoncPath : join(directory, "tui.json"), "https://opencode.ai/tui.json", "plugin"],
|
|
30
34
|
]
|
|
31
|
-
// Validate
|
|
35
|
+
// Validate every document before writing any of them. JSONC edits preserve
|
|
32
36
|
// comments, trailing commas and unrelated settings.
|
|
33
37
|
const updates = []
|
|
34
|
-
for (const [path, schema] of targets) {
|
|
35
|
-
const text = await readOptional(path) ?? JSON.stringify({ $schema: schema,
|
|
38
|
+
for (const [path, schema, key] of targets) {
|
|
39
|
+
const text = await readOptional(path) ?? JSON.stringify({ $schema: schema, [key]: [] }, null, 2) + "\n"
|
|
36
40
|
const errors = []
|
|
37
41
|
const config = parse(text, errors, { allowTrailingComma: true })
|
|
38
42
|
if (errors.length || !config || typeof config !== "object" || Array.isArray(config)) {
|
|
39
43
|
throw new Error(`Invalid configuration: ${path}`)
|
|
40
44
|
}
|
|
41
|
-
const plugins = config
|
|
42
|
-
if (!Array.isArray(plugins)) throw new Error(`Expected a
|
|
45
|
+
const plugins = config[key] ?? []
|
|
46
|
+
if (!Array.isArray(plugins)) throw new Error(`Expected a ${key} array: ${path}`)
|
|
43
47
|
const present = plugins.some((item) => {
|
|
44
48
|
const spec = typeof item === "string" ? item : item?.package
|
|
45
49
|
return spec === PLUGIN_SPEC || spec?.startsWith(`${PLUGIN_SPEC}@`)
|
|
46
50
|
})
|
|
47
51
|
if (present) continue
|
|
48
|
-
const result = applyEdits(text, modify(text, [
|
|
52
|
+
const result = applyEdits(text, modify(text, [key], [...plugins, PLUGIN_SPEC], {
|
|
49
53
|
formattingOptions: { insertSpaces: true, tabSize: 2 },
|
|
50
54
|
}))
|
|
51
55
|
updates.push([path, result])
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-deepseek-peak",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "OpenCode V2 plugin showing DeepSeek peak and off-peak pricing periods",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -51,6 +51,8 @@
|
|
|
51
51
|
"jsonc-parser": "^3.3.1"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
+
"@opencode-ai/plugin": "^1.18.16",
|
|
55
|
+
"@opencode-ai/sdk": "^1.18.16",
|
|
54
56
|
"@opencode/plugin": "^2.0.16",
|
|
55
57
|
"@opentui/core": "^0.5.10",
|
|
56
58
|
"@opentui/solid": "^0.5.10",
|
package/src/v1/tui.tsx
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
|
|
3
|
+
import type { TuiDialogStack, TuiPlugin, TuiPluginApi, TuiSlotContext, TuiThemeCurrent } from "@opencode-ai/plugin/tui"
|
|
4
|
+
import { createMemo, createSignal, For, Show } from "solid-js"
|
|
5
|
+
import { evidenceLabel, statusLabel } from "../core/display.js"
|
|
6
|
+
import { createT, detectLang, LANG_META, type LangCode } from "../i18n.js"
|
|
7
|
+
import { scheduleInfo } from "../schedule.js"
|
|
8
|
+
import type { PeakStatus } from "../types.js"
|
|
9
|
+
|
|
10
|
+
const KV = {
|
|
11
|
+
lang: "deepseek-peak.lang",
|
|
12
|
+
sidebar: "deepseek-peak.sidebar",
|
|
13
|
+
evidence: "deepseek-peak.evidence",
|
|
14
|
+
footer: "deepseek-peak.footer",
|
|
15
|
+
toast: "deepseek-peak.toast",
|
|
16
|
+
} as const
|
|
17
|
+
|
|
18
|
+
const REFRESH_MS = 30_000
|
|
19
|
+
|
|
20
|
+
function stringList(value: unknown, fallback: readonly string[]): string[] {
|
|
21
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : [...fallback]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createStatus(sessionID: string): PeakStatus {
|
|
25
|
+
const schedule = scheduleInfo(new Date())
|
|
26
|
+
return {
|
|
27
|
+
sessionID,
|
|
28
|
+
providerID: "deepseek",
|
|
29
|
+
modelID: "deepseek",
|
|
30
|
+
period: schedule.period,
|
|
31
|
+
scheduledPeriod: schedule.period,
|
|
32
|
+
source: "official-schedule",
|
|
33
|
+
phase: "request",
|
|
34
|
+
mismatch: false,
|
|
35
|
+
observedAt: Date.now(),
|
|
36
|
+
evidence: schedule.holiday ? `Chinese public holiday: ${schedule.holiday}` : "DeepSeek official UTC pricing schedule",
|
|
37
|
+
holiday: schedule.holiday,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function StatusPanel(props: {
|
|
42
|
+
colors: () => TuiThemeCurrent
|
|
43
|
+
status: () => PeakStatus
|
|
44
|
+
lang: () => LangCode
|
|
45
|
+
evidence: () => boolean
|
|
46
|
+
}) {
|
|
47
|
+
const t = createT(props.lang)
|
|
48
|
+
const segments = createMemo(() => {
|
|
49
|
+
const current = props.status()
|
|
50
|
+
const theme = props.colors()
|
|
51
|
+
const peak = current.period === "peak"
|
|
52
|
+
const out = [
|
|
53
|
+
{ text: "DeepSeek ", color: theme.textMuted },
|
|
54
|
+
{ text: `● ${t(peak ? "period.peak" : "period.offPeak")}`, color: peak ? theme.text : theme.textMuted },
|
|
55
|
+
]
|
|
56
|
+
if (current.mismatch) out.push({ text: ` · ${t("label.mismatch")} ⚠`, color: theme.warning })
|
|
57
|
+
return out
|
|
58
|
+
})
|
|
59
|
+
return (
|
|
60
|
+
<box flexDirection="column" alignItems="flex-start" alignSelf="flex-start">
|
|
61
|
+
<text>
|
|
62
|
+
<For each={segments()}>{(segment) => <span style={{ fg: segment.color }}>{segment.text}</span>}</For>
|
|
63
|
+
</text>
|
|
64
|
+
<Show when={props.evidence()}>
|
|
65
|
+
<text fg={props.colors().textMuted}>{evidenceLabel(props.status(), t)}</text>
|
|
66
|
+
</Show>
|
|
67
|
+
</box>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function StatusFooter(props: { colors: () => TuiThemeCurrent; status: () => PeakStatus; lang: () => LangCode }) {
|
|
72
|
+
const t = createT(props.lang)
|
|
73
|
+
const segments = createMemo(() => {
|
|
74
|
+
const current = props.status()
|
|
75
|
+
const theme = props.colors()
|
|
76
|
+
const peak = current.period === "peak"
|
|
77
|
+
return [
|
|
78
|
+
{ text: `DS ${t(peak ? "period.peak" : "period.offPeak")}`, color: peak ? theme.text : theme.textMuted },
|
|
79
|
+
{ text: " ·", color: theme.textMuted },
|
|
80
|
+
]
|
|
81
|
+
})
|
|
82
|
+
return (
|
|
83
|
+
<text>
|
|
84
|
+
<For each={segments()}>{(segment) => <span style={{ fg: segment.color }}>{segment.text}</span>}</For>
|
|
85
|
+
</text>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* V1 TUI plugin: OpenCode V1 has no server-side hooks here, so the official
|
|
91
|
+
* schedule alone drives the indicator (no API header source of truth).
|
|
92
|
+
*/
|
|
93
|
+
export const tui: TuiPlugin = async (api: TuiPluginApi, options) => {
|
|
94
|
+
const providerIDs = new Set(stringList((options as Record<string, unknown> | undefined)?.providerIDs, ["deepseek"]))
|
|
95
|
+
|
|
96
|
+
const [lang, setLang] = createSignal<LangCode>(detectLang())
|
|
97
|
+
const [showSidebar, setShowSidebar] = createSignal(true)
|
|
98
|
+
const [showEvidence, setShowEvidence] = createSignal(true)
|
|
99
|
+
const [showFooter, setShowFooter] = createSignal(true)
|
|
100
|
+
const [showToast, setShowToast] = createSignal(true)
|
|
101
|
+
const [status, setStatus] = createSignal<PeakStatus>(createStatus(""))
|
|
102
|
+
const [revision, setRevision] = createSignal(0)
|
|
103
|
+
const t = createT(lang)
|
|
104
|
+
|
|
105
|
+
// KV may not be ready during setup; restore persisted preferences when it is.
|
|
106
|
+
const restore = () => {
|
|
107
|
+
const stored = String(api.kv.get(KV.lang, "") ?? "")
|
|
108
|
+
if (LANG_META.some((meta) => meta.code === stored)) setLang(stored as LangCode)
|
|
109
|
+
setShowSidebar(api.kv.get<boolean>(KV.sidebar, true) !== false)
|
|
110
|
+
setShowEvidence(api.kv.get<boolean>(KV.evidence, true) !== false)
|
|
111
|
+
setShowFooter(api.kv.get<boolean>(KV.footer, true) !== false)
|
|
112
|
+
setShowToast(api.kv.get<boolean>(KV.toast, true) !== false)
|
|
113
|
+
}
|
|
114
|
+
if (api.kv.ready) {
|
|
115
|
+
restore()
|
|
116
|
+
} else {
|
|
117
|
+
const timer = setInterval(() => {
|
|
118
|
+
if (!api.kv.ready) return
|
|
119
|
+
clearInterval(timer)
|
|
120
|
+
restore()
|
|
121
|
+
setRevision((value) => value + 1)
|
|
122
|
+
}, 100)
|
|
123
|
+
api.lifecycle.onDispose(() => clearInterval(timer))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let lastPeriod = status().period
|
|
127
|
+
const refresh = () => {
|
|
128
|
+
const next = createStatus(status().sessionID)
|
|
129
|
+
setStatus(next)
|
|
130
|
+
setRevision((value) => value + 1)
|
|
131
|
+
if (next.period !== lastPeriod) {
|
|
132
|
+
lastPeriod = next.period
|
|
133
|
+
if (showToast()) {
|
|
134
|
+
api.ui.toast({
|
|
135
|
+
title: t("toast.title"),
|
|
136
|
+
message: statusLabel(next, t),
|
|
137
|
+
variant: next.period === "peak" ? "warning" : "success",
|
|
138
|
+
duration: 4500,
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const timer = setInterval(refresh, REFRESH_MS)
|
|
144
|
+
api.lifecycle.onDispose(() => clearInterval(timer))
|
|
145
|
+
const offSession = api.event.on("session.updated", refresh)
|
|
146
|
+
api.lifecycle.onDispose(offSession)
|
|
147
|
+
|
|
148
|
+
const sessionProvider = (sessionID: string) => {
|
|
149
|
+
if (!sessionID) return undefined
|
|
150
|
+
try {
|
|
151
|
+
return api.state.session.get(sessionID)?.model?.providerID ?? undefined
|
|
152
|
+
} catch {
|
|
153
|
+
return undefined
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const relevant = (sessionID: string) => {
|
|
157
|
+
revision()
|
|
158
|
+
const providerID = sessionProvider(sessionID)
|
|
159
|
+
return providerID === undefined || providerIDs.has(providerID)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
api.slots.register({
|
|
163
|
+
order: 60,
|
|
164
|
+
slots: {
|
|
165
|
+
sidebar_content(ctx: TuiSlotContext, input: { session_id: string }) {
|
|
166
|
+
const sessionID = input.session_id
|
|
167
|
+
return (
|
|
168
|
+
<Show when={showSidebar() && relevant(sessionID)}>
|
|
169
|
+
<StatusPanel
|
|
170
|
+
colors={() => ctx.theme.current}
|
|
171
|
+
status={() => status()}
|
|
172
|
+
lang={lang}
|
|
173
|
+
evidence={() => showEvidence()}
|
|
174
|
+
/>
|
|
175
|
+
</Show>
|
|
176
|
+
)
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
api.slots.register({
|
|
182
|
+
order: 60,
|
|
183
|
+
slots: {
|
|
184
|
+
session_prompt_right(ctx: TuiSlotContext, input: { session_id: string }) {
|
|
185
|
+
const sessionID = input.session_id
|
|
186
|
+
return (
|
|
187
|
+
<Show when={showFooter() && relevant(sessionID)}>
|
|
188
|
+
<StatusFooter colors={() => ctx.theme.current} status={() => status()} lang={lang} />
|
|
189
|
+
</Show>
|
|
190
|
+
)
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const onOff = (value: boolean) => (value ? t("settings.on") : t("settings.off"))
|
|
196
|
+
const openSections = (dialog?: TuiDialogStack) => {
|
|
197
|
+
dialog?.replace(() => (
|
|
198
|
+
<api.ui.DialogSelect
|
|
199
|
+
title={t("settings.title")}
|
|
200
|
+
options={[
|
|
201
|
+
{ title: `${t("settings.sidebar")}: ${onOff(showSidebar())}`, value: "sidebar" },
|
|
202
|
+
{ title: `${t("settings.evidence")}: ${onOff(showEvidence())}`, value: "evidence" },
|
|
203
|
+
{ title: `${t("settings.footer")}: ${onOff(showFooter())}`, value: "footer" },
|
|
204
|
+
{ title: `${t("settings.toast")}: ${onOff(showToast())}`, value: "toast" },
|
|
205
|
+
]}
|
|
206
|
+
onSelect={(option) => {
|
|
207
|
+
if (option.value === "sidebar") {
|
|
208
|
+
const value = !showSidebar()
|
|
209
|
+
setShowSidebar(value)
|
|
210
|
+
api.kv.set(KV.sidebar, value)
|
|
211
|
+
} else if (option.value === "evidence") {
|
|
212
|
+
const value = !showEvidence()
|
|
213
|
+
setShowEvidence(value)
|
|
214
|
+
api.kv.set(KV.evidence, value)
|
|
215
|
+
} else if (option.value === "footer") {
|
|
216
|
+
const value = !showFooter()
|
|
217
|
+
setShowFooter(value)
|
|
218
|
+
api.kv.set(KV.footer, value)
|
|
219
|
+
} else if (option.value === "toast") {
|
|
220
|
+
const value = !showToast()
|
|
221
|
+
setShowToast(value)
|
|
222
|
+
api.kv.set(KV.toast, value)
|
|
223
|
+
}
|
|
224
|
+
openSections(dialog)
|
|
225
|
+
}}
|
|
226
|
+
/>
|
|
227
|
+
))
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
api.command?.register(() => [
|
|
231
|
+
{
|
|
232
|
+
title: t("command.status.title"),
|
|
233
|
+
value: "deepseek-peak.status",
|
|
234
|
+
description: t("command.status.description"),
|
|
235
|
+
slash: { name: "deepseek-peak" },
|
|
236
|
+
onSelect: (dialog) => {
|
|
237
|
+
const current = status()
|
|
238
|
+
dialog?.replace(() => (
|
|
239
|
+
<api.ui.DialogAlert
|
|
240
|
+
title={t("status.title")}
|
|
241
|
+
message={`${statusLabel(current, t)}\n${t("status.source")}: ${current.source}\n${t("status.evidence")}: ${current.evidence}`}
|
|
242
|
+
/>
|
|
243
|
+
))
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
title: t("command.sections.title"),
|
|
248
|
+
value: "deepseek-peak.sections",
|
|
249
|
+
description: t("command.sections.description"),
|
|
250
|
+
slash: { name: "deepseek-peak-sections" },
|
|
251
|
+
onSelect: (dialog) => openSections(dialog),
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
title: t("command.lang.title"),
|
|
255
|
+
value: "deepseek-peak.lang",
|
|
256
|
+
description: t("command.lang.description"),
|
|
257
|
+
slash: { name: "deepseek-peak-lang" },
|
|
258
|
+
onSelect: (dialog) => {
|
|
259
|
+
dialog?.replace(() => (
|
|
260
|
+
<api.ui.DialogSelect
|
|
261
|
+
title={t("settings.lang")}
|
|
262
|
+
options={LANG_META.map((meta) => ({ title: meta.label, value: meta.code }))}
|
|
263
|
+
current={lang()}
|
|
264
|
+
onSelect={(option) => {
|
|
265
|
+
const picked = option.value as LangCode
|
|
266
|
+
setLang(picked)
|
|
267
|
+
api.kv.set(KV.lang, picked)
|
|
268
|
+
dialog?.clear()
|
|
269
|
+
}}
|
|
270
|
+
/>
|
|
271
|
+
))
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
])
|
|
275
|
+
}
|
package/tui/index.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
// Dual-format TUI entry:
|
|
2
|
+
// - OpenCode V1 reads `tui` (TuiPluginModule)
|
|
3
|
+
// - OpenCode V2 reads `setup`
|
|
4
|
+
import { tui as v1Tui } from "./../dist/tui-v1.js"
|
|
5
|
+
import v2Mod from "./../dist/tui.js"
|
|
2
6
|
|
|
3
7
|
export default {
|
|
4
8
|
id: "opencode-deepseek-peak-tui",
|
|
5
|
-
|
|
9
|
+
tui: v1Tui,
|
|
10
|
+
setup: v2Mod.setup,
|
|
6
11
|
}
|