@chentyit/dsh-chatbot-panel 0.1.0 → 0.2.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/README.md CHANGED
@@ -13,16 +13,22 @@ tool catalog entirely.
13
13
  - **Per-session capability switch.** Click the chip to open/close the panel. The chip itself never
14
14
  toggles a capability; enabling anything in the panel marks it active (tinted bot icon).
15
15
  - **Web search** — exposes the built-in `web_search` / `web_fetch` tools only while enabled.
16
+ - **Image search** — adds `chatbot_image_search` while web search is on. When the user's message
17
+ carries an image, the tool reads that image from the current turn and asks DeepSeek's native
18
+ `deepseek_web_search` server tool to search by it, returning an identification, source pages and
19
+ the matched `image_url`s. An explicit workspace path, `http(s)` URL or base64 data URI also works.
16
20
  - **Deep thinking search** — adds `chatbot_deep_search`, which runs DeepSeek's server-side
17
21
  `deepseek_web_search` tool with thinking enabled: the search model plans several queries,
18
- searches, opens and reads relevant pages over multiple steps, then answers with sources.
22
+ searches, opens and reads relevant pages over multiple steps, then answers with sources. Pass
23
+ `image: "auto"` to combine it with the current message's image.
19
24
  - **Default off.** A fresh session starts with no web tools declared; turning a capability off
20
25
  restores that state.
21
26
  - **Durable state.** The switch is stored in the session log through the `/chatbot` command
22
27
  lifecycle and read back from a session projection, so it survives page reloads, resume and forks.
23
- - **Usage and estimated cost.** The panel footer shows search calls / queries / sources, fetch
24
- calls, deep-search calls and tool calls, deep-search tokens and failures, per-category fees and
25
- the estimated total, with a link to the DeepSeek platform usage page.
28
+ - **Usage and estimated cost.** The panel footer shows billed searches / sources, fetch calls,
29
+ image searches / images sent / billed requests, deep-search calls and tool calls, deep-search
30
+ tokens and failures, per-category fees and the estimated total, with a link to the DeepSeek
31
+ platform usage page. Image and deep fees are split into tool and auxiliary-token fees.
26
32
  - **Theme-native, borderless.** The chip uses the host theme tokens and matches the permission
27
33
  chip's 28 px text-chip look; no border, no fill, hover tints the whole chip.
28
34
 
