opencode-deepseek-peak 0.2.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 +8 -1
- package/dist/index.js +17 -14
- package/dist/index.js.map +2 -2
- package/dist/tui-v1.js +377 -269
- package/dist/tui-v1.js.map +3 -3
- package/dist/tui.js +47750 -16
- package/dist/tui.js.map +4 -4
- package/package.json +1 -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 +27 -9
- package/src/v2/commands.ts +9 -2
- package/src/v2/settings-menu.ts +19 -1
package/package.json
CHANGED
package/src/core/display.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode/plugin/tui"
|
|
2
|
-
import type { T, TranslationKey } from "../i18n.js"
|
|
2
|
+
import type { LangCode, T, TranslationKey } from "../i18n.js"
|
|
3
|
+
import { nextTransition } from "../schedule.js"
|
|
3
4
|
import type { PeakStatus, TuiSettings } from "../types.js"
|
|
4
5
|
|
|
5
6
|
export const INITIAL_SETTINGS: TuiSettings = {
|
|
@@ -9,6 +10,7 @@ export const INITIAL_SETTINGS: TuiSettings = {
|
|
|
9
10
|
sidebarEvidence: true,
|
|
10
11
|
toastEveryRequest: false,
|
|
11
12
|
lang: "en",
|
|
13
|
+
timezone: "local",
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export function statusLabel(status: PeakStatus, t: T) {
|
|
@@ -26,9 +28,65 @@ const HOLIDAY_KEYS: Record<string, TranslationKey> = {
|
|
|
26
28
|
"Labour Day": "holiday.labourDay",
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
export interface LabelOptions {
|
|
32
|
+
/** Reference instant for the next transition; defaults to now. */
|
|
33
|
+
now?: Date
|
|
34
|
+
lang?: LangCode
|
|
35
|
+
/** IANA time zone; undefined uses the system zone, "UTC" shows the official times. */
|
|
36
|
+
timeZone?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function zoneDate(date: Date, timeZone: string | undefined): string {
|
|
40
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
41
|
+
timeZone,
|
|
42
|
+
year: "numeric",
|
|
43
|
+
month: "2-digit",
|
|
44
|
+
day: "2-digit",
|
|
45
|
+
}).formatToParts(date)
|
|
46
|
+
const value = (type: Intl.DateTimeFormatPartTypes) => parts.find((part) => part.type === type)?.value ?? ""
|
|
47
|
+
return `${value("year")}-${value("month")}-${value("day")}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatClock(date: Date, timeZone: string | undefined): string {
|
|
51
|
+
const parts = new Intl.DateTimeFormat("en-GB", {
|
|
52
|
+
timeZone,
|
|
53
|
+
hour: "2-digit",
|
|
54
|
+
minute: "2-digit",
|
|
55
|
+
hourCycle: "h23",
|
|
56
|
+
}).formatToParts(date)
|
|
57
|
+
const value = (type: Intl.DateTimeFormatPartTypes) => parts.find((part) => part.type === type)?.value ?? ""
|
|
58
|
+
return `${value("hour")}:${value("minute")}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** "10:00 UTC" in UTC mode, "07:00" in the local zone; adds a short weekday on another day. */
|
|
62
|
+
function formatTransitionAt(at: Date, now: Date, lang: LangCode, timeZone: string | undefined): string {
|
|
63
|
+
const clock = timeZone === "UTC" ? `${formatClock(at, timeZone)} UTC` : formatClock(at, timeZone)
|
|
64
|
+
if (zoneDate(at, timeZone) === zoneDate(now, timeZone)) return clock
|
|
65
|
+
const weekday = new Intl.DateTimeFormat(lang, { weekday: "short", timeZone }).format(at)
|
|
66
|
+
return `${weekday} ${clock}`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Localized "peak ends 10:00 UTC" / "peak starts Mon 01:00" for the next change. */
|
|
70
|
+
export function transitionLabel(t: T, options: LabelOptions = {}): string {
|
|
71
|
+
const now = options.now ?? new Date()
|
|
72
|
+
const next = nextTransition(now)
|
|
73
|
+
return t(next.kind === "peakEnd" ? "evidence.peakEnds" : "evidence.peakStarts", {
|
|
74
|
+
when: formatTransitionAt(next.at, now, options.lang ?? "en", options.timeZone),
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Compact sidebar form: ▲ peak starts / ▼ peak ends. Arrows need no translation. */
|
|
79
|
+
export function transitionCompact(options: LabelOptions = {}): string {
|
|
80
|
+
const now = options.now ?? new Date()
|
|
81
|
+
const next = nextTransition(now)
|
|
82
|
+
const arrow = next.kind === "peakEnd" ? "▼" : "▲"
|
|
83
|
+
return `${arrow} ${formatTransitionAt(next.at, now, options.lang ?? "en", options.timeZone)}`
|
|
84
|
+
}
|
|
85
|
+
|
|
29
86
|
/** Short, sidebar-friendly, localized version of the status evidence. */
|
|
30
|
-
export function evidenceLabel(status: PeakStatus, t: T): string {
|
|
87
|
+
export function evidenceLabel(status: PeakStatus, t: T, options: LabelOptions = {}): string {
|
|
31
88
|
let label: string
|
|
89
|
+
let transition = ""
|
|
32
90
|
// An API response header is the source of truth when present.
|
|
33
91
|
if (status.source === "api-header") {
|
|
34
92
|
label = t("evidence.apiHeader", { header: status.evidence.split(":")[0] })
|
|
@@ -37,10 +95,11 @@ export function evidenceLabel(status: PeakStatus, t: T): string {
|
|
|
37
95
|
label = t("evidence.holiday", { name: key ? t(key) : status.holiday })
|
|
38
96
|
} else {
|
|
39
97
|
label = t("evidence.schedule")
|
|
98
|
+
transition = ` · ${transitionCompact(options)}`
|
|
40
99
|
}
|
|
41
100
|
if (status.evidence.endsWith("(cached)")) label += ` ${t("evidence.cached")}`
|
|
42
101
|
if (status.evidence.includes("no request observed yet")) label += ` ${t("evidence.preview")}`
|
|
43
|
-
return label
|
|
102
|
+
return label + transition
|
|
44
103
|
}
|
|
45
104
|
|
|
46
105
|
/**
|
package/src/i18n.ts
CHANGED
|
@@ -10,11 +10,13 @@ export type LangCode = "en" | "pt" | "zh" | "ja" | "ko"
|
|
|
10
10
|
const EN_T = {
|
|
11
11
|
"period.peak": "PEAK",
|
|
12
12
|
"period.offPeak": "OFF-PEAK",
|
|
13
|
-
"evidence.schedule": "
|
|
13
|
+
"evidence.schedule": "Official schedule",
|
|
14
14
|
"evidence.apiHeader": "via {header}",
|
|
15
15
|
"evidence.cached": "(cached)",
|
|
16
16
|
"evidence.preview": "(no request yet)",
|
|
17
17
|
"evidence.holiday": "Holiday: {name}",
|
|
18
|
+
"evidence.peakEnds": "peak ends {when}",
|
|
19
|
+
"evidence.peakStarts": "peak starts {when}",
|
|
18
20
|
"label.mismatch": "mismatch",
|
|
19
21
|
"toast.title": "DeepSeek pricing",
|
|
20
22
|
"status.title": "DeepSeek Peak",
|
|
@@ -22,6 +24,8 @@ const EN_T = {
|
|
|
22
24
|
"status.noRequest": "No DeepSeek request observed in this session yet.",
|
|
23
25
|
"status.source": "Source",
|
|
24
26
|
"status.evidence": "Evidence",
|
|
27
|
+
"status.windows": "Peak: 01:00–04:00 and 06:00–10:00 UTC, Mon–Fri, except Chinese public holidays",
|
|
28
|
+
"status.nextChange": "Next change",
|
|
25
29
|
"status.unavailable": "DeepSeek Peak server plugin is unavailable.",
|
|
26
30
|
"command.status.title": "DeepSeek Peak: Show Status",
|
|
27
31
|
"command.status.description": "Show the last observed DeepSeek pricing period",
|
|
@@ -36,6 +40,9 @@ const EN_T = {
|
|
|
36
40
|
"settings.evidence": "Sidebar evidence",
|
|
37
41
|
"settings.frequency": "Toast frequency",
|
|
38
42
|
"settings.lang": "Language",
|
|
43
|
+
"settings.timezone": "Timezone",
|
|
44
|
+
"timezone.local": "Local",
|
|
45
|
+
"timezone.utc": "UTC",
|
|
39
46
|
"settings.on": "on",
|
|
40
47
|
"settings.off": "off",
|
|
41
48
|
"frequency.change": "on change",
|
|
@@ -54,11 +61,13 @@ export type TranslationKey = keyof Translation
|
|
|
54
61
|
const PT_T: Translation = {
|
|
55
62
|
"period.peak": "PICO",
|
|
56
63
|
"period.offPeak": "FORA DE PICO",
|
|
57
|
-
"evidence.schedule": "
|
|
64
|
+
"evidence.schedule": "Horário oficial",
|
|
58
65
|
"evidence.apiHeader": "via {header}",
|
|
59
66
|
"evidence.cached": "(cache)",
|
|
60
67
|
"evidence.preview": "(sem requisição ainda)",
|
|
61
68
|
"evidence.holiday": "Feriado: {name}",
|
|
69
|
+
"evidence.peakEnds": "pico termina {when}",
|
|
70
|
+
"evidence.peakStarts": "pico começa {when}",
|
|
62
71
|
"label.mismatch": "divergente",
|
|
63
72
|
"toast.title": "Preço DeepSeek",
|
|
64
73
|
"status.title": "DeepSeek Peak",
|
|
@@ -66,6 +75,8 @@ const PT_T: Translation = {
|
|
|
66
75
|
"status.noRequest": "Nenhuma requisição DeepSeek observada nesta sessão ainda.",
|
|
67
76
|
"status.source": "Fonte",
|
|
68
77
|
"status.evidence": "Evidência",
|
|
78
|
+
"status.windows": "Pico: 01:00–04:00 e 06:00–10:00 UTC, seg–sex, exceto feriados chineses",
|
|
79
|
+
"status.nextChange": "Próxima mudança",
|
|
69
80
|
"status.unavailable": "O plugin de servidor do DeepSeek Peak está indisponível.",
|
|
70
81
|
"command.status.title": "DeepSeek Peak: Mostrar status",
|
|
71
82
|
"command.status.description": "Mostra o último período de preço observado da DeepSeek",
|
|
@@ -80,6 +91,9 @@ const PT_T: Translation = {
|
|
|
80
91
|
"settings.evidence": "Evidência na barra lateral",
|
|
81
92
|
"settings.frequency": "Frequência do toast",
|
|
82
93
|
"settings.lang": "Idioma",
|
|
94
|
+
"settings.timezone": "Fuso horário",
|
|
95
|
+
"timezone.local": "Local",
|
|
96
|
+
"timezone.utc": "UTC",
|
|
83
97
|
"settings.on": "ativado",
|
|
84
98
|
"settings.off": "desativado",
|
|
85
99
|
"frequency.change": "ao mudar",
|
|
@@ -100,6 +114,8 @@ const ZH_T: Translation = {
|
|
|
100
114
|
"evidence.cached": "(缓存)",
|
|
101
115
|
"evidence.preview": "(尚无请求)",
|
|
102
116
|
"evidence.holiday": "节假日:{name}",
|
|
117
|
+
"evidence.peakEnds": "高峰于 {when} 结束",
|
|
118
|
+
"evidence.peakStarts": "高峰于 {when} 开始",
|
|
103
119
|
"label.mismatch": "不一致",
|
|
104
120
|
"toast.title": "DeepSeek 价格",
|
|
105
121
|
"status.title": "DeepSeek Peak",
|
|
@@ -107,6 +123,8 @@ const ZH_T: Translation = {
|
|
|
107
123
|
"status.noRequest": "此会话尚未观察到 DeepSeek 请求。",
|
|
108
124
|
"status.source": "来源",
|
|
109
125
|
"status.evidence": "证据",
|
|
126
|
+
"status.windows": "高峰时段:01:00–04:00、06:00–10:00 UTC,周一至周五,中国法定节假日除外",
|
|
127
|
+
"status.nextChange": "下次变化",
|
|
110
128
|
"status.unavailable": "DeepSeek Peak 服务端插件不可用。",
|
|
111
129
|
"command.status.title": "DeepSeek Peak:显示状态",
|
|
112
130
|
"command.status.description": "显示最近观察到的 DeepSeek 价格时段",
|
|
@@ -121,6 +139,9 @@ const ZH_T: Translation = {
|
|
|
121
139
|
"settings.evidence": "侧边栏证据",
|
|
122
140
|
"settings.frequency": "通知频率",
|
|
123
141
|
"settings.lang": "语言",
|
|
142
|
+
"settings.timezone": "时区",
|
|
143
|
+
"timezone.local": "本地",
|
|
144
|
+
"timezone.utc": "UTC",
|
|
124
145
|
"settings.on": "开",
|
|
125
146
|
"settings.off": "关",
|
|
126
147
|
"frequency.change": "变化时",
|
|
@@ -141,6 +162,8 @@ const JA_T: Translation = {
|
|
|
141
162
|
"evidence.cached": "(キャッシュ)",
|
|
142
163
|
"evidence.preview": "(まだリクエストなし)",
|
|
143
164
|
"evidence.holiday": "祝日:{name}",
|
|
165
|
+
"evidence.peakEnds": "ピークは {when} に終了",
|
|
166
|
+
"evidence.peakStarts": "ピークは {when} に開始",
|
|
144
167
|
"label.mismatch": "不一致",
|
|
145
168
|
"toast.title": "DeepSeek 料金",
|
|
146
169
|
"status.title": "DeepSeek Peak",
|
|
@@ -148,6 +171,8 @@ const JA_T: Translation = {
|
|
|
148
171
|
"status.noRequest": "このセッションではまだ DeepSeek リクエストがありません。",
|
|
149
172
|
"status.source": "ソース",
|
|
150
173
|
"status.evidence": "エビデンス",
|
|
174
|
+
"status.windows": "ピーク:01:00–04:00、06:00–10:00 UTC、月〜金、中国の祝日を除く",
|
|
175
|
+
"status.nextChange": "次の切り替え",
|
|
151
176
|
"status.unavailable": "DeepSeek Peak サーバープラグインが利用できません。",
|
|
152
177
|
"command.status.title": "DeepSeek Peak:ステータス表示",
|
|
153
178
|
"command.status.description": "最後に観測した DeepSeek 料金期間を表示",
|
|
@@ -162,6 +187,9 @@ const JA_T: Translation = {
|
|
|
162
187
|
"settings.evidence": "サイドバーのエビデンス",
|
|
163
188
|
"settings.frequency": "トースト頻度",
|
|
164
189
|
"settings.lang": "言語",
|
|
190
|
+
"settings.timezone": "タイムゾーン",
|
|
191
|
+
"timezone.local": "ローカル",
|
|
192
|
+
"timezone.utc": "UTC",
|
|
165
193
|
"settings.on": "オン",
|
|
166
194
|
"settings.off": "オフ",
|
|
167
195
|
"frequency.change": "変更時",
|
|
@@ -182,6 +210,8 @@ const KO_T: Translation = {
|
|
|
182
210
|
"evidence.cached": "(캐시)",
|
|
183
211
|
"evidence.preview": "(아직 요청 없음)",
|
|
184
212
|
"evidence.holiday": "공휴일: {name}",
|
|
213
|
+
"evidence.peakEnds": "피크 {when} 종료",
|
|
214
|
+
"evidence.peakStarts": "피크 {when} 시작",
|
|
185
215
|
"label.mismatch": "불일치",
|
|
186
216
|
"toast.title": "DeepSeek 요금",
|
|
187
217
|
"status.title": "DeepSeek Peak",
|
|
@@ -189,6 +219,8 @@ const KO_T: Translation = {
|
|
|
189
219
|
"status.noRequest": "이 세션에는 아직 DeepSeek 요청이 없습니다.",
|
|
190
220
|
"status.source": "출처",
|
|
191
221
|
"status.evidence": "근거",
|
|
222
|
+
"status.windows": "피크: 01:00–04:00, 06:00–10:00 UTC, 월–금, 중국 공휴일 제외",
|
|
223
|
+
"status.nextChange": "다음 전환",
|
|
192
224
|
"status.unavailable": "DeepSeek Peak 서버 플러그인을 사용할 수 없습니다.",
|
|
193
225
|
"command.status.title": "DeepSeek Peak: 상태 표시",
|
|
194
226
|
"command.status.description": "마지막으로 관측된 DeepSeek 요금 기간 표시",
|
|
@@ -203,6 +235,9 @@ const KO_T: Translation = {
|
|
|
203
235
|
"settings.evidence": "사이드바 근거",
|
|
204
236
|
"settings.frequency": "토스트 빈도",
|
|
205
237
|
"settings.lang": "언어",
|
|
238
|
+
"settings.timezone": "시간대",
|
|
239
|
+
"timezone.local": "로컬",
|
|
240
|
+
"timezone.utc": "UTC",
|
|
206
241
|
"settings.on": "켜짐",
|
|
207
242
|
"settings.off": "꺼짐",
|
|
208
243
|
"frequency.change": "변경 시",
|
package/src/index.ts
CHANGED
|
@@ -28,34 +28,41 @@ export default Plugin.define({
|
|
|
28
28
|
const status = statuses.get(sessionID)
|
|
29
29
|
return status ? { found: true, status } : { found: false }
|
|
30
30
|
},
|
|
31
|
-
//
|
|
32
|
-
//
|
|
31
|
+
// Re-synthesizes the current period on every call so an idle TUI can
|
|
32
|
+
// refresh itself without waiting for a request. A recent API header
|
|
33
|
+
// still wins over the official schedule.
|
|
33
34
|
preview: async (input) => {
|
|
34
35
|
const { sessionID } = input as { sessionID: string }
|
|
35
|
-
const observed = statuses.get(sessionID)
|
|
36
|
-
if (observed) return { found: true, status: observed }
|
|
37
36
|
try {
|
|
38
37
|
const session = await context.session.get({ sessionID })
|
|
39
38
|
const model = session.model
|
|
40
39
|
if (!model || !isDeepSeek(model.providerID)) return { found: false }
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
const observed = statuses.get(sessionID)
|
|
41
|
+
const now = Date.now()
|
|
42
|
+
const schedule = scheduleInfo(new Date(now))
|
|
43
|
+
const cached = apiSignals.get(modelKey(model.providerID, model.id))
|
|
44
|
+
const api = cached && now - cached.observedAt <= apiSignalTtlMs ? cached : undefined
|
|
45
|
+
const evidence = api
|
|
46
|
+
? `${api.evidence} (cached)`
|
|
47
|
+
: schedule.holiday
|
|
48
|
+
? `Chinese public holiday: ${schedule.holiday}${observed ? "" : " (no request observed yet)"}`
|
|
49
|
+
: observed
|
|
50
|
+
? "DeepSeek official UTC pricing schedule"
|
|
51
|
+
: "Current schedule (no request observed yet)"
|
|
45
52
|
return {
|
|
46
53
|
found: true,
|
|
47
54
|
status: {
|
|
48
55
|
sessionID,
|
|
49
56
|
providerID: model.providerID,
|
|
50
57
|
modelID: model.id,
|
|
51
|
-
period: schedule.period,
|
|
58
|
+
period: api?.period ?? schedule.period,
|
|
52
59
|
scheduledPeriod: schedule.period,
|
|
53
|
-
source: "official-schedule",
|
|
54
|
-
phase: "request",
|
|
55
|
-
mismatch: false,
|
|
56
|
-
observedAt:
|
|
60
|
+
source: api ? "api-header" : "official-schedule",
|
|
61
|
+
phase: observed?.phase ?? "request",
|
|
62
|
+
mismatch: api ? api.period !== schedule.period : false,
|
|
63
|
+
observedAt: now,
|
|
57
64
|
evidence,
|
|
58
|
-
holiday: schedule.holiday,
|
|
65
|
+
...(schedule.holiday !== undefined ? { holiday: schedule.holiday } : {}),
|
|
59
66
|
},
|
|
60
67
|
}
|
|
61
68
|
} catch {
|
|
@@ -93,7 +100,7 @@ export default Plugin.define({
|
|
|
93
100
|
: schedule.holiday
|
|
94
101
|
? `Chinese public holiday: ${schedule.holiday}`
|
|
95
102
|
: "DeepSeek official UTC pricing schedule",
|
|
96
|
-
holiday: schedule.holiday,
|
|
103
|
+
...(schedule.holiday !== undefined ? { holiday: schedule.holiday } : {}),
|
|
97
104
|
})
|
|
98
105
|
})
|
|
99
106
|
|
|
@@ -118,7 +125,7 @@ export default Plugin.define({
|
|
|
118
125
|
mismatch: api.period !== schedule.period,
|
|
119
126
|
observedAt: now,
|
|
120
127
|
evidence: api.evidence,
|
|
121
|
-
holiday: schedule.holiday,
|
|
128
|
+
...(schedule.holiday !== undefined ? { holiday: schedule.holiday } : {}),
|
|
122
129
|
})
|
|
123
130
|
})
|
|
124
131
|
|
package/src/panel/PeakPanel.tsx
CHANGED
|
@@ -17,6 +17,7 @@ export interface IndicatorProps {
|
|
|
17
17
|
|
|
18
18
|
export interface PanelProps extends IndicatorProps {
|
|
19
19
|
evidence: () => boolean
|
|
20
|
+
timeZone: () => string | undefined
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export function PeakPanel(props: PanelProps) {
|
|
@@ -51,7 +52,11 @@ export function PeakPanel(props: PanelProps) {
|
|
|
51
52
|
<For each={segments()}>{(segment) => <span style={{ fg: segment.color }}>{segment.text}</span>}</For>
|
|
52
53
|
</text>
|
|
53
54
|
<Show when={props.evidence() ? props.status() : undefined}>
|
|
54
|
-
{(status) =>
|
|
55
|
+
{(status) => (
|
|
56
|
+
<text wrapMode="none" fg={themeColors(props.context.theme).muted}>
|
|
57
|
+
{evidenceLabel(status(), t, { lang: props.lang(), timeZone: props.timeZone() })}
|
|
58
|
+
</text>
|
|
59
|
+
)}
|
|
55
60
|
</Show>
|
|
56
61
|
</box>
|
|
57
62
|
</Show>
|
package/src/schedule.ts
CHANGED
|
@@ -56,6 +56,45 @@ export function scheduleInfo(
|
|
|
56
56
|
return { period: peakWindow(date) }
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
export interface Transition {
|
|
60
|
+
kind: "peakStart" | "peakEnd"
|
|
61
|
+
/** Instant of the next change of the official schedule. */
|
|
62
|
+
at: Date
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function utcAt(date: Date, dayOffset: number, hour: number): Date {
|
|
66
|
+
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + dayOffset, hour))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isPeakDay(date: Date, holidayName: (value: Date) => string | undefined): boolean {
|
|
70
|
+
const day = date.getUTCDay()
|
|
71
|
+
return day !== 0 && day !== 6 && holidayName(date) === undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Next instant at which the official schedule changes period, considering the
|
|
76
|
+
* two weekday peak windows and China's public holidays.
|
|
77
|
+
*/
|
|
78
|
+
export function nextTransition(
|
|
79
|
+
date: Date,
|
|
80
|
+
holidayName: (value: Date) => string | undefined = chinesePublicHoliday,
|
|
81
|
+
): Transition {
|
|
82
|
+
if (scheduleInfo(date, holidayName).period === "peak") {
|
|
83
|
+
return { kind: "peakEnd", at: utcAt(date, 0, date.getUTCHours() < 4 ? 4 : 10) }
|
|
84
|
+
}
|
|
85
|
+
// Off-peak: the earliest future peak start on a weekday without a holiday.
|
|
86
|
+
// A public holiday can run for several consecutive days, so scan further.
|
|
87
|
+
for (let offset = 0; offset < 15; offset += 1) {
|
|
88
|
+
if (!isPeakDay(utcAt(date, offset, 0), holidayName)) continue
|
|
89
|
+
for (const hour of [1, 6]) {
|
|
90
|
+
const candidate = utcAt(date, offset, hour)
|
|
91
|
+
if (candidate.getTime() > date.getTime()) return { kind: "peakStart", at: candidate }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// date-holidays always yields a working weekday within two weeks; keep a fallback.
|
|
95
|
+
return { kind: "peakStart", at: utcAt(date, 1, 1) }
|
|
96
|
+
}
|
|
97
|
+
|
|
59
98
|
export function officialPricePeriod(
|
|
60
99
|
date: Date,
|
|
61
100
|
holidayCheck: (value: Date) => boolean = isChinesePublicHoliday,
|
package/src/tui.tsx
CHANGED
|
@@ -21,6 +21,8 @@ const mod: Plugin.Definition = {
|
|
|
21
21
|
// Stored settings may predate the `lang` field.
|
|
22
22
|
const lang = () => (settings.lang ?? detectLang()) as LangCode
|
|
23
23
|
const t = createT(lang)
|
|
24
|
+
// Local by default; `utc` pins the official schedule times.
|
|
25
|
+
const timeZone = () => (settings.timezone === "utc" ? "UTC" : undefined)
|
|
24
26
|
const [statuses, setStatuses] = createSignal<Record<string, PeakStatus>>({})
|
|
25
27
|
const lastToast = new Map<string, string>()
|
|
26
28
|
|
|
@@ -40,13 +42,19 @@ const mod: Plugin.Definition = {
|
|
|
40
42
|
})
|
|
41
43
|
|
|
42
44
|
const readStatus = async (sessionID: string) => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
// Preview re-synthesizes the current schedule, so an idle session stays
|
|
46
|
+
// fresh; it already folds in a recent API header when one exists.
|
|
45
47
|
try {
|
|
46
48
|
const preview = await client.preview({ sessionID }) as { found: boolean; status?: PeakStatus }
|
|
47
|
-
|
|
48
|
-
} catch {
|
|
49
|
+
if (preview.found && preview.status) return preview.status
|
|
49
50
|
return undefined
|
|
51
|
+
} catch {
|
|
52
|
+
try {
|
|
53
|
+
const result = await client.status({ sessionID }) as { found: boolean; status?: PeakStatus }
|
|
54
|
+
return result.found ? result.status : undefined
|
|
55
|
+
} catch {
|
|
56
|
+
return undefined
|
|
57
|
+
}
|
|
50
58
|
}
|
|
51
59
|
}
|
|
52
60
|
|
|
@@ -60,10 +68,22 @@ const mod: Plugin.Definition = {
|
|
|
60
68
|
.catch(() => {})
|
|
61
69
|
}
|
|
62
70
|
|
|
71
|
+
// Keep the period and the next-change label fresh while the session is idle.
|
|
72
|
+
const timer = setInterval(() => {
|
|
73
|
+
for (const sessionID of Object.keys(statuses())) refresh(sessionID)
|
|
74
|
+
}, 30_000)
|
|
75
|
+
|
|
63
76
|
const unregisterCommands = context.ui.slot({
|
|
64
77
|
append: "app",
|
|
65
78
|
render: () => (
|
|
66
|
-
<CommandRoot
|
|
79
|
+
<CommandRoot
|
|
80
|
+
context={context}
|
|
81
|
+
settings={settings}
|
|
82
|
+
update={update}
|
|
83
|
+
readStatus={readStatus}
|
|
84
|
+
lang={lang}
|
|
85
|
+
timeZone={timeZone}
|
|
86
|
+
/>
|
|
67
87
|
),
|
|
68
88
|
})
|
|
69
89
|
const unregisterSidebar = context.ui.slot({
|
|
@@ -77,6 +97,7 @@ const mod: Plugin.Definition = {
|
|
|
77
97
|
enabled={() => settings.sidebarIndicator !== false}
|
|
78
98
|
evidence={() => settings.sidebarEvidence !== false}
|
|
79
99
|
lang={lang}
|
|
100
|
+
timeZone={timeZone}
|
|
80
101
|
sessionID={sessionID}
|
|
81
102
|
refresh={refresh}
|
|
82
103
|
/>
|
|
@@ -101,6 +122,7 @@ const mod: Plugin.Definition = {
|
|
|
101
122
|
})
|
|
102
123
|
|
|
103
124
|
return () => {
|
|
125
|
+
clearInterval(timer)
|
|
104
126
|
unsubscribe()
|
|
105
127
|
unregisterCommands()
|
|
106
128
|
unregisterSidebar()
|
package/src/types.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { LangCode } from "./i18n.js"
|
|
|
3
3
|
export type PricePeriod = "peak" | "off-peak"
|
|
4
4
|
export type StatusSource = "official-schedule" | "api-header"
|
|
5
5
|
export type StatusPhase = "request" | "response"
|
|
6
|
+
export type TimezoneMode = "local" | "utc"
|
|
6
7
|
|
|
7
8
|
export interface PeakStatus {
|
|
8
9
|
sessionID: string
|
|
@@ -26,4 +27,5 @@ export interface TuiSettings {
|
|
|
26
27
|
sidebarEvidence: boolean
|
|
27
28
|
toastEveryRequest: boolean
|
|
28
29
|
lang: LangCode
|
|
30
|
+
timezone: TimezoneMode
|
|
29
31
|
}
|
package/src/v1/tui.tsx
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type { TuiDialogStack, TuiPlugin, TuiPluginApi, TuiSlotContext, TuiThemeCurrent } from "@opencode-ai/plugin/tui"
|
|
4
4
|
import { createMemo, createSignal, For, Show } from "solid-js"
|
|
5
|
-
import { evidenceLabel, statusLabel } from "../core/display.js"
|
|
5
|
+
import { evidenceLabel, statusLabel, transitionLabel } from "../core/display.js"
|
|
6
6
|
import { createT, detectLang, LANG_META, type LangCode } from "../i18n.js"
|
|
7
7
|
import { scheduleInfo } from "../schedule.js"
|
|
8
8
|
import type { PeakStatus } from "../types.js"
|
|
@@ -13,6 +13,7 @@ const KV = {
|
|
|
13
13
|
evidence: "deepseek-peak.evidence",
|
|
14
14
|
footer: "deepseek-peak.footer",
|
|
15
15
|
toast: "deepseek-peak.toast",
|
|
16
|
+
timezone: "deepseek-peak.timezone",
|
|
16
17
|
} as const
|
|
17
18
|
|
|
18
19
|
const REFRESH_MS = 30_000
|
|
@@ -34,7 +35,7 @@ function createStatus(sessionID: string): PeakStatus {
|
|
|
34
35
|
mismatch: false,
|
|
35
36
|
observedAt: Date.now(),
|
|
36
37
|
evidence: schedule.holiday ? `Chinese public holiday: ${schedule.holiday}` : "DeepSeek official UTC pricing schedule",
|
|
37
|
-
holiday: schedule.holiday,
|
|
38
|
+
...(schedule.holiday !== undefined ? { holiday: schedule.holiday } : {}),
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -43,6 +44,7 @@ function StatusPanel(props: {
|
|
|
43
44
|
status: () => PeakStatus
|
|
44
45
|
lang: () => LangCode
|
|
45
46
|
evidence: () => boolean
|
|
47
|
+
timeZone: () => string | undefined
|
|
46
48
|
}) {
|
|
47
49
|
const t = createT(props.lang)
|
|
48
50
|
const segments = createMemo(() => {
|
|
@@ -62,7 +64,9 @@ function StatusPanel(props: {
|
|
|
62
64
|
<For each={segments()}>{(segment) => <span style={{ fg: segment.color }}>{segment.text}</span>}</For>
|
|
63
65
|
</text>
|
|
64
66
|
<Show when={props.evidence()}>
|
|
65
|
-
<text fg={props.colors().textMuted}>
|
|
67
|
+
<text wrapMode="none" fg={props.colors().textMuted}>
|
|
68
|
+
{evidenceLabel(props.status(), t, { lang: props.lang(), timeZone: props.timeZone() })}
|
|
69
|
+
</text>
|
|
66
70
|
</Show>
|
|
67
71
|
</box>
|
|
68
72
|
)
|
|
@@ -98,9 +102,11 @@ export const tui: TuiPlugin = async (api: TuiPluginApi, options) => {
|
|
|
98
102
|
const [showEvidence, setShowEvidence] = createSignal(true)
|
|
99
103
|
const [showFooter, setShowFooter] = createSignal(true)
|
|
100
104
|
const [showToast, setShowToast] = createSignal(true)
|
|
105
|
+
const [utcZone, setUtcZone] = createSignal(false)
|
|
101
106
|
const [status, setStatus] = createSignal<PeakStatus>(createStatus(""))
|
|
102
107
|
const [revision, setRevision] = createSignal(0)
|
|
103
108
|
const t = createT(lang)
|
|
109
|
+
const timeZone = () => (utcZone() ? "UTC" : undefined)
|
|
104
110
|
|
|
105
111
|
// KV may not be ready during setup; restore persisted preferences when it is.
|
|
106
112
|
const restore = () => {
|
|
@@ -110,6 +116,7 @@ export const tui: TuiPlugin = async (api: TuiPluginApi, options) => {
|
|
|
110
116
|
setShowEvidence(api.kv.get<boolean>(KV.evidence, true) !== false)
|
|
111
117
|
setShowFooter(api.kv.get<boolean>(KV.footer, true) !== false)
|
|
112
118
|
setShowToast(api.kv.get<boolean>(KV.toast, true) !== false)
|
|
119
|
+
setUtcZone(String(api.kv.get(KV.timezone, "local") ?? "local") === "utc")
|
|
113
120
|
}
|
|
114
121
|
if (api.kv.ready) {
|
|
115
122
|
restore()
|
|
@@ -171,6 +178,7 @@ export const tui: TuiPlugin = async (api: TuiPluginApi, options) => {
|
|
|
171
178
|
status={() => status()}
|
|
172
179
|
lang={lang}
|
|
173
180
|
evidence={() => showEvidence()}
|
|
181
|
+
timeZone={timeZone}
|
|
174
182
|
/>
|
|
175
183
|
</Show>
|
|
176
184
|
)
|
|
@@ -202,6 +210,10 @@ export const tui: TuiPlugin = async (api: TuiPluginApi, options) => {
|
|
|
202
210
|
{ title: `${t("settings.evidence")}: ${onOff(showEvidence())}`, value: "evidence" },
|
|
203
211
|
{ title: `${t("settings.footer")}: ${onOff(showFooter())}`, value: "footer" },
|
|
204
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
|
+
},
|
|
205
217
|
]}
|
|
206
218
|
onSelect={(option) => {
|
|
207
219
|
if (option.value === "sidebar") {
|
|
@@ -220,6 +232,10 @@ export const tui: TuiPlugin = async (api: TuiPluginApi, options) => {
|
|
|
220
232
|
const value = !showToast()
|
|
221
233
|
setShowToast(value)
|
|
222
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")
|
|
223
239
|
}
|
|
224
240
|
openSections(dialog)
|
|
225
241
|
}}
|
|
@@ -235,12 +251,14 @@ export const tui: TuiPlugin = async (api: TuiPluginApi, options) => {
|
|
|
235
251
|
slash: { name: "deepseek-peak" },
|
|
236
252
|
onSelect: (dialog) => {
|
|
237
253
|
const current = status()
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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} />)
|
|
244
262
|
},
|
|
245
263
|
},
|
|
246
264
|
{
|
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()
|