@qthinker/opencode-deepseek-search 0.1.1 → 0.2.1

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 ADDED
@@ -0,0 +1,60 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
4
+ and the version numbers follow [Semantic Versioning](https://semver.org/).
5
+
6
+ ## [0.2.1] - 2026-11-02
7
+
8
+ ### Changed
9
+
10
+ - Release plumbing only: this version exists to validate the package's npm **Trusted Publishing** connection, so
11
+ tag-triggered releases publish with provenance and no long-lived token. No functional change since 0.2.0.
12
+
13
+ ## [0.2.0] - 2026-11-02
14
+
15
+ ### Added
16
+
17
+ - **TUI sidebar card** (`sidebar.content`): takeover state, today / month / lifetime usage, and a one-click
18
+ enable/disable at the right edge of the title. Everything degrades with the sidebar's *measured* width — full
19
+ `Search` / `Fetch` words when they fit, abbreviations only when they do not, never a wrapped row. `/deepseek`
20
+ opens the same actions as a menu, and the card also appears when the takeover is disabled.
21
+ - **Desktop side panel**: a DeepSeek tab in a session's side region, listed in the side region's *New tab* page and
22
+ its `+` menu, plus the `DeepSeek: 打开统计面板` command. Ships as the prebuilt CommonJS `gui-experimental.cjs`
23
+ bundle the desktop app requires.
24
+ - **RPC contract between the halves** (`status`, `setMode`, a `changed` event), so both sidebars read the same ledger
25
+ and the same stored switch as the tools. There is no second source of truth.
26
+ - **`counts` in the `status` payload** (optional): structured today / month / lifetime counters, so each surface can
27
+ lay the numbers out for its own width. Older clients keep working off the pre-formatted strings.
28
+ - **Packaging regression test**: `package.json` must stay free of `exports`, and the root entries (`index.ts`,
29
+ `tui.ts`, `gui-experimental.cjs`) must keep resolving for both directory and installed-package resolution.
30
+
31
+ ### Changed
32
+
33
+ - `peerDependencies["@opencode/plugin"]` is now `>=2.2.5`; `@opentui/core`, `@opentui/solid`, `solid-js` and
34
+ `@opencode/theme` are declared as optional peers, because the host provides them.
35
+ - `npm run typecheck` covers the GUI project as well; `prepublishOnly` builds the GUI bundle; CI builds and checks
36
+ the bundle before running the tests, and the release workflow does the same before publishing.
37
+ - Docs: which file enables which half (`opencode.json(c)` for server + desktop, `cli.json` for the TUI), the reload
38
+ rules for each half, and troubleshooting entries for the three “nothing shows up” cases.
39
+
40
+ ### Fixed
41
+
42
+ - The TUI card no longer wraps its usage rows in a narrow sidebar, and its action row can no longer be clipped.
43
+ - `status` RPC calls must send an empty object; an omitted input was rejected as `rpc.invalid_input` (400).
44
+ - The desktop panel now registers a `session.panel` menu item, so it appears in the side region's launcher instead of
45
+ being reachable only from the command palette.
46
+ - Removed the UTF-8 BOM from `package.json`, which made Vite's JSON parse fail while building the GUI bundle.
47
+
48
+ ## [0.1.1] - 2026-11-01
49
+
50
+ ### Changed
51
+
52
+ - npm landing page uses the English README, plus badges; repository URLs aligned with the public repo.
53
+
54
+ ## [0.1.0] - 2026-11-01
55
+
56
+ ### Added
57
+
58
+ - First release: the built-in `websearch` tool routed to DeepSeek Search, `deepseek_fetch` (clean Markdown),
59
+ `deepseek_usage` (local counts and estimated cost), `deepseek_mode` (persisted backend switch), and the
60
+ `tools/switch-mode.ps1` helper.
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 QThinker
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 QThinker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -6,9 +6,10 @@
6
6
  [![opencode](https://img.shields.io/badge/opencode-%E2%89%A52.2.5-black)](https://opencode.ai)
7
7
 
8
8
  DeepSeek Search & Fetch as an OpenCode V2 plugin: **the built-in `websearch` tool routes to DeepSeek**, plus a clean
9
- Markdown fetch tool, local usage/cost stats, and a **persisted** backend switch.
9
+ Markdown fetch tool, local usage/cost stats, a **persisted** backend switch, and **TUI + desktop sidebars** that show
10
+ the state and the numbers and flip the takeover with one click.
10
11
 
11
- Three ways back to native search: change the `mode` option, ask the model to call `deepseek_mode`, or run
12
+ Ways back to native search: the sidebar switch, the `mode` option, a `deepseek_mode` call, or
12
13
  `tools/switch-mode.ps1` from the repo.
13
14
  中文说明见 [docs/README.zh-CN.md](https://github.com/QThinkerJR/opencode-deepseek-search/blob/main/docs/README.zh-CN.md)。
14
15
 
@@ -23,6 +24,8 @@ runtime**, and reports **local usage and estimated cost**.
23
24
  - `opencode >= 2.2.5` (V2 plugin API)
24
25
  - Node.js `>= 22.18` (the plugin ships TypeScript sources)
25
26
  - A DeepSeek API key with Search/Fetch access
27
+ - For the sidebars: nothing extra — the TUI and the desktop app provide the renderer (`@opentui/*`, `solid-js`) and
28
+ load the plugin's two halves from the same package
26
29
 
27
30
  ## Install
28
31
 
@@ -42,6 +45,8 @@ Then add the plugin to `opencode.jsonc`:
42
45
  }
43
46
  ```
44
47
 
48
+ > **Which file loads which half.** The server plugin and the desktop panel are read from `opencode.json(c)` — the desktop app reads only that file, and a **global** `~/.config/opencode/opencode.jsonc` entry is what makes the panel appear in every window. The TUI half is read from `cli.json` (`"plugins": [...]`). So a plugin is enabled per half: list it in each file whose half you want, whether it comes from npm (by package name) or from a local directory (by path).
49
+
45
50
  With options (recommended — this is where `mode` and friends live):
46
51
 
47
52
  ```jsonc
@@ -64,13 +69,17 @@ Local development points at the repository directory (a config entry must be a *
64
69
  ```jsonc
65
70
  {
66
71
  "plugins": [
67
- { "package": "D:/MyDev/opencode-deepseek-search", "options": { "mode": "deepseek" } }
72
+ { "package": "D:/repos/opencode-deepseek-search", "options": { "mode": "deepseek" } }
68
73
  ]
69
74
  }
70
75
  ```
71
76
 
72
- `opencode plugin list` should show `deepseek-search`. Config changes take effect immediately: the plugin is
73
- re-run in place, no `/reload` or service restart needed.
77
+ `opencode plugin add <package-or-directory>` does the same edit for you (it writes the **global** config), and
78
+ `opencode plugin list` should then show `deepseek-search` with source `local` or the package name.
79
+
80
+ Reload rules: the **server half** re-runs in place when you save a source file — no `/reload`, no service restart.
81
+ The other two halves are clients: restart the **TUI**, and reload the **desktop window**, to pick up a change to
82
+ `tui.ts` / `gui-experimental.cjs` (and run `npm run build:gui` first when working from a clone).
74
83
 
75
84
  ## Options
76
85
 
@@ -97,10 +106,11 @@ and `need_content` is never requested.
97
106
 
98
107
  ## Switching backends
99
108
 
100
- ### Three ways
109
+ ### Ways to switch
101
110
 
102
111
  | Way | How | Persisted | Use it for |
103
112
  |---|---|---|---|
113
+ | Sidebar | Click the action on the TUI card (`[停用]` / `[启用]`), or the panel's button | Yes (plugin storage `mode/v1`) | While you are working |
104
114
  | Config | Edit `options.mode`; applies on save | Yes (in the config file) | The long-term default |
105
115
  | Tool | Ask the model to call `deepseek_mode`, or call it yourself | Yes (plugin storage `mode/v1`) | Switching mid-session without touching files |
106
116
  | Script | `.\tools\switch-mode.ps1 native` | Yes (the script edits the config) | Quick switch from a terminal, no editor |
@@ -139,6 +149,33 @@ explicitly with `providerID: "deepseek"` or config `"websearch": { "provider": "
139
149
  > provider" form never appears in deepseek mode. If a version does not expose that tool, `deepseek_mode` says it cannot
140
150
  > switch at runtime — use `websearch.provider` in the config instead.
141
151
 
152
+ ## TUI and desktop sidebars
153
+
154
+ The plugin also shows up where you work, and can be switched without the model or the config file:
155
+
156
+ | Surface | What you get | How to switch |
157
+ |---|---|---|
158
+ | TUI | A DeepSeek block appended to the session sidebar: state, then 今日 / 本月 / 累计 rows (`Search` · `Fetch` · `¥` estimate); it degrades to abbreviations and then to fewer columns as the sidebar narrows | Click the action at the right of the title, or run the `/deepseek` slash command for a menu |
159
+ | Desktop | A **DeepSeek** tab in a session's side region, listed in the side region's *New tab* page and its `+` menu, and openable with the `DeepSeek: 打开统计面板` command | The panel's button, plus a refresh control |
160
+
161
+ - Both surfaces read the same ledger and the same stored switch as `deepseek_usage` / `deepseek_mode`: a single
162
+ server-side source, reached over the plugin's own RPC, so the numbers never diverge between TUI and desktop.
163
+ - **"Stop" means hand the built-in `websearch` back to your configured backend** (exactly `deepseek_mode({"mode": "native"})`).
164
+ It is not an uninstall: `deepseek_fetch`, `deepseek_usage` and `deepseek_mode` stay registered, and the state is
165
+ persisted in `mode/v1` like any other switch.
166
+ - The action row always says what a click does. When no API key is configured there is no action to take, so it shows
167
+ the fix instead (`DEEPSEEK_API_KEY`); when the built-in tool could not be patched it reports 未接管 rather than
168
+ pretending to be enabled.
169
+ - Stats refresh on a `changed` event emitted after every tally and every switch, with a 10-second reread as a
170
+ fallback in both surfaces.
171
+
172
+ The desktop half ships as a prebuilt CommonJS bundle (`gui-experimental.cjs`). Published packages include it; if you
173
+ run the plugin from a clone, build it once — the TUI half needs no build and loads from TypeScript like the server half:
174
+
175
+ ```bash
176
+ npm install && npm run build:gui
177
+ ```
178
+
142
179
  ## Usage and cost
143
180
 
144
181
  | Counter | Counted when | Billed |
@@ -181,16 +218,21 @@ contain text that tries to steer the model.
181
218
  | `429` rate limited | Backed off once already; retry later |
182
219
  | `403` / `404` on fetch | The site blocks extraction or the page has no readable text; try the built-in `webfetch` |
183
220
  | Search still hits the native backend | `mode` or the persisted switch is `native`; if both are `deepseek`, the built-in `websearch` tool was not found — use config `"websearch": { "provider": "deepseek" }` |
221
+ | TUI sidebar has no DeepSeek card | The TUI half is enabled in `cli.json` (`"plugins": [...]`), not in `opencode.jsonc`; add the path/package there and restart the TUI |
222
+ | Desktop has no DeepSeek entry in the *New tab* list | The desktop reads plugins from the **global** `opencode.jsonc`, and its bundle must exist — run `npm run build:gui` when working from a clone, then reload the window |
223
+ | Sidebar says `读取失败` | The half cannot reach the RPC: check `opencode plugin list` reports `deepseek-search` as active, then look for `/api/rpc/deepseek-search/status` in `~/.local/share/opencode/log/opencode.log`. A `400 rpc.invalid_input` means a half and the server disagree on the contract — reload that client |
184
224
 
185
225
  ## Local development
186
226
 
187
227
  ```powershell
188
- cd D:\MyDev\opencode-deepseek-search
228
+ cd D:\repos\opencode-deepseek-search
189
229
  npm install
190
- npm run typecheck # tsc --noEmit
230
+ npm run typecheck # tsc --noEmit, both the server/TUI config and the GUI config
191
231
  npm test # node --test, pure functions, zero cost
232
+ npm run build:gui # src/gui -> gui-experimental.cjs (the desktop half's bundle)
233
+ node --experimental-strip-types scripts/check-gui-bundle.ts # bundle shape check, run in CI
192
234
  npm run smoke # real API smoke; skipped without DEEPSEEK_API_KEY
193
- npm pack --dry-run # audit the publish artifacts
235
+ npm pack --dry-run # audit the publish artifacts — run build:gui first, `pack` does not build the bundle
194
236
  ```
195
237
 
196
238
  One-command switch (repo-only helper; edits `options.mode` in `opencode.jsonc`, backing it up to `opencode.jsonc.bak-switch`):
@@ -210,20 +252,27 @@ powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\switch-mode.ps1 nati
210
252
  Shorter, via an alias in `$PROFILE`:
211
253
 
212
254
  ```powershell
213
- function dsmode { & "D:\MyDev\opencode-deepseek-search\tools\switch-mode.ps1" @args }
255
+ function dsmode { & "D:\repos\opencode-deepseek-search\tools\switch-mode.ps1" @args }
214
256
  ```
215
257
 
216
258
  Layout:
217
259
 
218
260
  ```
219
261
  index.ts # directory-plugin entry, re-exports src/index.ts
262
+ tui.ts # TUI entry, re-exports src/tui/main.ts
220
263
  src/index.ts # plugin: options, registration, websearch patch, three tools
221
264
  src/deepseek.ts # HTTP core: timeout/cancel, single backoff retry, Chinese errors, mapping
222
265
  src/usage.ts # pure ledger: micro-CNY pricing, 60-day prune, defensive parse, formatting
266
+ src/rpc.ts # RPC contract shared by the server, TUI and desktop halves
267
+ src/status-view.ts # pure view model both sidebars derive their copy from
268
+ src/tui/ # TUI half: card element tree + plugin wiring
269
+ src/gui/ # desktop half: side panel component + plugin wiring
270
+ vite.config.ts # builds src/gui into the root gui-experimental.cjs bundle
223
271
  test/*.test.ts # pure-function tests
224
272
  scripts/smoke.ts # real-API smoke (5 steps)
273
+ scripts/check-gui-bundle.ts # bundle shape check, run in CI after build:gui
225
274
  tools/switch-mode.ps1 # terminal backend switch
226
- .github/workflows/ # ci (typecheck + test) / release (tag-triggered, OIDC provenance)
275
+ .github/workflows/ # ci (build + typecheck + test + bundle check) / release
227
276
  ```
228
277
 
229
278
  Two packaging traps (already avoided — do not reintroduce them):
@@ -233,6 +282,10 @@ Two packaging traps (already avoided — do not reintroduce them):
233
282
  name, and when the resolved entrypoint falls outside the directory it is **silently dropped** (no log line,
234
283
  it just disappears from `plugin list`).
235
284
 
285
+ Both hold for every half of the plugin: `index.ts`, `tui.ts` and `gui-experimental.cjs` sit at the repository root,
286
+ which is what both the directory and the installed-package resolution paths find. `test/packaging.test.ts` fails if
287
+ `exports` comes back or a root entry stops resolving.
288
+
236
289
  Source edits hot-reload on save; verify with `opencode plugin list` plus one real search.
237
290
 
238
291
  ## License
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const o=require("solid-js/web"),_=require("@opencode/plugin/gui"),y=require("solid-js"),Y="deepseek-search",k={type:"object",properties:{search:{type:"integer"},fetch:{type:"integer"},empty:{type:"integer"},failed:{type:"integer"},costMicro:{type:"integer"}},required:["search","fetch","empty","failed","costMicro"],additionalProperties:!1},j={id:Y,methods:{status:{input:{type:"object",properties:{},additionalProperties:!1},output:{type:"object",properties:{mode:{type:"string",enum:["deepseek","native"]},source:{type:"string",enum:["override","config"]},patched:{type:"boolean"},hasKey:{type:"boolean"},pricePerThousand:{type:"number"},today:{type:"string"},month:{type:"string"},total:{type:"string"},counts:{type:"object",properties:{today:k,month:k,total:k},required:["today","month","total"],additionalProperties:!1}},required:["mode","source","patched","hasKey","pricePerThousand","today","month","total"],additionalProperties:!1}},setMode:{input:{type:"object",properties:{mode:{type:"string",enum:["deepseek","native","config"]}},required:["mode"],additionalProperties:!1},output:{type:"object",properties:{ok:{type:"boolean"},mode:{type:"string"},message:{type:"string"}},required:["ok","mode","message"],additionalProperties:!1}}},events:{changed:{schema:{type:"object",properties:{}}}}};function E(t){return typeof t=="object"&&t!==null&&!Array.isArray(t)}function z(t){return t==="deepseek"||t==="native"}function x(t){if(!E(t))return;const{search:e,fetch:r,empty:n,failed:i,costMicro:s}=t;for(const d of[e,r,n,i])if(typeof d!="number"||!Number.isInteger(d)||d<0)return;if(!(typeof s!="number"||!Number.isFinite(s)||s<0))return{search:e,fetch:r,empty:n,failed:i,costMicro:s}}function B(t){if(!E(t))return;const{mode:e,source:r,patched:n,hasKey:i,pricePerThousand:s,today:d,month:u,total:p,counts:a}=t;if(!z(e)||r!=="override"&&r!=="config"||typeof n!="boolean"||typeof i!="boolean"||typeof s!="number"||!Number.isFinite(s)||typeof d!="string"||typeof u!="string"||typeof p!="string")return;const c={mode:e,source:r,patched:n,hasKey:i,pricePerThousand:s,today:d,month:u,total:p};if(E(a)){const f=x(a.today),m=x(a.month),h=x(a.total);f&&m&&h&&(c.counts={today:f,month:m,total:h})}return c}function q(t){if(!E(t))return;const{ok:e,mode:r,message:n}=t;if(!(typeof e!="boolean"||typeof r!="string"||typeof n!="string"))return{ok:e,mode:r,message:n}}const U="DeepSeek search/fetch",D="DeepSeek",H=2,V=1,W=24,N=["today","month","total"],O={today:"今日",month:"本月",total:"累计"};function R(t){const e=t.codePointAt(0)??0;return e===0?0:e>=4352&&e<=4447||e>=11904&&e<=42191||e>=44032&&e<=55203||e>=63744&&e<=64255||e>=65040&&e<=65049||e>=65072&&e<=65135||e>=65280&&e<=65376||e>=65504&&e<=65510||e>=127744&&e<=129791||e>=131072&&e<=262141?2:1}function b(t){let e=0;for(const r of t)e+=R(r);return e}function S(t,e){if(e<1)return"";if(b(t)<=e)return t;let r="",n=0;for(const i of t){const s=R(i);if(n+s>e-1)return`${r}…`;r+=i,n+=s}return r}function G(t,e){const r=t.replace(/\s+/g," ").trim();return S(r||"读取失败",e)}function L(t){let r=(Number.isFinite(t)&&t>0?t/1e6:0).toFixed(3);for(;r.endsWith("0")&&r.length>4;)r=r.slice(0,-1);return`¥${r}`}function J(t){const e=N.map(r=>t[r]);return{search:Math.max(...e.map(r=>String(r.search).length)),fetch:Math.max(...e.map(r=>String(r.fetch).length)),cost:Math.max(...e.map(r=>L(r.costMicro).length))}}function X(t,e,r,n){const i=O[t],s=String(e.search).padStart(r.search," "),d=String(e.fetch).padStart(r.fetch," "),u=L(e.costMicro).padStart(r.cost," "),p=[`${i} Search ${s} Fetch ${d} ${u}`,`${i} Search ${s} Fetch ${d} ${u}`,`${i} Search ${s} Fetch ${d}`,`${i} Search ${s} Fetch ${d}`,`${i} ${s}S ${d}F ${u}`,`${i} ${s}S ${d}F`,`${i} ${s}S`];for(const a of p)if(b(a)<=n)return a;return b(i)<=n?i:S(i,n)}function Q(t,e,r){const n=O[t],i=`${n} ${e}`;if(b(i)<=r)return i;const s=e.split(" · ").filter(u=>!u.startsWith("¥")&&!u.startsWith("空/失")),d=[`${n} ${s.join(" ")}`.trimEnd(),`${n} ${s[0]??""}`.trimEnd(),n];for(const u of d)if(b(u)<=r)return u;return S(n,r)}function Z(t,e){if(e===Number.POSITIVE_INFINITY)return[`今日 ${t.today}`,`本月 ${t.month}`,`累计 ${t.total}`];const r=t.counts;if(r){const n=J(r);return N.map(i=>X(i,r[i],n,e))}return N.map(n=>Q(n,t[n],e))}function ee(t,e){const r=`[${t}]`;if(e===Number.POSITIVE_INFINITY||b(r)<=e)return r;const n=e-2;return n>=1?`[${S(t,n)}]`:S(r,e)}function C(t,e,r,n){const i=(p,a)=>{if(r===Number.POSITIVE_INFINITY)return Number.POSITIVE_INFINITY;const c=p?H:0,f=n===void 0||a?0:n+V;return Math.max(0,r-c-f)},s=[`${U} · ${t}`,`${D} · ${t}`,`${D} · ${e}`,t,e],d=n===void 0?[!1]:[!1,!0];for(const p of s)for(const a of d)for(const c of[!0,!1])if(b(p)<=i(c,a))return{title:p,glyph:c,actionBelow:a};const u=n!==void 0;return{title:S(e,i(!1,u)),glyph:!1,actionBelow:u}}function te(t){const e=["设置 DEEPSEEK_API_KEY 后重启","设置 API KEY 后重启","设置 API KEY","设置 KEY"];if(t===Number.POSITIVE_INFINITY)return e[0];for(const r of e)if(b(r)<=t)return r;return S(e[e.length-1],t)}function P(t,e,r,n){const i=ee(r,n);return{...C(t,e,n,b(i)),action:i}}function re(t){const{status:e,error:r}=t,n=t.maxColumns??W,i=e===void 0?[]:Z(e,n);if(e===void 0)return r===void 0||r.trim()===""?{...P("正在读取…","正在读取…","重试",n),hint:void 0,clickable:!0,lines:i,tone:"idle"}:{...P("读取失败","读取失败","重试",n),hint:void 0,clickable:!0,lines:[G(r,n)],tone:"error"};if(e.patched&&e.hasKey)return{...P("已启用","已启用","停用",n),hint:void 0,clickable:!0,lines:i,tone:"on"};if(!e.patched&&e.mode==="deepseek")return{...P("未接管内置 websearch","未接管","交还",n),hint:void 0,clickable:!0,lines:i,tone:"warn"};if(e.mode==="native")return{...P("已停用","已停用","启用",n),hint:void 0,clickable:!0,lines:i,tone:"off"};const s=te(n);return{...C("未配置 API Key","缺 API Key",n,void 0),action:s,hint:s,clickable:!1,lines:i,tone:"error"}}var F=o.template("<span>"),ne=o.template("<span style=opacity:0.6;font-weight:400>"),oe=o.template("<span style=opacity:0.3;font-weight:400> · "),ie=o.template("<code style=font-size:0.94em>DEEPSEEK_API_KEY"),se=o.template('<p style="margin:16px 0 0;font-size:12px;line-height:1.6;opacity:0.68;overflow-wrap:anywhere">'),ae=o.template("<div style=margin-top:18px>"),le=o.template('<div style="display:flex;flex-direction:column;height:100%;min-width:0;box-sizing:border-box;padding:16px 16px 24px;overflow-y:auto;overflow-x:hidden"><header style=display:flex;align-items:flex-start;gap:8px><span aria-hidden=true style=flex-shrink:0;font-size:10px;line-height:20px;font-weight:700></span><h2 aria-live=polite style=flex:1;min-width:0;margin:0;font-size:13px;font-weight:600;line-height:20px;overflow-wrap:anywhere>'),ce=o.template('<p style="margin:14px 0 0;font-size:12px;line-height:1.6;opacity:0.7;overflow-wrap:anywhere">'),de=o.template("<div style=display:flex;align-items:center;gap:8px;margin-top:14px;opacity:0.7><span style=font-size:12px>正在读取用量…"),ue=o.template('<dl style="margin:16px 0 0">'),fe=o.template('<div style="display:flex;align-items:baseline;gap:12px;padding:10px 0"><dt style=flex-shrink:0;font-size:11px;letter-spacing:0.08em;line-height:1.9;opacity:0.62></dt><dd style=margin:0;margin-left:auto;min-width:0;display:flex;flex-wrap:wrap;justify-content:flex-end;column-gap:6px;row-gap:2px;font-size:13px;line-height:1.55;font-variant-numeric:tabular-nums;text-align:right>'),pe=o.template("<span style=min-width:0;overflow-wrap:anywhere>");const he={on:"●",off:"○",warn:"◐",error:"!",idle:"…"},ye={dark:{on:"#63d19a",warn:"#e0ad4f",error:"#ee7b74",off:"#8b9198",idle:"#8b9198",rule:"rgba(255,255,255,0.14)"},light:{on:"#1e7a4c",warn:"#8a5f08",error:"#b03931",off:"#6b7076",idle:"#6b7076",rule:"rgba(0,0,0,0.13)"}},me={on:"停用(交还原生搜索)",off:"启用(由 DeepSeek 接管)",warn:"交还原生搜索",error:"重试",idle:"重试"},ge={on:"arrow-undo-down",off:"globe",warn:"arrow-undo-down",error:"refresh",idle:"refresh"};function ve(t,e){const r=t.indexOf(" · ");return r<0?(()=>{var n=F();return o.setStyleProperty(n,"color",e),o.insert(n,t),n})():[(()=>{var n=ne();return o.insert(n,()=>t.slice(0,r)),n})(),oe(),(()=>{var n=F();return o.setStyleProperty(n,"color",e),o.insert(n,()=>t.slice(r+3)),n})()]}function be(t){const e=t.indexOf(" ");return e<0?{label:t,chunks:[]}:{label:t.slice(0,e),chunks:t.slice(e+1).split(" · ")}}function $e(t,e){const r="DEEPSEEK_API_KEY",n=t.indexOf(r);return n<0?t:[o.memo(()=>t.slice(0,n)),(()=>{var i=ie();return o.setStyleProperty(i,"font-family",e),i})(),o.memo(()=>t.slice(n+r.length))]}function Se(t){const e=_.usePlugin(),r=y.createMemo(()=>re({status:t.status,error:t.error,maxColumns:1/0})),n=y.createMemo(()=>ye[e.appearance.mode()]),i=()=>e.appearance.font("mono");return y.onMount(()=>{const s=setInterval(()=>t.onRefresh(),1e4);y.onCleanup(()=>clearInterval(s))}),(()=>{var s=le(),d=s.firstChild,u=d.firstChild,p=u.nextSibling;return o.insert(u,()=>he[r().tone]),o.insert(p,()=>ve(r().title,n()[r().tone])),o.insert(d,o.createComponent(e.ui.Tooltip,{value:"刷新用量",placement:"bottom",get children(){return o.createComponent(e.ui.IconButton,{icon:"refresh",label:"刷新用量",size:"small",get onClick(){return t.onRefresh}})}}),null),o.insert(s,(()=>{var a=o.memo(()=>t.status===void 0);return()=>a()?o.memo(()=>r().lines.length>0)()?(()=>{var c=ce();return o.insert(c,()=>r().lines[0]),c})():(()=>{var c=de(),f=c.firstChild;return o.insert(c,o.createComponent(e.ui.Loader,{size:"small"}),f),c})():(()=>{var c=ue();return o.insert(c,o.createComponent(y.For,{get each(){return r().lines},children:(f,m)=>{const h=be(f);return(()=>{var l=fe(),g=l.firstChild,T=g.nextSibling;return o.insert(g,()=>h.label),o.insert(T,o.createComponent(y.For,{get each(){return h.chunks},children:(v,I)=>(()=>{var $=pe();return o.insert($,v,null),o.insert($,()=>I()<h.chunks.length-1?" ·":"",null),o.effect(w=>{var M=v.startsWith("¥")?500:400,A=v.startsWith("¥")?1:.8;return M!==w.e&&o.setStyleProperty($,"font-weight",w.e=M),A!==w.t&&o.setStyleProperty($,"opacity",w.t=A),w},{e:void 0,t:void 0}),$})()})),o.effect(v=>{var I=m()>0?`1px solid ${n().rule}`:void 0,$=i();return I!==v.e&&o.setStyleProperty(l,"border-top",v.e=I),$!==v.t&&o.setStyleProperty(T,"font-family",v.t=$),v},{e:void 0,t:void 0}),l})()}})),o.effect(f=>{var m=`1px solid ${n().rule}`,h=`1px solid ${n().rule}`;return m!==f.e&&o.setStyleProperty(c,"border-top",f.e=m),h!==f.t&&o.setStyleProperty(c,"border-bottom",f.t=h),f},{e:void 0,t:void 0}),c})()})(),null),o.insert(s,o.createComponent(y.Show,{get when(){return!r().clickable},get children(){var a=se();return o.insert(a,()=>$e(r().action,i())),a}}),null),o.insert(s,o.createComponent(y.Show,{get when(){return o.memo(()=>!!r().clickable)()&&r().tone!=="idle"},get children(){var a=ae();return o.insert(a,o.createComponent(e.ui.Button,{get variant(){return r().tone==="off"||r().tone==="error"?"contrast":"neutral"},get icon(){return ge[r().tone]},get disabled(){return t.busy},get busy(){return t.busy},get onClick(){return t.onToggle},get children(){return me[r().tone]}})),a}}),null),o.effect(a=>{var c=t.busy||void 0,f=n()[r().tone];return c!==a.e&&o.setAttribute(s,"aria-busy",a.e=c),f!==a.t&&o.setStyleProperty(u,"color",a.t=f),a},{e:void 0,t:void 0}),s})()}const K={id:"main",title:"DeepSeek"},we=80,Pe=_.Plugin.define({setup(t){const e=t.client.rpc(j),[r,n]=y.createSignal(),[i,s]=y.createSignal(),[d,u]=y.createSignal(!1),p=l=>(l instanceof Error?l.message:String(l)).slice(0,we);async function a(){try{const l=B(await e.status({}));l?(n(l),s(void 0)):s("状态数据格式不符")}catch(l){s(p(l))}}async function c(l){u(!0);try{const g=q(await e.setMode({mode:l}));g?g.ok||t.toast.show({title:"DeepSeek",description:g.message,variant:"error"}):t.toast.show({title:"DeepSeek",description:"模式切换结果格式不符",variant:"error"})}catch(g){t.toast.show({title:"DeepSeek",description:p(g),variant:"error"})}finally{await a(),u(!1)}}function f(){const l=r();if(!l||i()){a();return}c(l.mode==="deepseek"?"native":"deepseek")}t.add(_.Panel,{id:"main",region:"side",mobile:{title:"DeepSeek",order:50,kind:"menu",icon:"globe"},list:l=>l.open.includes(K.id)?[K]:[],render:()=>o.createComponent(Se,{get status(){return r()},get error(){return i()},get busy(){return d()},onToggle:f,onRefresh:()=>{a()}})});const m=()=>{const l=t.sessions.current();if(!l){t.toast.show({title:"DeepSeek",description:"请先打开一个会话"});return}t.layout.open(`${t.id}:main`,l)};t.add(_.Command,{id:"open",title:"DeepSeek: 打开统计面板",section:"session",run:m}),t.add(_.MenuItem,()=>{const l=t.sessions.current();return{menu:"session.panel",id:"open",title:"DeepSeek",description:"搜索用量与接管开关",icon:"globe",order:60,opened:l!==void 0&&t.layout.stored(l).includes("main"),run:m}});const h=e.events.on("changed",()=>{a()});return a(),h}});exports.default=Pe;
package/package.json CHANGED
@@ -1,58 +1,87 @@
1
- {
2
- "name": "@qthinker/opencode-deepseek-search",
3
- "version": "0.1.1",
4
- "description": "DeepSeek Search \u0026 Fetch for OpenCode V2: a switchable websearch backend, clean Markdown fetch, and local usage/cost stats",
5
- "type": "module",
6
- "license": "MIT",
7
- "author": "QThinker",
8
- "homepage": "https://github.com/QThinkerJR/opencode-deepseek-search#readme",
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/QThinkerJR/opencode-deepseek-search.git"
12
- },
13
- "bugs": {
14
- "url": "https://github.com/QThinkerJR/opencode-deepseek-search/issues"
15
- },
16
- "keywords": [
17
- "opencode",
18
- "opencode-plugin",
19
- "deepseek",
20
- "websearch",
21
- "search",
22
- "fetch",
23
- "usage",
24
- "cost"
25
- ],
26
- "files": [
27
- "index.ts",
28
- "src",
29
- "README.md",
30
- "LICENSE"
31
- ],
32
- "engines": {
33
- "node": "\u003e=22.18"
34
- },
35
- "scripts": {
36
- "typecheck": "tsc --noEmit",
37
- "test": "node --test test/*.test.ts",
38
- "smoke": "node --experimental-strip-types scripts/smoke.ts",
39
- "prepublishOnly": "npm run typecheck \u0026\u0026 npm test"
40
- },
41
- "peerDependencies": {
42
- "@opencode/plugin": "\u003e=2.2.0"
43
- },
44
- "peerDependenciesMeta": {
45
- "@opencode/plugin": {
46
- "optional": true
47
- }
48
- },
49
- "devDependencies": {
50
- "@opencode/plugin": "2.2.5",
51
- "@types/node": "^24.0.0",
52
- "typescript": "^5.9.0"
53
- },
54
- "main": "./index.ts",
55
- "publishConfig": {
56
- "access": "public"
57
- }
58
- }
1
+ {
2
+ "name": "@qthinker/opencode-deepseek-search",
3
+ "version": "0.2.1",
4
+ "description": "DeepSeek Search \u0026 Fetch for OpenCode V2: a switchable websearch backend, clean Markdown fetch, local usage/cost stats, and TUI + desktop sidebars",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "QThinker",
8
+ "homepage": "https://github.com/QThinkerJR/opencode-deepseek-search#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/QThinkerJR/opencode-deepseek-search.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/QThinkerJR/opencode-deepseek-search/issues"
15
+ },
16
+ "keywords": [
17
+ "opencode",
18
+ "opencode-plugin",
19
+ "deepseek",
20
+ "websearch",
21
+ "search",
22
+ "fetch",
23
+ "usage",
24
+ "cost",
25
+ "tui",
26
+ "desktop",
27
+ "sidebar"
28
+ ],
29
+ "files": [
30
+ "index.ts",
31
+ "tui.ts",
32
+ "gui-experimental.cjs",
33
+ "src",
34
+ "README.md",
35
+ "CHANGELOG.md",
36
+ "LICENSE"
37
+ ],
38
+ "engines": {
39
+ "node": "\u003e=22.18"
40
+ },
41
+ "scripts": {
42
+ "typecheck": "tsc --noEmit \u0026\u0026 tsc --noEmit -p tsconfig.gui.json",
43
+ "test": "node --test test/*.test.ts",
44
+ "build:gui": "vite build",
45
+ "smoke": "node --experimental-strip-types scripts/smoke.ts",
46
+ "prepublishOnly": "npm run typecheck \u0026\u0026 npm test \u0026\u0026 npm run build:gui"
47
+ },
48
+ "peerDependencies": {
49
+ "@opencode/plugin": "\u003e=2.2.5",
50
+ "@opencode/theme": "\u003e=2.2.5",
51
+ "@opentui/core": "\u003e=0.8.0",
52
+ "@opentui/solid": "\u003e=0.8.0",
53
+ "solid-js": "\u003e=1.9.0"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "@opencode/plugin": {
57
+ "optional": true
58
+ },
59
+ "@opencode/theme": {
60
+ "optional": true
61
+ },
62
+ "@opentui/core": {
63
+ "optional": true
64
+ },
65
+ "@opentui/solid": {
66
+ "optional": true
67
+ },
68
+ "solid-js": {
69
+ "optional": true
70
+ }
71
+ },
72
+ "devDependencies": {
73
+ "@opencode/plugin": "2.2.5",
74
+ "@opencode/theme": "2.2.5",
75
+ "@opentui/core": "^0.8.0",
76
+ "@opentui/solid": "^0.8.0",
77
+ "@types/node": "^24.0.0",
78
+ "solid-js": "^1.9.0",
79
+ "typescript": "^5.9.0",
80
+ "vite": "^7.3.7",
81
+ "vite-plugin-solid": "^2.11.10"
82
+ },
83
+ "main": "./index.ts",
84
+ "publishConfig": {
85
+ "access": "public"
86
+ }
87
+ }
@@ -0,0 +1,137 @@
1
+ // Desktop half of the plugin: a DeepSeek tab in a session's side region, plus a
2
+ // command that opens it.
3
+ //
4
+ // It reaches the server half only through the shared RPC contract, so the
5
+ // numbers and the switch match the TUI sidebar. The host loads it from the
6
+ // prebuilt CommonJS bundle `gui-experimental.cjs` (see vite.config.ts).
7
+
8
+ import {
9
+ Command,
10
+ MenuItem,
11
+ Panel,
12
+ Plugin,
13
+ type PanelTab,
14
+ type SessionPanelMenuItem,
15
+ } from "@opencode/plugin/gui"
16
+ import { createSignal } from "solid-js"
17
+ import { DeepSeekRpc, readModeResult, readStatus, type SearchStatus } from "../rpc.ts"
18
+ import { DeepSeekPanel } from "./panel.tsx"
19
+
20
+ // `Panel.list` must return the same tab object while it is unchanged: a new
21
+ // object renders the tab label again.
22
+ const TAB: PanelTab = { id: "main", title: "DeepSeek" }
23
+
24
+ const ERROR_MAX = 80
25
+
26
+ export default Plugin.define({
27
+ setup(ctx) {
28
+ const rpc = ctx.client.rpc(DeepSeekRpc)
29
+ const [status, setStatus] = createSignal<SearchStatus | undefined>()
30
+ const [error, setError] = createSignal<string | undefined>()
31
+ const [busy, setBusy] = createSignal(false)
32
+
33
+ const describeError = (e: unknown) => (e instanceof Error ? e.message : String(e)).slice(0, ERROR_MAX)
34
+
35
+ // `status` takes a required first argument: plain JSON Schema degrades the
36
+ // input type to `unknown`, and the host validates the payload against the
37
+ // method's object schema, so an omitted input is rejected as invalid.
38
+ async function refresh() {
39
+ try {
40
+ const next = readStatus(await rpc.status({}))
41
+ if (next) {
42
+ setStatus(next)
43
+ setError(undefined)
44
+ } else {
45
+ setError("状态数据格式不符")
46
+ }
47
+ } catch (e) {
48
+ setError(describeError(e))
49
+ }
50
+ }
51
+
52
+ async function applyMode(next: "deepseek" | "native") {
53
+ setBusy(true)
54
+ try {
55
+ const result = readModeResult(await rpc.setMode({ mode: next }))
56
+ if (!result) ctx.toast.show({ title: "DeepSeek", description: "模式切换结果格式不符", variant: "error" })
57
+ else if (!result.ok) ctx.toast.show({ title: "DeepSeek", description: result.message, variant: "error" })
58
+ } catch (e) {
59
+ ctx.toast.show({ title: "DeepSeek", description: describeError(e), variant: "error" })
60
+ } finally {
61
+ await refresh()
62
+ setBusy(false)
63
+ }
64
+ }
65
+
66
+ // Mirrors the TUI card: no status or a stale error rereads, otherwise
67
+ // switch. A missing key still allows releasing the takeover (the shared
68
+ // model hides the button only where there is genuinely nothing to do).
69
+ function toggle() {
70
+ const current = status()
71
+ if (!current || error()) {
72
+ void refresh()
73
+ return
74
+ }
75
+ void applyMode(current.mode === "deepseek" ? "native" : "deepseek")
76
+ }
77
+
78
+ ctx.add(Panel, {
79
+ id: "main",
80
+ region: "side",
81
+ mobile: { title: "DeepSeek", order: 50, kind: "menu", icon: "globe" },
82
+ list: (input) => (input.open.includes(TAB.id) ? [TAB] : []),
83
+ render: () => (
84
+ <DeepSeekPanel
85
+ status={status()}
86
+ error={error()}
87
+ busy={busy()}
88
+ onToggle={toggle}
89
+ onRefresh={() => void refresh()}
90
+ />
91
+ ),
92
+ })
93
+
94
+ // One entry point, used by both the palette command and the side panel's
95
+ // launcher item.
96
+ const openPanel = () => {
97
+ const session = ctx.sessions.current()
98
+ if (!session) {
99
+ ctx.toast.show({ title: "DeepSeek", description: "请先打开一个会话" })
100
+ return
101
+ }
102
+ ctx.layout.open(`${ctx.id}:main`, session)
103
+ }
104
+
105
+ ctx.add(Command, {
106
+ id: "open",
107
+ title: "DeepSeek: 打开统计面板",
108
+ section: "session",
109
+ run: openPanel,
110
+ })
111
+
112
+ // The new-tab launcher and the + menu list `session.panel` items, not panel
113
+ // tabs, so without this the panel is reachable only by typing the command.
114
+ // The item hides itself while its tab is already open.
115
+ ctx.add(MenuItem, (): SessionPanelMenuItem => {
116
+ const session = ctx.sessions.current()
117
+ const opened = session !== undefined && ctx.layout.stored(session).includes("main")
118
+ return {
119
+ menu: "session.panel",
120
+ id: "open",
121
+ title: "DeepSeek",
122
+ description: "搜索用量与接管开关",
123
+ icon: "globe",
124
+ order: 60,
125
+ opened,
126
+ run: openPanel,
127
+ }
128
+ })
129
+
130
+ // The `changed` event is the primary refresh path; a panel that mounts
131
+ // later renders the latest signals, and its refresh button rereads.
132
+ const off = rpc.events.on("changed", () => void refresh())
133
+ void refresh()
134
+
135
+ return off
136
+ },
137
+ })