opencode-deepseek-peak 0.1.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/LICENSE +21 -0
- package/README.md +79 -0
- package/dist/index.js +47817 -0
- package/dist/index.js.map +7 -0
- package/dist/rpc.js +77 -0
- package/dist/rpc.js.map +7 -0
- package/dist/tui.js +760 -0
- package/dist/tui.js.map +7 -0
- package/index.js +1 -0
- package/install.mjs +63 -0
- package/package.json +70 -0
- package/src/api-signal.ts +32 -0
- package/src/core/display.ts +59 -0
- package/src/i18n.ts +248 -0
- package/src/index.ts +131 -0
- package/src/panel/FooterStatus.tsx +32 -0
- package/src/panel/PeakPanel.tsx +59 -0
- package/src/rpc.ts +74 -0
- package/src/schedule.ts +66 -0
- package/src/tui.tsx +112 -0
- package/src/types.ts +29 -0
- package/src/v2/commands.ts +83 -0
- package/src/v2/settings-menu.ts +110 -0
- package/tui/index.js +6 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Plugin } from "@opencode/plugin"
|
|
2
|
+
import { DEFAULT_SIGNAL_HEADERS, readApiPricePeriod } from "./api-signal.js"
|
|
3
|
+
import { DeepSeekPeakRpc } from "./rpc.js"
|
|
4
|
+
import { scheduleInfo } from "./schedule.js"
|
|
5
|
+
import type { PeakStatus } from "./types.js"
|
|
6
|
+
|
|
7
|
+
function stringList(value: unknown, fallback: readonly string[]): string[] {
|
|
8
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string")
|
|
9
|
+
? value
|
|
10
|
+
: [...fallback]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default Plugin.define({
|
|
14
|
+
id: "opencode-deepseek-peak",
|
|
15
|
+
async setup(context) {
|
|
16
|
+
const providerIDs = new Set(stringList(context.options.providerIDs, ["deepseek"]))
|
|
17
|
+
const signalHeaders = stringList(context.options.apiSignalHeaders, DEFAULT_SIGNAL_HEADERS)
|
|
18
|
+
const configuredTtl = Number(context.options.apiSignalTtlMs)
|
|
19
|
+
const apiSignalTtlMs = Number.isFinite(configuredTtl) && configuredTtl >= 0
|
|
20
|
+
? configuredTtl
|
|
21
|
+
: 5 * 60 * 1000
|
|
22
|
+
const statuses = new Map<string, PeakStatus>()
|
|
23
|
+
const apiSignals = new Map<string, { period: PeakStatus["period"]; evidence: string; observedAt: number }>()
|
|
24
|
+
|
|
25
|
+
const rpc = await context.rpc.register(DeepSeekPeakRpc, {
|
|
26
|
+
status: async (input) => {
|
|
27
|
+
const { sessionID } = input as { sessionID: string }
|
|
28
|
+
const status = statuses.get(sessionID)
|
|
29
|
+
return status ? { found: true, status } : { found: false }
|
|
30
|
+
},
|
|
31
|
+
// Lets the TUI show the current period as soon as a session opens with a
|
|
32
|
+
// DeepSeek model selected, without waiting for the first request.
|
|
33
|
+
preview: async (input) => {
|
|
34
|
+
const { sessionID } = input as { sessionID: string }
|
|
35
|
+
const observed = statuses.get(sessionID)
|
|
36
|
+
if (observed) return { found: true, status: observed }
|
|
37
|
+
try {
|
|
38
|
+
const session = await context.session.get({ sessionID })
|
|
39
|
+
const model = session.model
|
|
40
|
+
if (!model || !isDeepSeek(model.providerID)) return { found: false }
|
|
41
|
+
const schedule = scheduleInfo(new Date())
|
|
42
|
+
const evidence = schedule.holiday
|
|
43
|
+
? `Chinese public holiday: ${schedule.holiday} (no request observed yet)`
|
|
44
|
+
: "Current schedule (no request observed yet)"
|
|
45
|
+
return {
|
|
46
|
+
found: true,
|
|
47
|
+
status: {
|
|
48
|
+
sessionID,
|
|
49
|
+
providerID: model.providerID,
|
|
50
|
+
modelID: model.id,
|
|
51
|
+
period: schedule.period,
|
|
52
|
+
scheduledPeriod: schedule.period,
|
|
53
|
+
source: "official-schedule",
|
|
54
|
+
phase: "request",
|
|
55
|
+
mismatch: false,
|
|
56
|
+
observedAt: Date.now(),
|
|
57
|
+
evidence,
|
|
58
|
+
holiday: schedule.holiday,
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
return { found: false }
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const publish = async (status: PeakStatus) => {
|
|
68
|
+
statuses.set(status.sessionID, status)
|
|
69
|
+
await rpc.events.emit("updated", status as unknown as Readonly<Record<string, unknown>>)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const isDeepSeek = (providerID: string) => providerIDs.has(providerID)
|
|
73
|
+
const modelKey = (providerID: string, modelID: string) => `${providerID}/${modelID}`
|
|
74
|
+
|
|
75
|
+
const stopRequest = await context.session.hook("model.request", async (event) => {
|
|
76
|
+
if (event.kind !== "primary" || !isDeepSeek(event.model.providerID)) return
|
|
77
|
+
const now = Date.now()
|
|
78
|
+
const schedule = scheduleInfo(new Date(now))
|
|
79
|
+
const cached = apiSignals.get(modelKey(event.model.providerID, event.model.id))
|
|
80
|
+
const api = cached && now - cached.observedAt <= apiSignalTtlMs ? cached : undefined
|
|
81
|
+
await publish({
|
|
82
|
+
sessionID: event.sessionID,
|
|
83
|
+
providerID: event.model.providerID,
|
|
84
|
+
modelID: event.model.id,
|
|
85
|
+
period: api?.period ?? schedule.period,
|
|
86
|
+
scheduledPeriod: schedule.period,
|
|
87
|
+
source: api ? "api-header" : "official-schedule",
|
|
88
|
+
phase: "request",
|
|
89
|
+
mismatch: api ? api.period !== schedule.period : false,
|
|
90
|
+
observedAt: now,
|
|
91
|
+
evidence: api
|
|
92
|
+
? `${api.evidence} (cached)`
|
|
93
|
+
: schedule.holiday
|
|
94
|
+
? `Chinese public holiday: ${schedule.holiday}`
|
|
95
|
+
: "DeepSeek official UTC pricing schedule",
|
|
96
|
+
holiday: schedule.holiday,
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const stopResponse = await context.session.hook("http.response", async (event) => {
|
|
101
|
+
if (event.kind !== "primary" || !isDeepSeek(event.model.providerID)) return
|
|
102
|
+
const api = readApiPricePeriod(event.response.headers, signalHeaders)
|
|
103
|
+
if (!api) return
|
|
104
|
+
const now = Date.now()
|
|
105
|
+
const schedule = scheduleInfo(new Date(now))
|
|
106
|
+
apiSignals.set(modelKey(event.model.providerID, event.model.id), {
|
|
107
|
+
...api,
|
|
108
|
+
observedAt: now,
|
|
109
|
+
})
|
|
110
|
+
await publish({
|
|
111
|
+
sessionID: event.sessionID,
|
|
112
|
+
providerID: event.model.providerID,
|
|
113
|
+
modelID: event.model.id,
|
|
114
|
+
period: api.period,
|
|
115
|
+
scheduledPeriod: schedule.period,
|
|
116
|
+
source: "api-header",
|
|
117
|
+
phase: "response",
|
|
118
|
+
mismatch: api.period !== schedule.period,
|
|
119
|
+
observedAt: now,
|
|
120
|
+
evidence: api.evidence,
|
|
121
|
+
holiday: schedule.holiday,
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
return async () => {
|
|
126
|
+
await stopRequest.dispose()
|
|
127
|
+
await stopResponse.dispose()
|
|
128
|
+
await rpc.dispose()
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
|
|
3
|
+
import { createMemo, For, Show } from "solid-js"
|
|
4
|
+
import { themeColors } from "../core/display.js"
|
|
5
|
+
import { createT } from "../i18n.js"
|
|
6
|
+
import type { IndicatorProps } from "./PeakPanel.js"
|
|
7
|
+
|
|
8
|
+
/** Prompt-footer status: peak stands out in the default text color, off-peak stays muted. */
|
|
9
|
+
export function FooterStatus(props: IndicatorProps) {
|
|
10
|
+
const t = createT(() => props.lang())
|
|
11
|
+
|
|
12
|
+
const segments = createMemo(() => {
|
|
13
|
+
const status = props.status()
|
|
14
|
+
if (!status) return []
|
|
15
|
+
const { normal, muted, warning } = themeColors(props.context.theme)
|
|
16
|
+
const peak = status.period === "peak"
|
|
17
|
+
const label = peak ? t("period.peak") : t("period.offPeak")
|
|
18
|
+
const out = [{ text: `DS ${label}`, color: peak ? normal : muted }]
|
|
19
|
+
if (status.source === "api-header") out.push({ text: " API", color: muted })
|
|
20
|
+
if (status.mismatch) out.push({ text: " ⚠", color: warning })
|
|
21
|
+
out.push({ text: " ·", color: muted })
|
|
22
|
+
return out
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Show when={props.enabled() && props.status()}>
|
|
27
|
+
<text>
|
|
28
|
+
<For each={segments()}>{(segment) => <span style={{ fg: segment.color }}>{segment.text}</span>}</For>
|
|
29
|
+
</text>
|
|
30
|
+
</Show>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
|
|
3
|
+
import type { Plugin } from "@opencode/plugin/tui"
|
|
4
|
+
import { createEffect, createMemo, For, Show } from "solid-js"
|
|
5
|
+
import { evidenceLabel, themeColors } from "../core/display.js"
|
|
6
|
+
import { createT, type LangCode } from "../i18n.js"
|
|
7
|
+
import type { PeakStatus } from "../types.js"
|
|
8
|
+
|
|
9
|
+
export interface IndicatorProps {
|
|
10
|
+
context: Plugin.Context
|
|
11
|
+
status: () => PeakStatus | undefined
|
|
12
|
+
enabled: () => boolean
|
|
13
|
+
lang: () => LangCode
|
|
14
|
+
sessionID: string
|
|
15
|
+
refresh: (sessionID: string) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PanelProps extends IndicatorProps {
|
|
19
|
+
evidence: () => boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function PeakPanel(props: PanelProps) {
|
|
23
|
+
const t = createT(() => props.lang())
|
|
24
|
+
|
|
25
|
+
// The server keeps the last observed request per session, so opening a
|
|
26
|
+
// session must pull that status instead of waiting for the next event.
|
|
27
|
+
createEffect(() => {
|
|
28
|
+
const sessionID = props.sessionID
|
|
29
|
+
if (sessionID) props.refresh(sessionID)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Single compact status line, flush left, plus an optional muted evidence line.
|
|
33
|
+
const segments = createMemo(() => {
|
|
34
|
+
const status = props.status()
|
|
35
|
+
if (!status) return []
|
|
36
|
+
const { normal, muted, warning } = themeColors(props.context.theme)
|
|
37
|
+
const peak = status.period === "peak"
|
|
38
|
+
const out = [
|
|
39
|
+
{ text: "DeepSeek ", color: muted },
|
|
40
|
+
{ text: `● ${t(peak ? "period.peak" : "period.offPeak")}`, color: peak ? normal : muted },
|
|
41
|
+
]
|
|
42
|
+
if (status.source === "api-header") out.push({ text: " · API", color: muted })
|
|
43
|
+
if (status.mismatch) out.push({ text: ` · ${t("label.mismatch")} ⚠`, color: warning })
|
|
44
|
+
return out
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Show when={props.enabled() && props.status()}>
|
|
49
|
+
<box flexDirection="column" alignItems="flex-start" alignSelf="flex-start">
|
|
50
|
+
<text>
|
|
51
|
+
<For each={segments()}>{(segment) => <span style={{ fg: segment.color }}>{segment.text}</span>}</For>
|
|
52
|
+
</text>
|
|
53
|
+
<Show when={props.evidence() ? props.status() : undefined}>
|
|
54
|
+
{(status) => <text fg={themeColors(props.context.theme).muted}>{evidenceLabel(status(), t)}</text>}
|
|
55
|
+
</Show>
|
|
56
|
+
</box>
|
|
57
|
+
</Show>
|
|
58
|
+
)
|
|
59
|
+
}
|
package/src/rpc.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Rpc } from "@opencode/plugin/rpc"
|
|
2
|
+
|
|
3
|
+
const statusSchema = {
|
|
4
|
+
type: "object",
|
|
5
|
+
properties: {
|
|
6
|
+
sessionID: { type: "string" },
|
|
7
|
+
providerID: { type: "string" },
|
|
8
|
+
modelID: { type: "string" },
|
|
9
|
+
period: { type: "string", enum: ["peak", "off-peak"] },
|
|
10
|
+
scheduledPeriod: { type: "string", enum: ["peak", "off-peak"] },
|
|
11
|
+
source: { type: "string", enum: ["official-schedule", "api-header"] },
|
|
12
|
+
phase: { type: "string", enum: ["request", "response"] },
|
|
13
|
+
mismatch: { type: "boolean" },
|
|
14
|
+
observedAt: { type: "number" },
|
|
15
|
+
evidence: { type: "string" },
|
|
16
|
+
holiday: { type: "string" },
|
|
17
|
+
},
|
|
18
|
+
required: [
|
|
19
|
+
"sessionID",
|
|
20
|
+
"providerID",
|
|
21
|
+
"modelID",
|
|
22
|
+
"period",
|
|
23
|
+
"scheduledPeriod",
|
|
24
|
+
"source",
|
|
25
|
+
"phase",
|
|
26
|
+
"mismatch",
|
|
27
|
+
"observedAt",
|
|
28
|
+
"evidence",
|
|
29
|
+
],
|
|
30
|
+
additionalProperties: false,
|
|
31
|
+
} as const
|
|
32
|
+
|
|
33
|
+
export const DeepSeekPeakRpc = Rpc.define({
|
|
34
|
+
id: "deepseek-peak",
|
|
35
|
+
methods: {
|
|
36
|
+
status: {
|
|
37
|
+
input: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: { sessionID: { type: "string" } },
|
|
40
|
+
required: ["sessionID"],
|
|
41
|
+
additionalProperties: false,
|
|
42
|
+
},
|
|
43
|
+
output: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
found: { type: "boolean" },
|
|
47
|
+
status: statusSchema,
|
|
48
|
+
},
|
|
49
|
+
required: ["found"],
|
|
50
|
+
additionalProperties: false,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
preview: {
|
|
54
|
+
input: {
|
|
55
|
+
type: "object",
|
|
56
|
+
properties: { sessionID: { type: "string" } },
|
|
57
|
+
required: ["sessionID"],
|
|
58
|
+
additionalProperties: false,
|
|
59
|
+
},
|
|
60
|
+
output: {
|
|
61
|
+
type: "object",
|
|
62
|
+
properties: {
|
|
63
|
+
found: { type: "boolean" },
|
|
64
|
+
status: statusSchema,
|
|
65
|
+
},
|
|
66
|
+
required: ["found"],
|
|
67
|
+
additionalProperties: false,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
events: {
|
|
72
|
+
updated: { schema: statusSchema },
|
|
73
|
+
},
|
|
74
|
+
})
|
package/src/schedule.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import Holidays from "date-holidays"
|
|
2
|
+
import type { PricePeriod } from "./types.js"
|
|
3
|
+
|
|
4
|
+
const chinaHolidays = new Holidays("CN", { languages: ["en"] })
|
|
5
|
+
|
|
6
|
+
function datePartsInChina(date: Date) {
|
|
7
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
8
|
+
timeZone: "Asia/Shanghai",
|
|
9
|
+
year: "numeric",
|
|
10
|
+
month: "2-digit",
|
|
11
|
+
day: "2-digit",
|
|
12
|
+
}).formatToParts(date)
|
|
13
|
+
const value = (type: Intl.DateTimeFormatPartTypes) =>
|
|
14
|
+
parts.find((part) => part.type === type)?.value ?? ""
|
|
15
|
+
return `${value("year")}-${value("month")}-${value("day")}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Name of the Chinese public holiday covering this instant, when any. */
|
|
19
|
+
export function chinesePublicHoliday(date: Date): string | undefined {
|
|
20
|
+
// Peak windows are 09:00–18:00 in China, so their UTC and China calendar
|
|
21
|
+
// dates align. Noon avoids boundary ambiguity in holiday libraries.
|
|
22
|
+
const chinaDate = datePartsInChina(date)
|
|
23
|
+
const holiday = chinaHolidays.isHoliday(new Date(`${chinaDate}T12:00:00+08:00`))
|
|
24
|
+
if (!Array.isArray(holiday)) return undefined
|
|
25
|
+
return holiday.find((entry) => entry.type === "public")?.name
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isChinesePublicHoliday(date: Date): boolean {
|
|
29
|
+
return chinesePublicHoliday(date) !== undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function peakWindow(date: Date): PricePeriod {
|
|
33
|
+
const hour = date.getUTCHours() + date.getUTCMinutes() / 60
|
|
34
|
+
return (hour >= 1 && hour < 4) || (hour >= 6 && hour < 10) ? "peak" : "off-peak"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ScheduleInfo {
|
|
38
|
+
period: PricePeriod
|
|
39
|
+
/** Set when a Chinese public holiday makes the whole day off-peak. */
|
|
40
|
+
holiday?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* DeepSeek's official pricing schedule as published in September 2026:
|
|
45
|
+
* 01:00–04:00 and 06:00–10:00 UTC, Monday–Friday, excluding Chinese
|
|
46
|
+
* public holidays. Interval ends are exclusive.
|
|
47
|
+
*/
|
|
48
|
+
export function scheduleInfo(
|
|
49
|
+
date: Date,
|
|
50
|
+
holidayName: (value: Date) => string | undefined = chinesePublicHoliday,
|
|
51
|
+
): ScheduleInfo {
|
|
52
|
+
const day = date.getUTCDay()
|
|
53
|
+
if (day === 0 || day === 6) return { period: "off-peak" }
|
|
54
|
+
const holiday = holidayName(date)
|
|
55
|
+
if (holiday) return { period: "off-peak", holiday }
|
|
56
|
+
return { period: peakWindow(date) }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function officialPricePeriod(
|
|
60
|
+
date: Date,
|
|
61
|
+
holidayCheck: (value: Date) => boolean = isChinesePublicHoliday,
|
|
62
|
+
): PricePeriod {
|
|
63
|
+
const day = date.getUTCDay()
|
|
64
|
+
if (day === 0 || day === 6 || holidayCheck(date)) return "off-peak"
|
|
65
|
+
return peakWindow(date)
|
|
66
|
+
}
|
package/src/tui.tsx
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
|
|
3
|
+
import type { Plugin } from "@opencode/plugin/tui"
|
|
4
|
+
import { createSignal } from "solid-js"
|
|
5
|
+
import { INITIAL_SETTINGS, statusLabel } from "./core/display.js"
|
|
6
|
+
import { createT, detectLang, type LangCode } from "./i18n.js"
|
|
7
|
+
import { FooterStatus } from "./panel/FooterStatus.js"
|
|
8
|
+
import { PeakPanel } from "./panel/PeakPanel.js"
|
|
9
|
+
import { DeepSeekPeakRpc } from "./rpc.js"
|
|
10
|
+
import type { PeakStatus, TuiSettings } from "./types.js"
|
|
11
|
+
import { CommandRoot } from "./v2/commands.js"
|
|
12
|
+
|
|
13
|
+
const mod: Plugin.Definition = {
|
|
14
|
+
id: "opencode-deepseek-peak-tui",
|
|
15
|
+
setup(context) {
|
|
16
|
+
const client = context.client.rpc(DeepSeekPeakRpc)
|
|
17
|
+
const [settings, update] = context.storage.store<TuiSettings>(
|
|
18
|
+
"opencode-deepseek-peak.settings",
|
|
19
|
+
{ initial: { ...INITIAL_SETTINGS, lang: detectLang() } },
|
|
20
|
+
)
|
|
21
|
+
// Stored settings may predate the `lang` field.
|
|
22
|
+
const lang = () => (settings.lang ?? detectLang()) as LangCode
|
|
23
|
+
const t = createT(lang)
|
|
24
|
+
const [statuses, setStatuses] = createSignal<Record<string, PeakStatus>>({})
|
|
25
|
+
const lastToast = new Map<string, string>()
|
|
26
|
+
|
|
27
|
+
const unsubscribe = client.events.on("updated", (event) => {
|
|
28
|
+
const status = event.data as unknown as PeakStatus
|
|
29
|
+
setStatuses((current) => ({ ...current, [status.sessionID]: status }))
|
|
30
|
+
if (!settings.toast) return
|
|
31
|
+
const fingerprint = `${status.period}:${status.mismatch}`
|
|
32
|
+
if (!settings.toastEveryRequest && lastToast.get(status.sessionID) === fingerprint) return
|
|
33
|
+
lastToast.set(status.sessionID, fingerprint)
|
|
34
|
+
context.ui.toast.show({
|
|
35
|
+
title: t("toast.title"),
|
|
36
|
+
message: statusLabel(status, t),
|
|
37
|
+
variant: status.period === "peak" ? "warning" : "success",
|
|
38
|
+
duration: 4500,
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const readStatus = async (sessionID: string) => {
|
|
43
|
+
const result = await client.status({ sessionID }) as { found: boolean; status?: PeakStatus }
|
|
44
|
+
if (result.found && result.status) return result.status
|
|
45
|
+
try {
|
|
46
|
+
const preview = await client.preview({ sessionID }) as { found: boolean; status?: PeakStatus }
|
|
47
|
+
return preview.found ? preview.status : undefined
|
|
48
|
+
} catch {
|
|
49
|
+
return undefined
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Pull the server's last observed status so the indicators render as soon
|
|
54
|
+
// as a session opens, not only after the next DeepSeek request.
|
|
55
|
+
const refresh = (sessionID: string) => {
|
|
56
|
+
void readStatus(sessionID)
|
|
57
|
+
.then((status) => {
|
|
58
|
+
if (status) setStatuses((current) => ({ ...current, [status.sessionID]: status }))
|
|
59
|
+
})
|
|
60
|
+
.catch(() => {})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const unregisterCommands = context.ui.slot({
|
|
64
|
+
append: "app",
|
|
65
|
+
render: () => (
|
|
66
|
+
<CommandRoot context={context} settings={settings} update={update} readStatus={readStatus} lang={lang} />
|
|
67
|
+
),
|
|
68
|
+
})
|
|
69
|
+
const unregisterSidebar = context.ui.slot({
|
|
70
|
+
append: "sidebar.content",
|
|
71
|
+
render: (props) => {
|
|
72
|
+
const sessionID = String(props.sessionID ?? "")
|
|
73
|
+
return (
|
|
74
|
+
<PeakPanel
|
|
75
|
+
context={context}
|
|
76
|
+
status={() => statuses()[sessionID]}
|
|
77
|
+
enabled={() => settings.sidebarIndicator !== false}
|
|
78
|
+
evidence={() => settings.sidebarEvidence !== false}
|
|
79
|
+
lang={lang}
|
|
80
|
+
sessionID={sessionID}
|
|
81
|
+
refresh={refresh}
|
|
82
|
+
/>
|
|
83
|
+
)
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
const unregisterFooter = context.ui.slot({
|
|
87
|
+
append: "prompt.footer.status",
|
|
88
|
+
render: (props) => {
|
|
89
|
+
const sessionID = String(props.sessionID ?? "")
|
|
90
|
+
return (
|
|
91
|
+
<FooterStatus
|
|
92
|
+
context={context}
|
|
93
|
+
status={() => statuses()[sessionID]}
|
|
94
|
+
enabled={() => settings.indicator}
|
|
95
|
+
lang={lang}
|
|
96
|
+
sessionID={sessionID}
|
|
97
|
+
refresh={refresh}
|
|
98
|
+
/>
|
|
99
|
+
)
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
return () => {
|
|
104
|
+
unsubscribe()
|
|
105
|
+
unregisterCommands()
|
|
106
|
+
unregisterSidebar()
|
|
107
|
+
unregisterFooter()
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default mod
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { LangCode } from "./i18n.js"
|
|
2
|
+
|
|
3
|
+
export type PricePeriod = "peak" | "off-peak"
|
|
4
|
+
export type StatusSource = "official-schedule" | "api-header"
|
|
5
|
+
export type StatusPhase = "request" | "response"
|
|
6
|
+
|
|
7
|
+
export interface PeakStatus {
|
|
8
|
+
sessionID: string
|
|
9
|
+
providerID: string
|
|
10
|
+
modelID: string
|
|
11
|
+
period: PricePeriod
|
|
12
|
+
scheduledPeriod: PricePeriod
|
|
13
|
+
source: StatusSource
|
|
14
|
+
phase: StatusPhase
|
|
15
|
+
mismatch: boolean
|
|
16
|
+
observedAt: number
|
|
17
|
+
evidence: string
|
|
18
|
+
/** Chinese public holiday making the whole day off-peak, when applicable. */
|
|
19
|
+
holiday?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TuiSettings {
|
|
23
|
+
toast: boolean
|
|
24
|
+
indicator: boolean
|
|
25
|
+
sidebarIndicator: boolean
|
|
26
|
+
sidebarEvidence: boolean
|
|
27
|
+
toastEveryRequest: boolean
|
|
28
|
+
lang: LangCode
|
|
29
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode/plugin/tui"
|
|
2
|
+
import { statusLabel } from "../core/display.js"
|
|
3
|
+
import { createT, LANG_META, type LangCode } from "../i18n.js"
|
|
4
|
+
import type { PeakStatus, TuiSettings } from "../types.js"
|
|
5
|
+
import { openSettingsMenu } from "./settings-menu.js"
|
|
6
|
+
|
|
7
|
+
interface CommandProps {
|
|
8
|
+
context: Plugin.Context
|
|
9
|
+
settings: TuiSettings
|
|
10
|
+
update: (mutation: (draft: TuiSettings) => void) => Promise<void>
|
|
11
|
+
readStatus: (sessionID: string) => Promise<PeakStatus | undefined>
|
|
12
|
+
lang: () => LangCode
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Command layer (registered in the app slot: slash commands stay available when the sidebar is hidden). */
|
|
16
|
+
export function CommandRoot(props: CommandProps) {
|
|
17
|
+
const { context } = props
|
|
18
|
+
const t = createT(() => props.lang())
|
|
19
|
+
|
|
20
|
+
context.keymap.layer(() => ({
|
|
21
|
+
mode: "global" as const,
|
|
22
|
+
commands: [
|
|
23
|
+
{
|
|
24
|
+
id: "opencode-deepseek-peak.peak.status",
|
|
25
|
+
title: t("command.status.title"),
|
|
26
|
+
description: t("command.status.description"),
|
|
27
|
+
group: "DeepSeek",
|
|
28
|
+
palette: true,
|
|
29
|
+
slash: { name: "deepseek-peak" },
|
|
30
|
+
run: async () => {
|
|
31
|
+
const route = context.ui.router.current()
|
|
32
|
+
if (route.type !== "session") {
|
|
33
|
+
await context.ui.dialog.alert({ title: t("status.title"), message: t("status.noSession") })
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const status = await props.readStatus(route.sessionID)
|
|
38
|
+
await context.ui.dialog.alert({
|
|
39
|
+
title: t("status.title"),
|
|
40
|
+
message: status
|
|
41
|
+
? `${statusLabel(status, t)}\n${t("status.source")}: ${status.source}\n${t("status.evidence")}: ${status.evidence}`
|
|
42
|
+
: t("status.noRequest"),
|
|
43
|
+
})
|
|
44
|
+
} catch {
|
|
45
|
+
context.ui.toast.show({ message: t("status.unavailable"), variant: "error" })
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "opencode-deepseek-peak.peak.sections",
|
|
51
|
+
title: t("command.sections.title"),
|
|
52
|
+
description: t("command.sections.description"),
|
|
53
|
+
group: "DeepSeek",
|
|
54
|
+
palette: true,
|
|
55
|
+
slash: { name: "deepseek-peak-sections" },
|
|
56
|
+
run: () => {
|
|
57
|
+
openSettingsMenu(context, props.settings, props.update, props.lang)
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "opencode-deepseek-peak.peak.lang",
|
|
62
|
+
title: t("command.lang.title"),
|
|
63
|
+
description: t("command.lang.description"),
|
|
64
|
+
group: "DeepSeek",
|
|
65
|
+
palette: true,
|
|
66
|
+
slash: { name: "deepseek-peak-lang" },
|
|
67
|
+
run: async () => {
|
|
68
|
+
const picked = await context.ui.dialog.select<LangCode>({
|
|
69
|
+
title: t("settings.lang"),
|
|
70
|
+
options: LANG_META.map((meta) => ({ title: meta.label, value: meta.code })),
|
|
71
|
+
current: props.lang(),
|
|
72
|
+
})
|
|
73
|
+
if (picked !== undefined) {
|
|
74
|
+
await props.update((draft) => {
|
|
75
|
+
draft.lang = picked
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
}))
|
|
82
|
+
return null
|
|
83
|
+
}
|