@qthinker/opencode-deepseek-search 0.3.0 → 0.4.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/CHANGELOG.md +42 -0
- package/README.md +50 -31
- package/gui-experimental.cjs +1 -1
- package/package.json +86 -86
- package/src/deepseek.ts +137 -11
- package/src/gui/index.tsx +13 -7
- package/src/gui/panel.tsx +79 -13
- package/src/i18n/en.ts +22 -3
- package/src/i18n/index.ts +8 -6
- package/src/i18n/ja.ts +13 -4
- package/src/i18n/zh.ts +14 -4
- package/src/index.ts +108 -11
- package/src/rpc.ts +67 -2
- package/src/status-view.ts +147 -32
- package/src/tui/card.ts +71 -8
- package/src/tui/main.ts +13 -5
package/src/tui/card.ts
CHANGED
|
@@ -9,6 +9,10 @@ import type { SidebarRows } from "../status-view.ts"
|
|
|
9
9
|
|
|
10
10
|
/** A measured width under this cannot show even the usage label; treat it as unmeasured. */
|
|
11
11
|
const MIN_CARD_COLUMNS = 8
|
|
12
|
+
/** Body rows are inset this far from the title, so the card reads as one block. */
|
|
13
|
+
const INDENT = " "
|
|
14
|
+
/** Under this many columns the indent costs more than it explains. */
|
|
15
|
+
const INDENT_MIN_COLUMNS = 16
|
|
12
16
|
|
|
13
17
|
/**
|
|
14
18
|
* Columns the card may use. Before the first layout pass the host reports 0 (or
|
|
@@ -69,6 +73,40 @@ function element(tag: string, props: Props | undefined, children: readonly Child
|
|
|
69
73
|
return node
|
|
70
74
|
}
|
|
71
75
|
|
|
76
|
+
/** The amount at the end of a row, e.g. `¥0.015` / `$6.17`; it gets its own color. */
|
|
77
|
+
const MONEY_TAIL = /([¥$][\d.,]+)$/u
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* One body row split into the three tiers the card colors. The view model emits
|
|
81
|
+
* plain strings — one row per line, deliberately not structured — so the row is
|
|
82
|
+
* split here: the leading label reads faint, the counters in the middle muted,
|
|
83
|
+
* and the amount at the end is the number the user came for, so it gets full
|
|
84
|
+
* contrast.
|
|
85
|
+
*/
|
|
86
|
+
interface BodyParts {
|
|
87
|
+
readonly label: string
|
|
88
|
+
readonly middle: string
|
|
89
|
+
readonly amount: string
|
|
90
|
+
/** The row starts with `!`: the balance could not be read. */
|
|
91
|
+
readonly unavailable: boolean
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function bodyParts(line: string): BodyParts {
|
|
95
|
+
// `!` is the only unavailable marker in the row text and is consumed here as a
|
|
96
|
+
// color directive: it is never drawn. The label and the amount still are.
|
|
97
|
+
const unavailable = line.startsWith("!")
|
|
98
|
+
const text = unavailable ? line.slice(1) : line
|
|
99
|
+
const amount = MONEY_TAIL.exec(text)?.[1] ?? ""
|
|
100
|
+
const head = amount === "" ? text : text.slice(0, text.length - amount.length)
|
|
101
|
+
const gap = head.indexOf(" ")
|
|
102
|
+
return {
|
|
103
|
+
label: gap < 0 ? head : head.slice(0, gap),
|
|
104
|
+
middle: gap < 0 ? "" : head.slice(gap),
|
|
105
|
+
amount,
|
|
106
|
+
unavailable,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
72
110
|
export function sidebarCard(input: {
|
|
73
111
|
readonly status: SearchStatus | undefined
|
|
74
112
|
readonly error: string | undefined
|
|
@@ -85,10 +123,14 @@ export function sidebarCard(input: {
|
|
|
85
123
|
}): JSX.Element {
|
|
86
124
|
// The terminal sidebar is the surface this ladder is tuned for: the measured
|
|
87
125
|
// width, not a guessed constant, decides whether the words fit.
|
|
126
|
+
const columns = cardColumns(input.maxColumns)
|
|
127
|
+
// The indent is part of the drawn row, so it is subtracted from the budget the
|
|
128
|
+
// model composes against: every row still ends inside the measured width.
|
|
129
|
+
const indent = columns >= INDENT_MIN_COLUMNS ? INDENT : ""
|
|
88
130
|
const rows = sidebarRows({
|
|
89
131
|
status: input.status,
|
|
90
132
|
error: input.error,
|
|
91
|
-
maxColumns:
|
|
133
|
+
maxColumns: columns - indent.length,
|
|
92
134
|
language: input.language,
|
|
93
135
|
})
|
|
94
136
|
const theme = input.theme
|
|
@@ -108,14 +150,18 @@ export function sidebarCard(input: {
|
|
|
108
150
|
//
|
|
109
151
|
// The state word carries the tone color, so a narrow sidebar that has to drop
|
|
110
152
|
// the glyph still shows the state at a glance (the desktop panel does the same).
|
|
153
|
+
// When the glyph is gone the word also goes bold — the emphasis has to live
|
|
154
|
+
// somewhere. Bold rides in `style`, because the reconciler ignores an
|
|
155
|
+
// `attributes` prop on text nodes and only maps `style` (fg/bg/attributes).
|
|
156
|
+
const stateStyle = rows.glyph ? { fg: state } : { fg: state, bold: true }
|
|
111
157
|
const separator = rows.title.indexOf(" · ")
|
|
112
158
|
const titleSpans: Child[] =
|
|
113
159
|
separator < 0
|
|
114
|
-
? [element("span", { style:
|
|
160
|
+
? [element("span", { style: stateStyle }, [rows.title])]
|
|
115
161
|
: [
|
|
116
162
|
element("span", { style: { fg: theme.text.base } }, [rows.title.slice(0, separator)]),
|
|
117
163
|
element("span", { style: { fg: theme.text.faint } }, [" · "]),
|
|
118
|
-
element("span", { style:
|
|
164
|
+
element("span", { style: stateStyle }, [rows.title.slice(separator + 3)]),
|
|
119
165
|
]
|
|
120
166
|
if (rows.glyph) {
|
|
121
167
|
titleSpans.unshift(element("span", { style: { fg: state } }, [`${GLYPH[rows.tone]} `]))
|
|
@@ -157,11 +203,28 @@ export function sidebarCard(input: {
|
|
|
157
203
|
[action],
|
|
158
204
|
)
|
|
159
205
|
|
|
160
|
-
// The model already composed every row to fit the sidebar, so each body row
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
)
|
|
206
|
+
// The model already composed every row to fit the sidebar, so each body row is
|
|
207
|
+
// one text node: the host must never get the chance to wrap it. The row's own
|
|
208
|
+
// text is split into colored spans, so the label, the counters and the amount
|
|
209
|
+
// each read at their own weight without a second row or a box per line.
|
|
210
|
+
const body = rows.lines.map((line) => {
|
|
211
|
+
const parts = bodyParts(line)
|
|
212
|
+
const spans: Child[] = []
|
|
213
|
+
if (indent !== "") spans.push(element("span", { style: { fg: theme.text.faint } }, [indent]))
|
|
214
|
+
if (parts.label !== "") {
|
|
215
|
+
spans.push(element("span", { style: { fg: theme.text.faint } }, [parts.label]))
|
|
216
|
+
}
|
|
217
|
+
if (parts.middle !== "") {
|
|
218
|
+
spans.push(element("span", { style: { fg: theme.text.muted } }, [parts.middle]))
|
|
219
|
+
}
|
|
220
|
+
if (parts.amount !== "") {
|
|
221
|
+
// An unavailable balance is the one thing on the card that must not look
|
|
222
|
+
// like an ordinary amount: it is the reason the next call will fail.
|
|
223
|
+
const fg = parts.unavailable ? theme.text.feedback.warning.base : theme.text.base
|
|
224
|
+
spans.push(element("span", { style: { fg } }, [parts.amount]))
|
|
225
|
+
}
|
|
226
|
+
return element("text", { selectable: false, wrapMode: "none", width: "100%" }, spans)
|
|
227
|
+
})
|
|
165
228
|
// A missing key has no action, so the fix hint gets the row right under the
|
|
166
229
|
// title. It is plain text: there is nothing to click.
|
|
167
230
|
const hint =
|
package/src/tui/main.ts
CHANGED
|
@@ -35,9 +35,14 @@ export default Plugin.define({
|
|
|
35
35
|
// `status` takes a required first argument: plain JSON Schema degrades the
|
|
36
36
|
// input type to `unknown`, and the host validates the payload against the
|
|
37
37
|
// method's object schema, so an omitted input is rejected as invalid.
|
|
38
|
-
|
|
38
|
+
//
|
|
39
|
+
// `force` asks the server to re-probe the account balance instead of serving
|
|
40
|
+
// the cache; only a user action may spend that request. The timer and the
|
|
41
|
+
// `changed` event therefore send `{}` exactly — an older server rejects
|
|
42
|
+
// unknown input properties, so a stray `force: false` would break it.
|
|
43
|
+
async function refresh(force = false) {
|
|
39
44
|
try {
|
|
40
|
-
const next = readStatus(await rpc.status({}))
|
|
45
|
+
const next = readStatus(await rpc.status(force ? { force: true } : {}))
|
|
41
46
|
if (next) {
|
|
42
47
|
setStatus(next)
|
|
43
48
|
setError(undefined)
|
|
@@ -65,11 +70,12 @@ export default Plugin.define({
|
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
// The card labels the action it can really perform: switching the takeover
|
|
68
|
-
// when a status is known, retrying the read when it is not.
|
|
73
|
+
// when a status is known, retrying the read when it is not. A retry is a user
|
|
74
|
+
// action, so it refreshes the cached balance snapshot as well.
|
|
69
75
|
function toggle() {
|
|
70
76
|
const current = status()
|
|
71
77
|
if (!current || error()) {
|
|
72
|
-
void refresh()
|
|
78
|
+
void refresh(true)
|
|
73
79
|
return
|
|
74
80
|
}
|
|
75
81
|
void applyMode(current.mode === "deepseek" ? "native" : "deepseek")
|
|
@@ -88,7 +94,9 @@ export default Plugin.define({
|
|
|
88
94
|
current: status()?.mode,
|
|
89
95
|
})
|
|
90
96
|
if (choice === undefined) return
|
|
91
|
-
|
|
97
|
+
// The dialog's refresh is an explicit "show me now", so it bypasses the
|
|
98
|
+
// server's balance cache; enabling/disabling refreshes through applyMode.
|
|
99
|
+
if (choice === "refresh") await refresh(true)
|
|
92
100
|
else await applyMode(choice)
|
|
93
101
|
}
|
|
94
102
|
|