@@ -32,8 +38,8 @@ Requires DeepSeek Harness `0.2.2-rc.3` or another compatible `0.2.x` runtime (`e
32
38
 
33
39
  After the package is published to a registry:
34
40
 
35
- 1. **Settings → Plugins → install** and enter `@chentyit/dsh-chatbot-panel@0.1.0`, or
36
- 2. use the plugin manager tool: `install_bundle` with target `@chentyit/dsh-chatbot-panel@0.1.0`.
41
+ 1. **Settings → Plugins → install** and enter `@chentyit/dsh-chatbot-panel@0.2.0`, or
42
+ 2. use the plugin manager tool: `install_bundle` with target `@chentyit/dsh-chatbot-panel@0.2.0`.
37
43
 
38
44
  From a local checkout (development):
39
45
 
@@ -50,8 +56,11 @@ restart the app. Uninstall from Settings → Plugins (or remove the bundle).
50
56
  1. Open a session. The **ChatBot** chip sits right of the permission chip.
51
57
  2. Click it; the panel opens upward. Tick **Web search** (and optionally **Deep thinking search**).
52
58
  3. Ask the model a question that needs current information — it now has the matching tools.
53
- 4. Untick everything, and the tools disappear from the model's catalog again.
54
- 5. The panel footer reports this session's usage and estimated cost.
59
+ 4. Attach an image and ask what it is, where it comes from, or to find similar images. With web
60
+ search on, the model calls `chatbot_image_search`; with deep search on it can also add
61
+ `image: "auto"` to `chatbot_deep_search` for a slower, multi-step image search.
62
+ 5. Untick everything, and the tools disappear from the model's catalog again.
63
+ 6. The panel footer reports this session's usage and estimated cost.
55
64
 
56
65
  You can also drive the same state with the slash command:
57
66
 
@@ -63,7 +72,7 @@ You can also drive the same state with the slash command:
63
72
 
64
73
  ## Usage and billing
65
74
 
66
- The official price list bills web search and page fetch separately from model tokens:
75
+ The official price list bills web search, image search and page fetch separately from model tokens:
67
76
  **CNY 5 per 1,000 successful requests** (≈ ¥0.005 per call; failed or empty results are not
68
77
  billed).
69
78
 
@@ -71,6 +80,7 @@ billed).
71
80
  |---|---|
72
81
  | Search | one successful `web_search` tool call fans out one request per query, so a successful call counts `queries.length` billed searches |
73
82
  | Fetch | successful `web_fetch` calls; anonymous HTTP fetch is free, so no fee is added unless the fetch provider is the DeepSeek Fetch API (`countFetchCost: true`) |
83
+ | Image | `chatbot_image_search` calls, images sent, and the billed request count (`tool_search` + `tool_fetch` + `tool_image_search`); the fee is split into tool and auxiliary-token fees |
74
84
  | Deep | deep-search tool calls plus the billed `tool_search` / `tool_fetch` / `tool_image_search` counters from the provider and the auxiliary model's input/output tokens |
75
85
  | Failed | calls that ended in an error; a failed multi-query search may already have billed some requests |
76
86
 
@@ -95,18 +105,26 @@ Override the row's `config` in the profile patch (`~/.dsh/profiles/<profile>/cor
95
105
  model: deepseek-flash
96
106
  maxTokens: 4096
97
107
  timeoutMs: 180000
98
- tokenPrice: # auxiliary deep-search turn, deepseek-flash idle prices
108
+ tokenPrice: # auxiliary search turn, deepseek-flash idle prices
99
109
  inputPerMillion: 1
100
110
  outputPerMillion: 4
101
111
  ```
102
112
 
103
- Deep search authenticates with the signed-in DeepSeek account when the session runs on the
104
- account route (`x-dsh-auth-token`), otherwise with the API key named by `deepSearch.apiKeyEnv`
105
- (`$DSH_HOME/.env` or the process environment). Without either it fails with a clear message.
113
+ Image search uses the same `deepSearch` endpoint, model and credential as deep search. It
114
+ authenticates with the signed-in DeepSeek account when the session runs on the account route
115
+ (`x-dsh-auth-token`), otherwise with a stored API key or the key named by
116
+ `deepSearch.apiKeyEnv` (`$DSH_HOME/.env`, the credential service, or the process environment).
117
+ Without either it fails with a clear message.
106
118
 
107
119
  ## Limitations
108
120
 
109
- - Deep search needs a DeepSeek account sign-in or an API key.
121
+ - Deep search and image search need a DeepSeek account sign-in or an API key.
122
+ - Image search reads the images attached to the **current turn's** user message. In a later turn,
123
+ attach the image again or pass an explicit workspace path, `http(s)` URL or data URI.
124
+ - One image search accepts up to four images; each must be PNG, JPEG, WebP or GIF, up to 8 MB
125
+ (attached images are resized toward the DeepSeek vision budget first).
126
+ - Searching by image uploads that image to DeepSeek's Messages API. The result's `image_url` is for
127
+ display only and is not guaranteed to be reachable.
110
128
  - Toggling a capability changes the request's tool catalog, so that session's DeepSeek prompt
111
129
  cache is invalidated once.
112
130
  - With plan mode active, the plan chip sits between the permission chip and this chip (official
@@ -121,11 +139,15 @@ account route (`x-dsh-auth-token`), otherwise with the API key named by `deepSea
121
139
  The package is plain JavaScript — no build step, no runtime dependencies:
122
140
 
123
141
  ```bash
124
- npm run check # syntax + metadata validation
142
+ npm run check # syntax + metadata validation + node:test suites
125
143
  npm pack --dry-run # inspect the published file list
126
144
  npm publish --access public
127
145
  ```
128
146
 
147
+ The Host half is `index.js`; `pure.js` holds the testable capability, usage and response helpers;
148
+ `client.js` is the browser half. `test/` covers the pure helpers and the two auxiliary tools with a
149
+ mocked Cordis context and Messages response.
150
+
129
151
  ## License
130
152
 
131
153
  MIT. Copyright (c) 2026 chentyit.
@@ -140,12 +162,14 @@ MIT. Copyright (c) 2026 chentyit.
140
162
  点击按钮展开向上弹出的能力面板;在面板里勾选能力,才对本会话生效。**关闭时不是"拒绝调用",而是把对应工具从模型的工具目录里彻底移除。**
141
163
 
142
164
  - **联网搜索**:仅在开启时向模型暴露内置 `web_search` / `web_fetch`。
143
- - **深度思考搜索**:额外提供 `chatbot_deep_search`,走 DeepSeek 服务端 `deepseek_web_search`(thinking 开启)多步搜索、打开网页后带来源作答。
165
+ - **以图搜图**:联网搜索开启时额外提供 `chatbot_image_search`。用户消息带图时,工具读取本轮的图片,交给 DeepSeek 服务端 `deepseek_web_search` 按图搜索,返回识别结论、来源页面和匹配到的 `image_url`;也支持工作区路径、`http(s)` URL 或 base64 data URI。
166
+ - **深度思考搜索**:额外提供 `chatbot_deep_search`,走 DeepSeek 服务端 `deepseek_web_search`(thinking 开启)多步搜索、打开网页后带来源作答;传 `image: "auto"` 可结合本轮图片做深度以图搜图。
144
167
  - **默认关闭**;开关按会话写入会话日志,刷新、恢复、分叉后仍有效。
145
- - **用量与费用**:面板底部按官方口径记录搜索次数/查询条数/来源数、抓取次数、深搜次数与工具调用、深搜 token、失败次数、各项费用与估算合计,并链接开放平台用量页。
168
+ - **用量与费用**:面板底部按官方口径记录搜索、抓取、搜图、深搜的次数与估算费用,并链接开放平台用量页。
169
+ - **图片限制**:只读取本条消息里的图片;一次最多 4 张,支持 PNG/JPEG/WebP/GIF,单张 ≤ 8MB;搜索时图片会上传给 DeepSeek,搜图与深搜需 DeepSeek 账号或 API key。
146
170
 
147
- 安装:`Settings → Plugins` 输入 `@chentyit/dsh-chatbot-panel@0.1.0`,或用 `plugin_manager` 的 `install_bundle`。
171
+ 安装:`Settings → Plugins` 输入 `@chentyit/dsh-chatbot-panel@0.2.0`,或用 `plugin_manager` 的 `install_bundle`。
148
172
 
149
- 计费口径:联网搜索/抓取官方价 **5 元 / 1000 次成功请求**(失败或零结果不计费);一次 `web_search` 按查询词数拆成多次请求,成功计 `queries.length` 次。面板金额是**估算**(API 只返回次数),以平台账单为准。
173
+ 计费口径:联网搜索/抓取/图片搜索官方价 **5 元 / 1000 次成功请求**(失败或零结果不计费);一次 `web_search` 按查询词数拆成多次请求;图片搜索每张图片计 1 次,模型框选图片中的区域时每个区域另计 1 次。面板金额是**估算**(API 只返回次数),以平台账单为准。
150
174
 
151
175
  配置项与限制见上方英文段落(`manageBuiltinWebTools` / `pricePerThousand` / `countFetchCost` / `deepSearch` / `tokenPrice`)。
package/client.js CHANGED
@@ -28,18 +28,21 @@ window.__ModuleLoader__.load({
28
28
  scope: '本会话',
29
29
  groupWeb: '联网工具',
30
30
  web: '联网搜索',
31
- webHint: '按成功查询计费 ¥5 / 千次',
31
+ webHint: '¥5 / 千次 · 附图提问即搜图',
32
32
  deep: '深度思考搜索',
33
- deepHint: '多步推理后联网,更慢、更耗 token',
33
+ deepHint: '多步推理 · 支持以图搜图 · 更耗 token',
34
34
  deepNeedsWeb: '先开启联网搜索',
35
+ searchNote: '以图搜图只读本轮图片;搜图与深搜需 DeepSeek 账号或 API key。',
35
36
  pending: '应用中…',
36
37
  usageTitle: '本会话用量',
37
38
  search: '搜索',
38
39
  fetch: '抓取',
40
+ imageUsage: '搜图',
41
+ billed: '次计费请求',
42
+ imagesCount: '张图',
39
43
  deepUsage: '深搜',
40
44
  failed: '失败',
41
45
  times: '次',
42
- queries: '条查询',
43
46
  sources: '个来源',
44
47
  toolCalls: '次工具调用',
45
48
  input: '输入',
@@ -47,12 +50,16 @@ window.__ModuleLoader__.load({
47
50
  feeTitle: '费用估算',
48
51
  searchFee: '搜索费',
49
52
  fetchFee: '抓取费',
53
+ imageToolFee: '搜图工具费',
54
+ imageTokenFee: '搜图 token 费',
50
55
  deepToolFee: '深搜工具费',
51
56
  deepTokenFee: '深搜 token 费',
52
57
  totalFee: '合计',
53
58
  fetchFree: '匿名抓取不计费',
54
59
  feeNote: '按官方价估算,实际以平台账单为准',
55
60
  usageLink: '查看官方用量 ↗',
61
+ active: '已开启',
62
+ inactive: '未开启',
56
63
  errorUnknown: '命令未生效',
57
64
  errorPrefix: '操作失败:',
58
65
  }
@@ -64,18 +71,21 @@ window.__ModuleLoader__.load({
64
71
  scope: 'this session',
65
72
  groupWeb: 'Web tools',
66
73
  web: 'Web search',
67
- webHint: 'Billed per successful query, ¥5 / 1,000',
74
+ webHint: '¥5 / 1,000 · attach an image and ask',
68
75
  deep: 'Deep thinking search',
69
- deepHint: 'Searches while reasoning; slower and token-heavier',
76
+ deepHint: 'Multi-step · image search · token-heavy',
70
77
  deepNeedsWeb: 'Turn on web search first',
78
+ searchNote: 'Image search reads only this turn\'s images; image and deep search need a DeepSeek sign-in or API key.',
71
79
  pending: 'Applying…',
72
80
  usageTitle: 'Session usage',
73
81
  search: 'Search',
74
82
  fetch: 'Fetch',
83
+ imageUsage: 'Image search',
84
+ billed: 'billed requests',
85
+ imagesCount: 'images',
75
86
  deepUsage: 'Deep',
76
87
  failed: 'Failed',
77
88
  times: '',
78
- queries: 'queries',
79
89
  sources: 'sources',
80
90
  toolCalls: 'tool calls',
81
91
  input: 'in',
@@ -83,12 +93,16 @@ window.__ModuleLoader__.load({
83
93
  feeTitle: 'Estimated cost',
84
94
  searchFee: 'Search',
85
95
  fetchFee: 'Fetch',
96
+ imageToolFee: 'Image tools',
97
+ imageTokenFee: 'Image tokens',
86
98
  deepToolFee: 'Deep tools',
87
99
  deepTokenFee: 'Deep tokens',
88
100
  totalFee: 'Total',
89
101
  fetchFree: 'Anonymous fetch is free',
90
102
  feeNote: 'Estimated from official prices; the platform bill is authoritative',
91
103
  usageLink: 'Open platform usage ↗',
104
+ active: 'on',
105
+ inactive: 'off',
92
106
  errorUnknown: 'The command did not apply',
93
107
  errorPrefix: 'Failed: ',
94
108
  }
@@ -113,15 +127,16 @@ window.__ModuleLoader__.load({
113
127
  '.cbp-arrow{display:inline-flex;flex:0 0 auto;margin-left:1px;color:var(--dsw-alias-label-caption)}',
114
128
  '.cbp-arrow svg{display:block;width:12px;height:12px;transition:transform 120ms ease}',
115
129
  '.cbp-main[data-open="true"] .cbp-arrow svg{transform:rotate(180deg)}',
116
- '.cbp-panel{position:fixed;z-index:1200;box-sizing:border-box;display:flex;flex-direction:column;padding:6px;border-radius:var(--dsw-radius-lg);background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-1,#fff));backdrop-filter:var(--dsw-menu-backdrop-filter,none);box-shadow:var(--dsw-elevation-prominent,0 12px 32px rgba(0,0,0,.18));color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family,inherit)}',
130
+ '.cbp-panel{position:fixed;z-index:1200;box-sizing:border-box;display:flex;flex-direction:column;padding:6px;border-radius:var(--dsw-radius-lg);background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-1,#fff));backdrop-filter:var(--dsw-menu-backdrop-filter,none);box-shadow:var(--dsw-elevation-prominent,0 12px 32px rgba(0,0,0,.18));color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family,inherit);overflow-y:auto;overscroll-behavior:contain}',
117
131
  '.cbp-head{display:flex;align-items:baseline;justify-content:space-between;gap:8px;padding:6px 8px 2px}',
118
132
  '.cbp-head-title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}',
119
133
  '.cbp-head-scope{font-size:11px;color:var(--dsw-alias-label-caption)}',
120
134
  '.cbp-group{padding:8px 8px 2px;font-size:11px;font-weight:600;letter-spacing:.03em;color:var(--dsw-alias-label-tertiary)}',
135
+ '.cbp-group-note{padding:4px 8px 2px}',
121
136
  '.cbp-row{display:flex;align-items:center;gap:10px;width:100%;box-sizing:border-box;padding:7px 8px;border:0;border-radius:var(--dsw-radius-md,8px);background:transparent;color:var(--dsw-alias-label-primary);font-family:inherit;text-align:left;cursor:pointer}',
122
- '.cbp-row:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}',
137
+ '.cbp-row:hover:not([aria-disabled="true"]){background:var(--dsw-alias-interactive-bg-hover)}',
123
138
  '.cbp-row:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-1px}',
124
- '.cbp-row:disabled{cursor:default;opacity:.55}',
139
+ '.cbp-row[aria-disabled="true"]{cursor:default;opacity:.55}',
125
140
  '.cbp-row-sub{padding-left:22px}',
126
141
  '.cbp-row-text{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1}',
127
142
  '.cbp-row-label{font-size:13px;line-height:18px}',
@@ -139,7 +154,7 @@ window.__ModuleLoader__.load({
139
154
  '.cbp-note{font-size:10px;line-height:15px;color:var(--dsw-alias-label-caption)}',
140
155
  '.cbp-link{font-size:11px;line-height:17px;color:var(--dsw-alias-link);text-decoration:none}',
141
156
  '.cbp-link:hover{text-decoration:underline}',
142
- '.cbp-error{margin:4px 6px 2px;padding:5px 8px;border-radius:var(--dsw-radius-sm,6px);background:var(--dsw-alias-state-error-tertiary,rgba(220,38,38,.12));color:var(--dsw-alias-state-error-primary,#dc2626);font-size:11px;line-height:15px}',
157
+ '.cbp-error{margin:4px 6px 2px;padding:5px 8px;border-radius:var(--dsw-radius-sm,6px);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent);color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:15px}',
143
158
  ].join('')
144
159
 
145
160
  /** The capsule owns its stylesheet, removed with the plugin's activation. */
@@ -164,6 +179,12 @@ window.__ModuleLoader__.load({
164
179
  return amount >= 1000 ? `${(amount / 1000).toFixed(1)}k` : String(amount)
165
180
  }
166
181
 
182
+ /** Join a count and its unit without a trailing space when the unit is empty. */
183
+ function count(value, unit) {
184
+ const amount = typeof value === 'number' && Number.isFinite(value) ? value : 0
185
+ return typeof unit === 'string' && unit.length > 0 ? `${amount} ${unit}` : String(amount)
186
+ }
187
+
167
188
  /** A small check mark drawn in the theme's inverted label color. */
168
189
  function CheckGlyph() {
169
190
  return h('svg', { viewBox: '0 0 12 12', 'aria-hidden': true },
@@ -232,11 +253,11 @@ window.__ModuleLoader__.load({
232
253
  const { t, checked, label, hint, sub, disabled, onToggle } = props
233
254
  return h('button', {
234
255
  type: 'button',
235
- role: 'menuitemcheckbox',
256
+ role: 'checkbox',
236
257
  'aria-checked': checked ? 'true' : 'false',
237
- disabled,
258
+ 'aria-disabled': disabled ? 'true' : undefined,
238
259
  className: sub === true ? 'cbp-row cbp-row-sub' : 'cbp-row',
239
- onClick: onToggle,
260
+ onClick: disabled ? undefined : onToggle,
240
261
  },
241
262
  h('span', { className: 'cbp-row-text' },
242
263
  h('span', { className: 'cbp-row-label' }, label),
@@ -251,40 +272,56 @@ window.__ModuleLoader__.load({
251
272
  if (usage === undefined || usage === null) {
252
273
  return h('div', { className: 'cbp-usage' },
253
274
  h('div', { className: 'cbp-usage-title' }, t('usageTitle')),
254
- h('div', { className: 'cbp-usage-line' }, h('span', null, t('search')), h('b', null, `0 ${t('times')}`)))
275
+ h('div', { className: 'cbp-usage-line' }, h('span', null, t('search')), h('b', null, count(0, t('times')))))
255
276
  }
256
277
  const search = usage.search || { calls: 0, queries: 0, sources: 0, failed: 0, failedQueries: 0 }
257
278
  const fetch = usage.fetch || { calls: 0, failed: 0 }
279
+ const image = usage.image || { calls: 0, failed: 0, images: 0, results: 0, imageResults: 0, toolSearch: 0, toolFetch: 0, imageSearch: 0, inputTokens: 0, outputTokens: 0 }
258
280
  const deep = usage.deep || { calls: 0, failed: 0, toolSearch: 0, toolFetch: 0, imageSearch: 0, inputTokens: 0, outputTokens: 0, sources: 0 }
259
- const fees = usage.fees || { search: 0, fetch: 0, deepTools: 0, deepTokens: 0, total: 0 }
281
+ const fees = usage.fees || { search: 0, fetch: 0, image: 0, imageTools: 0, imageTokens: 0, deepTools: 0, deepTokens: 0, total: 0 }
260
282
  const deepTools = (deep.toolSearch || 0) + (deep.toolFetch || 0) + (deep.imageSearch || 0)
261
- const failures = (search.failed || 0) + (fetch.failed || 0) + (deep.failed || 0)
283
+ const imageBilled = (image.toolSearch || 0) + (image.toolFetch || 0) + (image.imageSearch || 0)
284
+ const showImage = image.calls > 0 || image.failed > 0 || imageBilled > 0
285
+ const failures = (search.failed || 0) + (fetch.failed || 0) + (image.failed || 0) + (deep.failed || 0)
262
286
  const lines = []
263
287
  lines.push(h('div', { key: 'title', className: 'cbp-usage-title' }, t('usageTitle')))
264
288
  lines.push(h('div', { key: 'search', className: 'cbp-usage-line' },
265
289
  h('span', null, t('search')),
266
290
  h('span', null,
267
- h('b', null, `${search.calls} ${t('times')}`),
268
- ` · ${search.queries} ${t('queries')} · ${search.sources} ${t('sources')}`)))
291
+ h('b', null, count(search.calls, t('billed'))),
292
+ ` · ${count(search.sources, t('sources'))}`)))
293
+ if (showImage) {
294
+ lines.push(h('div', { key: 'image', className: 'cbp-usage-line' },
295
+ h('span', null, t('imageUsage')),
296
+ h('span', null,
297
+ h('b', null, count(image.calls, t('times'))),
298
+ ` · ${count(image.images, t('imagesCount'))} · ${count(imageBilled, t('billed'))}`)))
299
+ }
269
300
  lines.push(h('div', { key: 'fetch', className: 'cbp-usage-line' },
270
301
  h('span', null, t('fetch')),
271
302
  h('span', null,
272
- h('b', null, `${fetch.calls} ${t('times')}`),
303
+ h('b', null, count(fetch.calls, t('times'))),
273
304
  usage.countFetchCost === true ? '' : ` · ${t('fetchFree')}`)))
274
305
  lines.push(h('div', { key: 'deep', className: 'cbp-usage-line' },
275
306
  h('span', null, t('deepUsage')),
276
307
  h('span', null,
277
- h('b', null, `${deep.calls} ${t('times')}`),
278
- ` · ${deepTools} ${t('toolCalls')} · ${tokens(deep.inputTokens)}/${tokens(deep.outputTokens)} tok`)))
308
+ h('b', null, count(deep.calls, t('times'))),
309
+ ` · ${count(deepTools, t('toolCalls'))} · ${tokens(deep.inputTokens)}/${tokens(deep.outputTokens)} tok`)))
279
310
  if (failures > 0) {
280
311
  lines.push(h('div', { key: 'failed', className: 'cbp-usage-line' },
281
312
  h('span', null, t('failed')),
282
- h('span', null, `${failures} ${t('times')}`)))
313
+ h('span', null, count(failures, t('times')))))
283
314
  }
284
315
  lines.push(h('div', { key: 'sep', className: 'cbp-sep' }))
285
316
  lines.push(h('div', { key: 'fee-title', className: 'cbp-usage-title' }, t('feeTitle')))
286
317
  lines.push(h('div', { key: 'fee-search', className: 'cbp-usage-line' },
287
318
  h('span', null, t('searchFee')), h('span', null, yuan(fees.search))))
319
+ if (showImage) {
320
+ lines.push(h('div', { key: 'fee-image-tools', className: 'cbp-usage-line' },
321
+ h('span', null, t('imageToolFee')), h('span', null, yuan(fees.imageTools))))
322
+ lines.push(h('div', { key: 'fee-image-tokens', className: 'cbp-usage-line' },
323
+ h('span', null, t('imageTokenFee')), h('span', null, yuan(fees.imageTokens))))
324
+ }
288
325
  lines.push(h('div', { key: 'fee-fetch', className: 'cbp-usage-line' },
289
326
  h('span', null, t('fetchFee')), h('span', null, yuan(fees.fetch))))
290
327
  lines.push(h('div', { key: 'fee-deep-tools', className: 'cbp-usage-line' },
@@ -324,7 +361,8 @@ window.__ModuleLoader__.load({
324
361
  const run = React.useCallback((line) => {
325
362
  setBusy(true)
326
363
  setError(null)
327
- Promise.resolve(submit(sessionId, line))
364
+ Promise.resolve()
365
+ .then(() => submit(sessionId, line))
328
366
  .then((result) => {
329
367
  if (result === undefined || result === null) { setError(t('errorUnknown')); return }
330
368
  if (result.ok !== true) {
@@ -346,9 +384,10 @@ window.__ModuleLoader__.load({
346
384
  const element = wrapRef.current
347
385
  if (element === null || typeof window === 'undefined') return
348
386
  const rect = element.getBoundingClientRect()
349
- const left = Math.min(Math.max(8, rect.left), Math.max(8, window.innerWidth - PANEL_WIDTH - 8))
387
+ const width = Math.min(PANEL_WIDTH, Math.max(160, window.innerWidth - 16))
388
+ const left = Math.min(Math.max(8, rect.left), Math.max(8, window.innerWidth - width - 8))
350
389
  const bottom = Math.max(8, window.innerHeight - rect.top + 8)
351
- setPosition({ left, bottom })
390
+ setPosition({ left, bottom, width, maxHeight: Math.max(120, window.innerHeight - bottom - 8) })
352
391
  }, [])
353
392
 
354
393
  React.useLayoutEffect(() => {
@@ -359,6 +398,13 @@ window.__ModuleLoader__.load({
359
398
  return () => { window.removeEventListener('resize', onResize) }
360
399
  }, [open, place])
361
400
 
401
+ React.useEffect(() => {
402
+ if (!open || panelRef.current === null) return undefined
403
+ const first = panelRef.current.querySelector('button[role="checkbox"]:not([aria-disabled="true"])')
404
+ if (first !== null && typeof first.focus === 'function') first.focus({ preventScroll: true })
405
+ return undefined
406
+ }, [open])
407
+
362
408
  React.useEffect(() => {
363
409
  if (!open) return undefined
364
410
  const onPointerDown = (event) => {
@@ -393,9 +439,9 @@ window.__ModuleLoader__.load({
393
439
  type: 'button',
394
440
  className: 'cbp-main',
395
441
  'data-open': open ? 'true' : 'false',
396
- 'aria-haspopup': 'menu',
442
+ 'aria-haspopup': 'dialog',
397
443
  'aria-expanded': open ? 'true' : 'false',
398
- 'aria-label': t('openPanel'),
444
+ 'aria-label': `${t('openPanel')} · ${active ? t('active') : t('inactive')}`,
399
445
  title: t('openPanel'),
400
446
  onClick: () => { setOpen(value => !value) },
401
447
  },
@@ -408,12 +454,13 @@ window.__ModuleLoader__.load({
408
454
  panel = createPortal(h('div', {
409
455
  ref: panelRef,
410
456
  className: 'cbp-panel',
411
- role: 'menu',
457
+ role: 'dialog',
412
458
  'aria-label': t('panelTitle'),
413
459
  style: {
414
460
  left: position === null ? 0 : `${position.left}px`,
415
461
  bottom: position === null ? 12 : `${position.bottom}px`,
416
- width: `${PANEL_WIDTH}px`,
462
+ width: position === null ? 'min(328px, calc(100vw - 16px))' : `${position.width}px`,
463
+ maxHeight: position === null ? 'calc(100vh - 24px)' : `${position.maxHeight}px`,
417
464
  },
418
465
  },
419
466
  h('div', { className: 'cbp-head' },
@@ -437,9 +484,10 @@ window.__ModuleLoader__.load({
437
484
  disabled: disabled || !web,
438
485
  onToggle: () => { run(deep ? 'deep off' : 'deep on') },
439
486
  }),
487
+ h('div', { className: 'cbp-note cbp-group-note' }, t('searchNote')),
440
488
  h('div', { className: 'cbp-sep' }),
441
489
  h(UsageBlock, { t, usage }),
442
- pending ? h('div', { className: 'cbp-note' }, t('pending')) : null,
490
+ pending || busy ? h('div', { className: 'cbp-note' }, t('pending')) : null,
443
491
  error === null ? null : h('div', { className: 'cbp-error', role: 'alert' }, error)),
444
492
  document.body)
445
493
  }
package/cordis.patch.yml CHANGED
@@ -11,6 +11,7 @@
11
11
  # Anonymous HTTP fetch is free; enable only when the fetch provider is
12
12
  # the DeepSeek Fetch API.
13
13
  countFetchCost: false
14
+ # Deep and image search share this endpoint, model and credential.
14
15
  deepSearch:
15
16
  baseURL: https://api.deepseek.com/anthropic/v1
16
17
  apiKeyEnv: DEEPSEEK_API_KEY