opencode-deepseek-peak 0.1.0 → 0.2.2

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/src/v1/tui.tsx ADDED
@@ -0,0 +1,293 @@
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, transitionLabel } 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
+ timezone: "deepseek-peak.timezone",
17
+ } as const
18
+
19
+ const REFRESH_MS = 30_000
20
+
21
+ function stringList(value: unknown, fallback: readonly string[]): string[] {
22
+ return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : [...fallback]
23
+ }
24
+
25
+ function createStatus(sessionID: string): PeakStatus {
26
+ const schedule = scheduleInfo(new Date())
27
+ return {
28
+ sessionID,
29
+ providerID: "deepseek",
30
+ modelID: "deepseek",
31
+ period: schedule.period,
32
+ scheduledPeriod: schedule.period,
33
+ source: "official-schedule",
34
+ phase: "request",
35
+ mismatch: false,
36
+ observedAt: Date.now(),
37
+ evidence: schedule.holiday ? `Chinese public holiday: ${schedule.holiday}` : "DeepSeek official UTC pricing schedule",
38
+ ...(schedule.holiday !== undefined ? { holiday: schedule.holiday } : {}),
39
+ }
40
+ }
41
+
42
+ function StatusPanel(props: {
43
+ colors: () => TuiThemeCurrent
44
+ status: () => PeakStatus
45
+ lang: () => LangCode
46
+ evidence: () => boolean
47
+ timeZone: () => string | undefined
48
+ }) {
49
+ const t = createT(props.lang)
50
+ const segments = createMemo(() => {
51
+ const current = props.status()
52
+ const theme = props.colors()
53
+ const peak = current.period === "peak"
54
+ const out = [
55
+ { text: "DeepSeek ", color: theme.textMuted },
56
+ { text: `● ${t(peak ? "period.peak" : "period.offPeak")}`, color: peak ? theme.text : theme.textMuted },
57
+ ]
58
+ if (current.mismatch) out.push({ text: ` · ${t("label.mismatch")} ⚠`, color: theme.warning })
59
+ return out
60
+ })
61
+ return (
62
+ <box flexDirection="column" alignItems="flex-start" alignSelf="flex-start">
63
+ <text>
64
+ <For each={segments()}>{(segment) => <span style={{ fg: segment.color }}>{segment.text}</span>}</For>
65
+ </text>
66
+ <Show when={props.evidence()}>
67
+ <text wrapMode="none" fg={props.colors().textMuted}>
68
+ {evidenceLabel(props.status(), t, { lang: props.lang(), timeZone: props.timeZone() })}
69
+ </text>
70
+ </Show>
71
+ </box>
72
+ )
73
+ }
74
+
75
+ function StatusFooter(props: { colors: () => TuiThemeCurrent; status: () => PeakStatus; lang: () => LangCode }) {
76
+ const t = createT(props.lang)
77
+ const segments = createMemo(() => {
78
+ const current = props.status()
79
+ const theme = props.colors()
80
+ const peak = current.period === "peak"
81
+ return [
82
+ { text: `DS ${t(peak ? "period.peak" : "period.offPeak")}`, color: peak ? theme.text : theme.textMuted },
83
+ { text: " ·", color: theme.textMuted },
84
+ ]
85
+ })
86
+ return (
87
+ <text>
88
+ <For each={segments()}>{(segment) => <span style={{ fg: segment.color }}>{segment.text}</span>}</For>
89
+ </text>
90
+ )
91
+ }
92
+
93
+ /**
94
+ * V1 TUI plugin: OpenCode V1 has no server-side hooks here, so the official
95
+ * schedule alone drives the indicator (no API header source of truth).
96
+ */
97
+ export const tui: TuiPlugin = async (api: TuiPluginApi, options) => {
98
+ const providerIDs = new Set(stringList((options as Record<string, unknown> | undefined)?.providerIDs, ["deepseek"]))
99
+
100
+ const [lang, setLang] = createSignal<LangCode>(detectLang())
101
+ const [showSidebar, setShowSidebar] = createSignal(true)
102
+ const [showEvidence, setShowEvidence] = createSignal(true)
103
+ const [showFooter, setShowFooter] = createSignal(true)
104
+ const [showToast, setShowToast] = createSignal(true)
105
+ const [utcZone, setUtcZone] = createSignal(false)
106
+ const [status, setStatus] = createSignal<PeakStatus>(createStatus(""))
107
+ const [revision, setRevision] = createSignal(0)
108
+ const t = createT(lang)
109
+ const timeZone = () => (utcZone() ? "UTC" : undefined)
110
+
111
+ // KV may not be ready during setup; restore persisted preferences when it is.
112
+ const restore = () => {
113
+ const stored = String(api.kv.get(KV.lang, "") ?? "")
114
+ if (LANG_META.some((meta) => meta.code === stored)) setLang(stored as LangCode)
115
+ setShowSidebar(api.kv.get<boolean>(KV.sidebar, true) !== false)
116
+ setShowEvidence(api.kv.get<boolean>(KV.evidence, true) !== false)
117
+ setShowFooter(api.kv.get<boolean>(KV.footer, true) !== false)
118
+ setShowToast(api.kv.get<boolean>(KV.toast, true) !== false)
119
+ setUtcZone(String(api.kv.get(KV.timezone, "local") ?? "local") === "utc")
120
+ }
121
+ if (api.kv.ready) {
122
+ restore()
123
+ } else {
124
+ const timer = setInterval(() => {
125
+ if (!api.kv.ready) return
126
+ clearInterval(timer)
127
+ restore()
128
+ setRevision((value) => value + 1)
129
+ }, 100)
130
+ api.lifecycle.onDispose(() => clearInterval(timer))
131
+ }
132
+
133
+ let lastPeriod = status().period
134
+ const refresh = () => {
135
+ const next = createStatus(status().sessionID)
136
+ setStatus(next)
137
+ setRevision((value) => value + 1)
138
+ if (next.period !== lastPeriod) {
139
+ lastPeriod = next.period
140
+ if (showToast()) {
141
+ api.ui.toast({
142
+ title: t("toast.title"),
143
+ message: statusLabel(next, t),
144
+ variant: next.period === "peak" ? "warning" : "success",
145
+ duration: 4500,
146
+ })
147
+ }
148
+ }
149
+ }
150
+ const timer = setInterval(refresh, REFRESH_MS)
151
+ api.lifecycle.onDispose(() => clearInterval(timer))
152
+ const offSession = api.event.on("session.updated", refresh)
153
+ api.lifecycle.onDispose(offSession)
154
+
155
+ const sessionProvider = (sessionID: string) => {
156
+ if (!sessionID) return undefined
157
+ try {
158
+ return api.state.session.get(sessionID)?.model?.providerID ?? undefined
159
+ } catch {
160
+ return undefined
161
+ }
162
+ }
163
+ const relevant = (sessionID: string) => {
164
+ revision()
165
+ const providerID = sessionProvider(sessionID)
166
+ return providerID === undefined || providerIDs.has(providerID)
167
+ }
168
+
169
+ api.slots.register({
170
+ order: 60,
171
+ slots: {
172
+ sidebar_content(ctx: TuiSlotContext, input: { session_id: string }) {
173
+ const sessionID = input.session_id
174
+ return (
175
+ <Show when={showSidebar() && relevant(sessionID)}>
176
+ <StatusPanel
177
+ colors={() => ctx.theme.current}
178
+ status={() => status()}
179
+ lang={lang}
180
+ evidence={() => showEvidence()}
181
+ timeZone={timeZone}
182
+ />
183
+ </Show>
184
+ )
185
+ },
186
+ },
187
+ })
188
+
189
+ api.slots.register({
190
+ order: 60,
191
+ slots: {
192
+ session_prompt_right(ctx: TuiSlotContext, input: { session_id: string }) {
193
+ const sessionID = input.session_id
194
+ return (
195
+ <Show when={showFooter() && relevant(sessionID)}>
196
+ <StatusFooter colors={() => ctx.theme.current} status={() => status()} lang={lang} />
197
+ </Show>
198
+ )
199
+ },
200
+ },
201
+ })
202
+
203
+ const onOff = (value: boolean) => (value ? t("settings.on") : t("settings.off"))
204
+ const openSections = (dialog?: TuiDialogStack) => {
205
+ dialog?.replace(() => (
206
+ <api.ui.DialogSelect
207
+ title={t("settings.title")}
208
+ options={[
209
+ { title: `${t("settings.sidebar")}: ${onOff(showSidebar())}`, value: "sidebar" },
210
+ { title: `${t("settings.evidence")}: ${onOff(showEvidence())}`, value: "evidence" },
211
+ { title: `${t("settings.footer")}: ${onOff(showFooter())}`, value: "footer" },
212
+ { title: `${t("settings.toast")}: ${onOff(showToast())}`, value: "toast" },
213
+ {
214
+ title: `${t("settings.timezone")}: ${utcZone() ? t("timezone.utc") : t("timezone.local")}`,
215
+ value: "timezone",
216
+ },
217
+ ]}
218
+ onSelect={(option) => {
219
+ if (option.value === "sidebar") {
220
+ const value = !showSidebar()
221
+ setShowSidebar(value)
222
+ api.kv.set(KV.sidebar, value)
223
+ } else if (option.value === "evidence") {
224
+ const value = !showEvidence()
225
+ setShowEvidence(value)
226
+ api.kv.set(KV.evidence, value)
227
+ } else if (option.value === "footer") {
228
+ const value = !showFooter()
229
+ setShowFooter(value)
230
+ api.kv.set(KV.footer, value)
231
+ } else if (option.value === "toast") {
232
+ const value = !showToast()
233
+ setShowToast(value)
234
+ api.kv.set(KV.toast, value)
235
+ } else if (option.value === "timezone") {
236
+ const value = !utcZone()
237
+ setUtcZone(value)
238
+ api.kv.set(KV.timezone, value ? "utc" : "local")
239
+ }
240
+ openSections(dialog)
241
+ }}
242
+ />
243
+ ))
244
+ }
245
+
246
+ api.command?.register(() => [
247
+ {
248
+ title: t("command.status.title"),
249
+ value: "deepseek-peak.status",
250
+ description: t("command.status.description"),
251
+ slash: { name: "deepseek-peak" },
252
+ onSelect: (dialog) => {
253
+ const current = status()
254
+ const message = [
255
+ statusLabel(current, t),
256
+ `${t("status.source")}: ${current.source}`,
257
+ `${t("status.evidence")}: ${current.evidence}`,
258
+ t("status.windows"),
259
+ `${t("status.nextChange")}: ${transitionLabel(t, { lang: lang(), timeZone: timeZone() })}`,
260
+ ].join("\n")
261
+ dialog?.replace(() => <api.ui.DialogAlert title={t("status.title")} message={message} />)
262
+ },
263
+ },
264
+ {
265
+ title: t("command.sections.title"),
266
+ value: "deepseek-peak.sections",
267
+ description: t("command.sections.description"),
268
+ slash: { name: "deepseek-peak-sections" },
269
+ onSelect: (dialog) => openSections(dialog),
270
+ },
271
+ {
272
+ title: t("command.lang.title"),
273
+ value: "deepseek-peak.lang",
274
+ description: t("command.lang.description"),
275
+ slash: { name: "deepseek-peak-lang" },
276
+ onSelect: (dialog) => {
277
+ dialog?.replace(() => (
278
+ <api.ui.DialogSelect
279
+ title={t("settings.lang")}
280
+ options={LANG_META.map((meta) => ({ title: meta.label, value: meta.code }))}
281
+ current={lang()}
282
+ onSelect={(option) => {
283
+ const picked = option.value as LangCode
284
+ setLang(picked)
285
+ api.kv.set(KV.lang, picked)
286
+ dialog?.clear()
287
+ }}
288
+ />
289
+ ))
290
+ },
291
+ },
292
+ ])
293
+ }
@@ -1,5 +1,5 @@
1
1
  import type { Plugin } from "@opencode/plugin/tui"
