dsh-cline-pass 0.2.9 → 0.2.10

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.en.md CHANGED
@@ -22,7 +22,7 @@ Requires Node.js `>= 20.3.0`, dsh `>= 0.1.2-alpha.3 < 0.3.0`, and a valid Cline
22
22
  dsh plugin --profile web add dsh-cline-pass
23
23
  ```
24
24
 
25
- Restart dsh, open **Settings → Built-in Plugins → Cline Pass** to save and test your API key, then select a Cline Pass model in the model picker. Keys are stored in the dsh credential store and displayed only in masked form.
25
+ Restart dsh, open **Settings → Cline Pass** to save and test your API key, then select a Cline Pass model in the model picker. Keys are stored in the dsh credential store and displayed only in masked form.
26
26
 
27
27
  Alternatively, configure the key through an environment variable:
28
28
 
package/README.md CHANGED
@@ -22,7 +22,7 @@
22
22
  dsh plugin --profile web add dsh-cline-pass
23
23
  ```
24
24
 
25
- 安装后重启 dsh,在 **设置 → 内置插件 → Cline Pass** 中保存并测试 API Key,然后在模型选择器中选择 Cline Pass 模型。Key 存入 dsh 凭据库,界面仅显示掩码。
25
+ 安装后重启 dsh,在 **设置 → Cline Pass** 中保存并测试 API Key,然后在模型选择器中选择 Cline Pass 模型。Key 存入 dsh 凭据库,界面仅显示掩码。
26
26
 
27
27
  也可以通过环境变量配置:
28
28
 
