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/README.md +27 -3
- package/dist/index.js +17 -14
- package/dist/index.js.map +2 -2
- package/dist/tui-v1.js +48204 -0
- package/dist/tui-v1.js.map +7 -0
- package/dist/tui.js +47750 -16
- package/dist/tui.js.map +4 -4
- package/install.mjs +12 -8
- package/package.json +3 -1
- package/src/core/display.ts +62 -3
- package/src/i18n.ts +37 -2
- package/src/index.ts +23 -16
- package/src/panel/PeakPanel.tsx +6 -1
- package/src/schedule.ts +39 -0
- package/src/tui.tsx +27 -5
- package/src/types.ts +2 -0
- package/src/v1/tui.tsx +293 -0
- package/src/v2/commands.ts +9 -2
- package/src/v2/settings-menu.ts +19 -1
- package/tui/index.js +7 -2
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
|
+
}
|
package/src/v2/commands.ts
CHANGED
|
@@ -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
|
-
?
|
|
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 {
|
package/src/v2/settings-menu.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|