2
- import { statusLabel } from "../core/display.js"
2
+ import { statusLabel, transitionLabel } from "../core/display.js"
3
3
  import { createT, LANG_META, type LangCode } from "../i18n.js"
4
4
  import type { PeakStatus, TuiSettings } from "../types.js"
5
5
  import { openSettingsMenu } from "./settings-menu.js"
@@ -10,6 +10,7 @@ interface CommandProps {
10
10
  update: (mutation: (draft: TuiSettings) => void) => Promise<void>
11
11
  readStatus: (sessionID: string) => Promise<PeakStatus | undefined>
12
12
  lang: () => LangCode
13
+ timeZone: () => string | undefined
13
14
  }
14
15
 
15
16
  /** Command layer (registered in the app slot: slash commands stay available when the sidebar is hidden). */
@@ -38,7 +39,13 @@ export function CommandRoot(props: CommandProps) {
38
39
  await context.ui.dialog.alert({
39
40
  title: t("status.title"),
40
41
  message: status
41
- ? `${statusLabel(status, t)}\n${t("status.source")}: ${status.source}\n${t("status.evidence")}: ${status.evidence}`
42
+ ? [
43
+ statusLabel(status, t),
44
+ `${t("status.source")}: ${status.source}`,
45
+ `${t("status.evidence")}: ${status.evidence}`,
46
+ t("status.windows"),
47
+ `${t("status.nextChange")}: ${transitionLabel(t, { lang: props.lang(), timeZone: props.timeZone() })}`,
48
+ ].join("\n")
42
49
  : t("status.noRequest"),
43
50
  })
44
51
  } catch {
@@ -1,6 +1,6 @@
1
1
  import type { Plugin } from "@opencode/plugin/tui"
2
2
  import { createT, LANG_META, type LangCode } from "../i18n.js"
3
- import type { TuiSettings } from "../types.js"
3
+ import type { TimezoneMode, TuiSettings } from "../types.js"
4
4
 
5
5
  const nextTick = () => new Promise<void>((resolve) => setTimeout(resolve, 0))
6
6
 
@@ -45,6 +45,10 @@ export function openSettingsMenu(
45
45
  title: `${t("settings.lang")}: ${LANG_META.find((meta) => meta.code === lang())?.label ?? lang()}`,
46
46
  value: "lang",
47
47
  },
48
+ {
49
+ title: `${t("settings.timezone")}: ${settings.timezone === "utc" ? t("timezone.utc") : t("timezone.local")}`,
50
+ value: "timezone",
51
+ },
48
52
  ],
49
53
  })
50
54
  if (choice === undefined) return
@@ -102,6 +106,20 @@ export function openSettingsMenu(
102
106
  draft.lang = picked
103
107
  })
104
108
  }
109
+ } else if (choice === "timezone") {
110
+ const picked = await context.ui.dialog.select<TimezoneMode>({
111
+ title: t("settings.timezone"),
112
+ options: [
113
+ { title: t("timezone.local"), value: "local" },
114
+ { title: t("timezone.utc"), value: "utc" },
115
+ ],
116
+ current: settings.timezone === "utc" ? "utc" : "local",
117
+ })
118
+ if (picked !== undefined) {
119
+ await update((draft) => {
120
+ draft.timezone = picked
121
+ })
122
+ }
105
123
  }
106
124
 
107
125
  await nextTick()
package/tui/index.js CHANGED
@@ -1,6 +1,11 @@
1
- import mod from "./../dist/tui.js"
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
- setup: mod.setup,
9
+ tui: v1Tui,
10
+ setup: v2Mod.setup,
6
11
  }