package/lib/client.js CHANGED
@@ -84,6 +84,17 @@ window.__ModuleLoader__.load({
84
84
  '.cp-dot{width:8px;height:8px;border-radius:50%;display:inline-block;flex:none}',
85
85
  '.cp-table{display:flex;flex-direction:column;gap:6px}',
86
86
  '.cp-model{border:0.5px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px;display:flex;flex-direction:column;gap:8px}',
87
+ // The bulk toolbar sits directly above the first row; the card's own 10px gap
88
+ // reads as none against two stacked borders, so this card opens it up.
89
+ '.cp-models{gap:14px}',
90
+ '.cp-models-body{flex-direction:column;gap:16px;display:flex}',
91
+ // One line per model. `.cp-row` wraps by default, which a long id plus a long
92
+ // pin order turns into a two-line row that reads as broken rather than dense;
93
+ // both texts ellipsis instead, and their full value stays in `title`.
94
+ '.cp-model-head{flex-wrap:nowrap;min-width:0}',
95
+ '.cp-model-head .cp-grow{min-width:0}',
96
+ '.cp-model-mode{flex:none;max-width:55%}',
97
+ '.cp-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
87
98
  // The account table is a real table for the same reason the history one
88
99
  // is: per-row grids size their `auto` tracks from their own content, so a
89
100
  // header cell never lines up with the data under it.
@@ -195,16 +206,10 @@ window.__ModuleLoader__.load({
195
206
  // Plugins-page card chrome, mirrored token for token from the host's own
196
207
  // `PluginCard`: that tab hands a plugin the whole card, so matching it —
197
208
  // rather than inventing a second look — is this bundle's job.
198
- '.cp-set-card{border:0.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-3);border-radius:16px;list-style:none;transition:border-color .16s,background .16s}',
199
- '.cp-set-card:hover{border-color:var(--dsw-alias-label-dimmed)}',
200
- '.cp-set-card-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}',
201
- '.cp-set-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}',
202
- '.cp-set-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}',
203
- '.cp-set-head-text{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}',
209
+ '.cp-set-card{border:0.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-3);border-radius:16px;list-style:none}',
210
+ '.cp-set-head{flex-direction:column;gap:4px;display:flex;padding:14px 16px}',
204
211
  '.cp-set-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}',
205
212
  '.cp-set-desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}',
206
- '.cp-set-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}',
207
- '.cp-set-chevron-open{transform:rotate(180deg)}',
208
213
  '.cp-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s;display:inline-flex}',
209
214
  'details[open]>summary .cp-chevron{transform:rotate(180deg)}',
210
215
  // A summary laid out as a flex row keeps no native affordance, so the
@@ -288,8 +293,6 @@ window.__ModuleLoader__.load({
288
293
  title: 'Cline Pass',
289
294
  description: '订阅模型的渠道钉住与账号池配置',
290
295
  panelUnavailable: '浏览器通道不可用。',
291
- expand: '展开设置',
292
- collapse: '收起设置',
293
296
  keyOk: 'API Key 已配置',
294
297
  keyMissing: '尚未配置 API Key',
295
298
  keyValue: 'API Key',
@@ -317,9 +320,10 @@ window.__ModuleLoader__.load({
317
320
  active: '当前账号',
318
321
  addAccount: '添加',
319
322
  accountName: '账号名',
320
- models: '订阅模型',
323
+ models: 'cline-pass',
321
324
  modelsCount: '{n} 个模型',
322
- hiddenModels: '{n} 个已从选择列表隐藏',
325
+ hiddenModels: '{n} 隐藏',
326
+ hiddenModelsHint: '{n} 个已从选择列表隐藏',
323
327
  visible: '在模型选择列表中显示',
324
328
  hiddenTag: '已隐藏',
325
329
  selectAll: '全选',
@@ -360,6 +364,10 @@ window.__ModuleLoader__.load({
360
364
  noChannels: '未探测渠道',
361
365
  pinned: '已钉住',
362
366
  auto: '自动路由',
367
+ modeAuto: '智能路由',
368
+ modeStrict: '钉住',
369
+ modePreferred: '优先',
370
+ channelsCount: '{n} 个渠道',
363
371
  probe: '探测',
364
372
  validate: '校验',
365
373
  testPin: '测试',
@@ -377,8 +385,6 @@ window.__ModuleLoader__.load({
377
385
  title: 'Cline Pass',
378
386
  description: 'Channel pins and the account pool for the subscription models',
379
387
  panelUnavailable: 'The browser channel is unavailable.',
380
- expand: 'Show settings',
381
- collapse: 'Hide settings',
382
388
  keyOk: 'API key configured',
383
389
  keyMissing: 'No API key',
384
390
  keyValue: 'API key',
@@ -406,9 +412,10 @@ window.__ModuleLoader__.load({
406
412
  active: 'Active',
407
413
  addAccount: 'Add',
408
414
  accountName: 'Name',
409
- models: 'Models',
415
+ models: 'cline-pass',
410
416
  modelsCount: '{n} model(s)',
411
- hiddenModels: '{n} hidden from the pickers',
417
+ hiddenModels: '{n} hidden',
418
+ hiddenModelsHint: '{n} hidden from the model pickers',
412
419
  visible: 'Show in the model pickers',
413
420
  hiddenTag: 'Hidden',
414
421
  selectAll: 'Check all',
@@ -449,6 +456,10 @@ window.__ModuleLoader__.load({
449
456
  noChannels: 'No channels probed',
450
457
  pinned: 'Pinned',
451
458
  auto: 'Auto',
459
+ modeAuto: 'Smart routing',
460
+ modeStrict: 'Pinned',
461
+ modePreferred: 'Preferred',
462
+ channelsCount: '{n} channels',
452
463
  probe: 'Probe',
453
464
  validate: 'Validate',
454
465
  testPin: 'Test',
@@ -1170,12 +1181,29 @@ window.__ModuleLoader__.load({
1170
1181
  const { t, model, disabled, actions } = props
1171
1182
  const [open, setOpen] = React.useState(false)
1172
1183
  const hidden = model.hidden === true
1173
- const pinnedLabel = model.pinned.length === 0 ? t('auto') : model.pinned.join(' › ')
1184
+ // What a request will actually do, readable without expanding the row. The
1185
+ // old line read `已钉住: 自动路由` — claiming a pin while saying nothing is
1186
+ // pinned, which is exactly how it was misread.
1187
+ // Every row is this route, and the card header already names it, so the prefix
1188
+ // is 11 characters of noise on each of nineteen rows. The full id stays in
1189
+ // `title` for anything that needs to copy it.
1190
+ const label = typeof model.displayName === 'string' && model.displayName !== ''
1191
+ ? model.displayName
1192
+ : String(model.id).replace(/^cline-pass\//, '')
1193
+ const mode = model.pinned.length === 0
1194
+ ? t('modeAuto')
1195
+ : `${t(model.pinMode === 'preferred' ? 'modePreferred' : 'modeStrict')} · ${model.pinned.join(' › ')}`
1196
+ // How many channels sit behind it and how many of those the order pins. After a
1197
+ // probe only: before that the list is empty and a count would be a guess.
1198
+ const channelCount = model.upstreams.length === 0 ? '' : fill(t('channelsCount'), { n: model.upstreams.length })
1174
1199
  const action = (key, label, run) => h('button', {
1175
1200
  key, type: 'button', className: 'cp-btn', disabled, onClick: () => run(model.id),
1176
1201
  }, label)
1177
1202
  return h('div', { className: `cp-model${hidden ? ' cp-model-hidden' : ''}` }, [
1178
- h('div', { key: 'head', className: 'cp-row' }, [
1203
+ // One line, always. The id and the mode are long enough to collide on a
1204
+ // narrow panel, so both ellipsis and keep their full text in `title`: a
1205
+ // second line here reads as a broken row, not as more information.
1206
+ h('div', { key: 'head', className: 'cp-row cp-model-head' }, [
1179
1207
  h('input', {
1180
1208
  key: 'vis',
1181
1209
  type: 'checkbox',
@@ -1186,11 +1214,12 @@ window.__ModuleLoader__.load({
1186
1214
  'aria-label': `${t('visible')}: ${model.id}`,
1187
1215
  onChange: (event) => actions.setModelVisible(model.id, event.target.checked),
1188
1216
  }),
1189
- h('span', { key: 'name', className: 'cp-mono cp-grow' }, model.id),
1217
+ h('span', { key: 'name', className: 'cp-mono cp-grow cp-ellipsis', title: model.id }, label),
1190
1218
  hidden ? h('span', { key: 'off', className: 'cp-muted' }, t('hiddenTag')) : null,
1191
- h('span', { key: 'pin', className: 'cp-muted cp-mono' }, `${t('pinned')}: ${pinnedLabel}`),
1219
+ h('span', { key: 'mode', className: 'cp-muted cp-mono cp-model-mode cp-ellipsis', title: mode }, mode),
1192
1220
  h('button', {
1193
- key: 'more', type: 'button', className: 'cp-btn', title: t('channels'),
1221
+ key: 'more', type: 'button', className: 'cp-btn',
1222
+ title: channelCount === '' ? t('channels') : `${t('channels')} · ${channelCount}`,
1194
1223
  onClick: () => setOpen(!open),
1195
1224
  }, open ? '▾' : '▸'),
1196
1225
  ]),
@@ -1392,8 +1421,8 @@ window.__ModuleLoader__.load({
1392
1421
  * which drops the browser's own disclosure marker — without it a folded
1393
1422
  * section looks like a plain card title, not something to open.
1394
1423
  */
1395
- function foldHeader(title) {
1396
- return h('summary', { key: 'sum', className: 'cp-card-title cp-fold' }, [
1424
+ function foldHeader(title, hint) {
1425
+ return h('summary', { key: 'sum', className: 'cp-card-title cp-fold', ...(hint === undefined || hint === '' ? {} : { title: hint }) }, [
1397
1426
  h('span', { key: 'l' }, title),
1398
1427
  h(Chevron, { key: 'chevron', className: 'cp-chevron' }),
1399
1428
  ])
@@ -1409,27 +1438,40 @@ window.__ModuleLoader__.load({
1409
1438
  function ModelsCard(props) {
1410
1439
  const { t, data, disabled, busy, actions } = props
1411
1440
  const hidden = Number(data.hiddenModels ?? 0)
1412
- return h('details', { className: 'cp-card' }, [
1413
- foldHeader(`${t('models')} · ${fill(t('modelsCount'), { n: data.models.length })}${hidden === 0 ? '' : ` · ${fill(t('hiddenModels'), { n: hidden })}`}`),
1441
+ // Open by default: the pins are what this page is for, and a collapsed card
1442
+ // makes a user click before they can see whether anything is pinned at all.
1443
+ // The fold stays available for the days a pool has nineteen models in it.
1444
+ return h('details', { className: 'cp-card cp-models', open: true }, [
1445
+ // Short enough for a fold header; the long spelling is the tooltip.
1446
+ foldHeader(
1447
+ `${t('models')} · ${fill(t('modelsCount'), { n: data.models.length })}${hidden === 0 ? '' : ` · ${fill(t('hiddenModels'), { n: hidden })}`}`,
1448
+ hidden === 0 ? '' : fill(t('hiddenModelsHint'), { n: hidden }),
1449
+ ),
1414
1450
  // The refresh and the bulk checkbox actions sit in the body, not the
1415
1451
  // header: a button inside a summary would also toggle the fold.
1416
- h('div', { key: 'bar', className: 'cp-row' }, [
1417
- h('button', {
1418
- key: 'all', type: 'button', className: 'cp-btn', disabled,
1419
- onClick: () => actions.setModelsVisibility('all'),
1420
- }, t('selectAll')),
1421
- h('button', {
1422
- key: 'invert', type: 'button', className: 'cp-btn', disabled,
1423
- onClick: () => actions.setModelsVisibility('invert'),
1424
- }, t('invertSelection')),
1425
- h('button', {
1426
- key: 'r', type: 'button', className: 'cp-btn cp-grow-end', disabled,
1427
- onClick: () => actions.refreshModels(),
1428
- }, t('refresh')),
1452
+ //
1453
+ // `details` wraps everything after its summary, so the card's own flex gap
1454
+ // stops at that wrapper and the toolbar sat flush against the first row. This
1455
+ // grouping carries the spacing itself, whatever the engine's `details` layout.
1456
+ h('div', { key: 'models', className: 'cp-models-body' }, [
1457
+ h('div', { key: 'bar', className: 'cp-row' }, [
1458
+ h('button', {
1459
+ key: 'all', type: 'button', className: 'cp-btn', disabled,
1460
+ onClick: () => actions.setModelsVisibility('all'),
1461
+ }, t('selectAll')),
1462
+ h('button', {
1463
+ key: 'invert', type: 'button', className: 'cp-btn', disabled,
1464
+ onClick: () => actions.setModelsVisibility('invert'),
1465
+ }, t('invertSelection')),
1466
+ h('button', {
1467
+ key: 'r', type: 'button', className: 'cp-btn cp-grow-end', disabled,
1468
+ onClick: () => actions.refreshModels(),
1469
+ }, t('refresh')),
1470
+ ]),
1471
+ h('div', { key: 'rows', className: 'cp-table' }, data.models.map((model) => h(ModelRow, {
1472
+ key: model.id, t, model, disabled, busy, actions,
1473
+ }))),
1429
1474
  ]),
1430
- h('div', { key: 'rows', className: 'cp-table' }, data.models.map((model) => h(ModelRow, {
1431
- key: model.id, t, model, disabled, busy, actions,
1432
- }))),
1433
1475
  ])
1434
1476
  }
1435
1477
 
@@ -1837,31 +1879,21 @@ window.__ModuleLoader__.load({
1837
1879
  * host's own cards — and because the panel only mounts once opened, a folded
1838
1880
  * card costs no round trip.
1839
1881
  */
1882
+ /**
1883
+ * The section's page: the panel itself, under a static heading.
1884
+ *
1885
+ * It used to fold, because it lived inside the Plugins tab where a long panel
1886
+ * had to stay out of the way. As a first-level page the fold only asks for a
1887
+ * click on the thing the user just navigated to.
1888
+ */
1840
1889
  function PluginSettingsCard(props) {
1841
1890
  const t = props.t
1842
- const [open, setOpen] = React.useState(false)
1843
- const title = t('title')
1844
- return h('div', { className: `cp-set-card${open ? ' cp-set-card-open' : ''}` }, [
1845
- h('button', {
1846
- key: 'head',
1847
- type: 'button',
1848
- className: 'cp-set-header',
1849
- 'aria-expanded': open,
1850
- 'aria-label': `${t(open ? 'collapse' : 'expand')}: ${title}`,
1851
- onClick: () => setOpen(!open),
1852
- }, [
1853
- h('span', { key: 'text', className: 'cp-set-head-text' }, [
1854
- h('span', { key: 'name', className: 'cp-set-name' }, title),
1855
- h('span', { key: 'desc', className: 'cp-set-desc' }, t('description')),
1856
- ]),
1857
- h(Chevron, {
1858
- key: 'chevron',
1859
- className: `cp-set-chevron${open ? ' cp-set-chevron-open' : ''}`,
1860
- }),
1891
+ return h('div', { className: 'cp-set-card' }, [
1892
+ h('div', { key: 'head', className: 'cp-set-head' }, [
1893
+ h('span', { key: 'name', className: 'cp-set-name' }, t('title')),
1894
+ h('span', { key: 'desc', className: 'cp-set-desc' }, t('description')),
1861
1895
  ]),
1862
- open
1863
- ? h('div', { key: 'body', className: 'cp-set-body' }, h(Panel, { ...props, hideTitle: true }))
1864
- : null,
1896
+ h('div', { key: 'body', className: 'cp-set-body' }, h(Panel, { ...props, hideTitle: true })),
1865
1897
  ])
1866
1898
  }
1867
1899
 
@@ -2147,19 +2179,20 @@ window.__ModuleLoader__.load({
2147
2179
  const renderPluginCard = (props) => h(PluginSettingsCard, { ...props, ...face, t: translate })
2148
2180
  const renderQuick = (props) => h(QuickCard, { ...props, ...face, t: translate })
2149
2181
 
2150
- // 1. The one configuration surface: a tab inside the Plugins settings
2151
- // section, which already enumerates this namespace. No
2152
- // `settings.section` alongside it — a second page in the Settings nav
2153
- // would only duplicate this panel, and the nav is for surfaces that are
2154
- // not already listed there.
2182
+ // 1. The one configuration surface: a first-level page in the Settings nav.
2183
+ //
2184
+ // It used to be a tab under Plugins, which put the channel pins five or six
2185
+ // clicks deep — Settings, Plugins, the tab, the collapsed card, the model
2186
+ // list, the row. The pins are what this plugin is for, so they get the same
2187
+ // standing as the shell's own pages: `order: 12` sits between Models (10) and
2188
+ // Plugins (15), which is also where a reader looks for a provider's routing.
2155
2189
  //
2156
- // `settings.plugins.tab` is a list slot: its key is `id`, its position
2157
- // is `order` (the host's own inventory tab sits at 10), and its tab
2158
- // label is a thunk so it follows the active locale.
2159
- ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({
2160
- name: 'settings.plugins.tab',
2190
+ // `settings.section` is a list slot: its key is `id`, its position is
2191
+ // `order`, and its nav label is a thunk so it follows the active locale.
2192
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
2193
+ name: 'settings.section',
2161
2194
  id: NS,
2162
- order: 20,
2195
+ order: 12,
2163
2196
  label: () => translate('title'),
2164
2197
  locale: NS,
2165
2198
  inject: face,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-cline-pass",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
4
4
  "description": "Self-contained Cline Pass provider for dsh: register Cline Pass subscription models on the harness LLM seam, enumerate and validate the upstream channels behind each model, pin or exclude them with failover, and drive an account pool. No proxy process and no external cline-pass-switcher install required.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",