dsh-openai-subscription 0.1.4 → 0.1.6

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
@@ -53,7 +53,22 @@ Restart `dsh web`, then refresh the existing page. DSH registers the plugin auto
53
53
  4. Return to DSH after authorization completes; the plugin fetches and syncs models automatically.
54
54
  5. Select an available model from the DSH model picker.
55
55
 
56
- `llm-pi-ai` automatically renews refreshable OAuth credentials during model requests. **Refresh authorization** in this settings page is primarily a manual recovery action.
56
+ The main area keeps **Sync models / Update models**: fetch the model list while preserving local configuration, without switching accounts. Expand **Connection management** for infrequent operations:
57
+
58
+ - **Refresh authorization / Reconnect**: recover from authorization problems. `llm-pi-ai` automatically renews refreshable credentials during requests; routine manual refresh is unnecessary. Recovery is also shown directly for expired or unknown authorization.
59
+ - **Reload status**: read local state only; does not renew authorization or sync models.
60
+ - **Disconnect**: remove the local connection after confirmation; does not delete your ChatGPT account.
61
+
62
+ ### 1M and custom context windows
63
+
64
+ Under **Model context**, select a model, choose **Use model default / 1M / Custom**, then **Save context**. 1M means `1,000,000` tokens for input and output combined, not a fixed request size or a higher output-token limit.
65
+
66
+ - Only the selected model’s `llm-pi-ai.providers.openai-codex.models[].contextWindow` changes. Other models are not forced to 1M.
67
+ - Catalog defaults and maximum windows are separate. The 1M option is disabled below a known 1M maximum, and custom values cannot exceed known maxima.
68
+ - When no maximum is declared, an explicit override is allowed with a warning. Local configuration does not guarantee model/account capacity or unlock upstream access.
69
+ - Later model syncs preserve this local edit. Choose **Use model default** and save to restore the latest synchronized default field.
70
+ - Saving changes local settings only, without login or discovery. Concurrent edits produce a settings conflict; reload before saving again.
71
+ - Older snapshots may lack default/maximum metadata; **Update models** fills in facts the catalog supplies. After upgrading, load both the new host and client plugin, restart DSH, and refresh the page.
57
72
 
58
73
  ### Status and recovery
59
74
 
@@ -120,6 +135,12 @@ OpenAI's terms, privacy policy, model availability, and usage limits still apply
120
135
 
121
136
  After installing dependencies, run `npm run check` (or `pnpm run check`) for typechecking, build, and regression tests. Use `npm pack --dry-run --ignore-scripts` to inspect published files. Tests mock OAuth, network, and credential services; they do not access real accounts. After editing the plugin, rebuild and reinstall/load that version, restart `dsh web`, and refresh the existing page. Editing source alone does not update the installed GUI plugin.
122
137
 
138
+ ### Settings UI and navigation icons
139
+
140
+ Buttons use theme-aware, high-contrast `1px` borders. The header and navigation reuse one local OpenAI SVG, with no additional runtime dependency or remote image requests; see [third-party notices](THIRD_PARTY_NOTICES.md).
141
+
142
+ The currently tested upstream DSH build does not expose custom settings navigation icons. Unextended hosts keep the default gear without affecting other plugin features. This project includes an **opt-in, build-guarded, reversible** local DSH extension; installing the plugin does not patch the host automatically. It preserves string labels and passes icons through `icon({ size, className })`, not DOM/CSS substitution. See [DSH settings icon extension](docs/DSH_SETTINGS_ICONS.md) for usage and upgrade caveats.
143
+
123
144
  ## License
124
145
 
125
146
  MIT — see [LICENSE](LICENSE).
package/README.zh.md CHANGED
@@ -53,7 +53,22 @@ dsh plugin --profile web add dsh-openai-subscription
53
53
  4. 授权完成后返回 DSH;插件会自动获取并同步模型。
54
54
  5. 在 DSH 的模型选择器中选择可用模型。
55
55
 
56
- `llm-pi-ai` 会在模型请求期间自动续期可刷新的 OAuth 凭证。设置页中的 **刷新授权** 主要用于授权异常时的手动恢复。
56
+ 常用区域只保留 **同步模型 / 更新模型**:重新获取模型列表,保留本地配置,不切换账号。展开 **连接管理** 才会看到低频操作:
57
+
58
+ - **刷新授权 / 重新连接**:用于授权异常恢复。`llm-pi-ai` 会在模型请求期间自动续期可刷新的凭证,无需日常手动刷新;过期或有效性未知时会直接显示恢复按钮。
59
+ - **重新读取状态**:只读取本机状态,不刷新授权、不同步模型。
60
+ - **断开连接**:清理本机连接,操作前需确认,不会删除 ChatGPT 账号。
61
+
62
+ ### 1M 与自定义上下文
63
+
64
+ 在 **模型上下文** 中选择模型,再选择 **跟随模型默认 / 1M / 自定义**,点击 **保存上下文**。1M 表示 `1,000,000` tokens,是输入与输出合计窗口;不是每次固定发送 1M tokens,也不会提高单次输出上限。
65
+
66
+ - 仅修改选中模型的 `llm-pi-ai.providers.openai-codex.models[].contextWindow`,不把所有模型强制提升到 1M。
67
+ - 目录的默认窗口与最大窗口分开处理;已知上限低于 1M 时禁用 1M 选项,自定义也不能超过已知上限。
68
+ - 未知上限时允许明确设置,但本机配置不代表模型或账号支持该容量,不会解锁上游权限。
69
+ - 后续模型同步会保留本地上下文编辑;选择 **跟随模型默认** 并保存可恢复最近一次同步的默认字段。
70
+ - 保存仅操作本机设置,不触发登录或模型发现;其他页面同时改动时会报设置冲突,请重新读取后再保存。
71
+ - 旧版记录可能缺少默认/上限信息;点击 **更新模型** 可补齐目录提供的元数据。升级后需同时加载新版主机端与客户端,并重启 DSH、刷新页面。
57
72
 
58
73
  ### 状态与恢复
59
74
 
@@ -120,6 +135,12 @@ dsh plugin --profile web remove dsh-openai-subscription
120
135
 
121
136
  安装依赖后运行 `npm run check`(或 `pnpm run check`),完成类型检查、构建和回归测试;`npm pack --dry-run --ignore-scripts` 检查发布文件。测试使用模拟 OAuth、网络与凭证服务,不访问真实账号。修改插件后需要重新构建并重新安装/加载该版本,再重启 `dsh web`、刷新现有页面;仅修改源码不会更新已安装的 GUI 插件。
122
137
 
138
+ ### 设置界面与导航图标
139
+
140
+ 按钮使用主题自适应的 `1px` 高对比度边框。顶部和导航复用本地 OpenAI SVG,无额外运行时依赖或远程图片请求;来源及许可证见 [第三方声明](THIRD_PARTY_NOTICES.md)。
141
+
142
+ 当前官方 DSH 设置导航尚未提供自定义图标接口,未扩展的宿主仍显示默认齿轮,插件其他功能不受影响。本项目提供**显式执行、限定构建版本、可还原**的本机 DSH 扩展,不在安装时自动修改宿主。它保留字符串标签,通过 `icon({ size, className })` 传递图标,不使用 DOM/CSS 替换技巧。用法和升级注意事项见 [DSH 图标接口扩展](docs/DSH_SETTINGS_ICONS.md)。
143
+
123
144
  ## 许可证
124
145
 
125
146
  MIT——见 [LICENSE](LICENSE)。
@@ -0,0 +1,59 @@
1
+ # Third-party notices
2
+
3
+ ## OpenAI / ChatGPT icon — Lobe Icons
4
+
5
+ The monochrome SVG geometry embedded in `src/client.ts` is from
6
+ [`@lobehub/icons-static-svg` 1.95.0, `icons/openai.svg`](https://www.npmjs.com/package/@lobehub/icons-static-svg/v/1.95.0).
7
+ Upstream: [Lobe Icons](https://github.com/lobehub/lobe-icons),
8
+ [icon preview](https://lobehub.com/icons/openai).
9
+
10
+ The path geometry is unchanged. The wrapper is adapted to React, accepts a size
11
+ and CSS class, inherits `currentColor`, and is hidden from assistive technology
12
+ because adjacent text provides the label. The same component is used in the
13
+ settings header and navigation; no icon library or remote image is loaded at runtime.
14
+
15
+ OpenAI and ChatGPT names and marks belong to their respective owners. Use here
16
+ identifies the supported service and does not imply affiliation or endorsement.
17
+ The software license below does not grant trademark rights.
18
+
19
+ ## DeepSeek Harness patch anchors
20
+
21
+ The optional `scripts/patch-dsh-settings-icons.mjs` includes small source anchors
22
+ from DeepSeek Harness's MIT-licensed settings and slots packages.
23
+
24
+ Copyright (c) 2026 DeepSeek
25
+
26
+ The MIT permission and disclaimer below also apply to these anchors; preserve
27
+ this copyright notice when distributing them. The extension is local to this
28
+ project and is not an upstream DeepSeek release.
29
+
30
+ ## MIT license text
31
+
32
+ Reproduced from `@lobehub/icons` 1.95.0, `LICENSE` (the static SVG package
33
+ also declares MIT). The DeepSeek Harness packages use the same MIT terms with
34
+ the DeepSeek copyright notice above:
35
+
36
+
37
+ ```text
38
+ MIT License
39
+
40
+ Copyright (c) 2023 LobeHub
41
+
42
+ Permission is hereby granted, free of charge, to any person obtaining a copy
43
+ of this software and associated documentation files (the "Software"), to deal
44
+ in the Software without restriction, including without limitation the rights
45
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
46
+ copies of the Software, and to permit persons to whom the Software is
47
+ furnished to do so, subject to the following conditions:
48
+
49
+ The above copyright notice and this permission notice shall be included in all
50
+ copies or substantial portions of the Software.
51
+
52
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
53
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
54
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
55
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
56
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
57
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
58
+ SOFTWARE.
59
+ ```
package/dist/client.d.ts CHANGED
@@ -30,6 +30,18 @@ interface ModelSyncInfo {
30
30
  count: number;
31
31
  warningCode?: string;
32
32
  }
33
+ interface ModelContextInfo {
34
+ id: string;
35
+ name?: string;
36
+ contextWindow?: number;
37
+ defaultContextWindow?: number;
38
+ maxContextWindow?: number;
39
+ customized: boolean;
40
+ }
41
+ interface ModelContextsInfo {
42
+ revision: number;
43
+ models: ModelContextInfo[];
44
+ }
33
45
  interface RemoteResult<T> {
34
46
  ok?: boolean;
35
47
  error?: {
@@ -67,6 +79,7 @@ interface SettingsSectionMeta {
67
79
  id: string;
68
80
  order: number;
69
81
  label: string | (() => string);
82
+ icon?: IconComponent;
70
83
  locale?: string;
71
84
  }
72
85
  interface SlotsService {
@@ -100,8 +113,13 @@ interface SectionProps {
100
113
  subscribeReset?: (listener: () => void) => () => void;
101
114
  }
102
115
  type SectionComponent = (props: SectionProps) => ReactElement;
116
+ interface IconProps {
117
+ size?: number;
118
+ className?: string;
119
+ }
120
+ type IconComponent = (props: IconProps) => ReactElement;
103
121
  interface ReactModule {
104
- createElement(type: string | SectionComponent, props: Record<string, unknown> | null, ...children: ReactNode[]): ReactElement;
122
+ createElement(type: string | SectionComponent | IconComponent, props: Record<string, unknown> | null, ...children: ReactNode[]): ReactElement;
105
123
  useState<S>(initial: S): [S, (update: S | ((previous: S) => S)) => void];
106
124
  useEffect(effect: () => void | (() => void), deps?: ReadonlyArray<unknown>): void;
107
125
  useRef<T>(initial: T): {
package/dist/client.js CHANGED
@@ -17,7 +17,35 @@ const ZH = {
17
17
  'model.synced': '模型已同步',
18
18
  'model.attention': '模型需要同步',
19
19
  'model.count': '{count} 个可用模型',
20
- 'model.synced.detail': '账号目录、内置目录和本地编辑已安全合并。',
20
+ 'model.synced.detail': '已合并账号模型与 DSH 内置模型,保留你的本地编辑。',
21
+ 'model.sync.help': '重新获取可用模型列表,不会更换账号或重置本地配置。',
22
+ 'context.title': '模型上下文',
23
+ 'context.help': '按模型设置总上下文窗口(输入 + 输出)。同步模型会保留此设置。',
24
+ 'context.model': '模型',
25
+ 'context.window': '上下文窗口',
26
+ 'context.default': '跟随模型默认',
27
+ 'context.million': '1M · 1,000,000 tokens',
28
+ 'context.custom': '自定义',
29
+ 'context.tokens': '自定义 tokens 数',
30
+ 'context.current': '当前:{count} tokens',
31
+ 'context.maximum': '目录声明上限:{count} tokens',
32
+ 'context.unknown': '目录未声明最大窗口;本机配置不会解锁上游权限,请确认模型和账号支持。',
33
+ 'context.limit.help': '超过目录声明上限的配置不可保存;实际可用性仍取决于账号和上游。',
34
+ 'context.empty': '同步模型后即可配置上下文。',
35
+ 'context.loading': '正在读取模型配置…',
36
+ 'context.save': '保存上下文',
37
+ 'context.saving': '正在保存…',
38
+ 'context.saved': '上下文配置已保存,后续模型请求会使用新设置。',
39
+ 'context.error': '无法读取上下文配置。请确认插件主机端已更新并重启 DSH 后重试。',
40
+ 'error.invalid-context-window': '请输入正整数 tokens 数。',
41
+ 'error.context-window-exceeded': '此设置超过了模型目录声明的最大上下文窗口。',
42
+ 'error.model-not-found': '模型已不在当前列表中,请重新读取配置。',
43
+ 'action.more': '连接管理',
44
+ 'action.less': '收起连接管理',
45
+ 'manage.refresh.help': '请求时会自动续期;仅在授权异常时手动刷新,无需重新登录。',
46
+ 'manage.reconnect.help': '重新进行设备验证,以恢复此账号的授权。',
47
+ 'manage.reload.help': '只重新读取本机状态,不刷新授权,也不同步模型。',
48
+ 'manage.disconnect.help': '删除本机授权和未修改的插件模型项,不影响 ChatGPT 账号。',
21
49
  'model.attention.detail': '确认同步后会保留本地新增项与已编辑字段。',
22
50
  'action.connect': '连接 ChatGPT',
23
51
  'action.connecting': '正在连接…',
@@ -120,7 +148,35 @@ const EN = {
120
148
  'model.synced': 'Models synced',
121
149
  'model.attention': 'Models need syncing',
122
150
  'model.count': '{count} models available',
123
- 'model.synced.detail': 'Account, built-in, and locally edited catalogs are safely merged.',
151
+ 'model.synced.detail': 'Account and DSH models are merged, keeping your local edits.',
152
+ 'model.sync.help': 'Fetch the available model list without switching accounts or resetting local settings.',
153
+ 'context.title': 'Model context',
154
+ 'context.help': 'Set each model’s total context window (input + output). Model sync preserves this setting.',
155
+ 'context.model': 'Model',
156
+ 'context.window': 'Context window',
157
+ 'context.default': 'Use model default',
158
+ 'context.million': '1M · 1,000,000 tokens',
159
+ 'context.custom': 'Custom',
160
+ 'context.tokens': 'Custom token count',
161
+ 'context.current': 'Current: {count} tokens',
162
+ 'context.maximum': 'Catalog limit: {count} tokens',
163
+ 'context.unknown': 'The catalog does not declare a maximum. Local settings do not unlock upstream access; confirm model and account support.',
164
+ 'context.limit.help': 'Values above the catalog limit cannot be saved. Availability still depends on your account and upstream service.',
165
+ 'context.empty': 'Sync models to configure their context windows.',
166
+ 'context.loading': 'Reading model settings…',
167
+ 'context.save': 'Save context',
168
+ 'context.saving': 'Saving…',
169
+ 'context.saved': 'Context settings saved. Subsequent model requests will use the new setting.',
170
+ 'context.error': 'Could not read context settings. Ensure the host plugin is updated and restart DSH before retrying.',
171
+ 'error.invalid-context-window': 'Enter a positive whole number of tokens.',
172
+ 'error.context-window-exceeded': 'This exceeds the maximum context window declared by the model catalog.',
173
+ 'error.model-not-found': 'The model is no longer in the current list. Reload settings.',
174
+ 'action.more': 'Connection management',
175
+ 'action.less': 'Hide connection management',
176
+ 'manage.refresh.help': 'Authorization renews automatically during requests. Refresh manually only to recover from authorization errors.',
177
+ 'manage.reconnect.help': 'Repeat device verification to restore authorization for this account.',
178
+ 'manage.reload.help': 'Read local status only. Does not renew authorization or sync models.',
179
+ 'manage.disconnect.help': 'Remove local authorization and unchanged plugin models, not your ChatGPT account.',
124
180
  'model.attention.detail': 'Syncing preserves local additions and fields you edited.',
125
181
  'action.connect': 'Connect ChatGPT',
126
182
  'action.connecting': 'Connecting…',
@@ -222,8 +278,26 @@ window.__ModuleLoader__.load({
222
278
  const exports = module.exports;
223
279
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
224
280
  const React = require('react');
281
+ // Lobe Icons @lobehub/icons-static-svg 1.95.0, icons/openai.svg (MIT).
282
+ // Geometry is unchanged; see THIRD_PARTY_NOTICES.md for attribution.
283
+ // One local SVG component serves both the host navigation and this header:
284
+ // no extra React runtime, icon-library bundle, or external image request.
285
+ function OpenAIIcon({ size = 24, className }) {
286
+ return React.createElement('svg', {
287
+ xmlns: 'http://www.w3.org/2000/svg', viewBox: '0 0 24 24',
288
+ width: size, height: size, fill: 'currentColor', fillRule: 'evenodd',
289
+ className, 'aria-hidden': true, focusable: false,
290
+ }, React.createElement('path', {
291
+ d: 'M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z',
292
+ }));
293
+ }
225
294
  const CSS = `
226
295
  .oasub-wrap {
296
+ --oasub-section-inset: 20px;
297
+ --oasub-card-border-width: 1px;
298
+ /* Control boundaries need more contrast than the host's decorative hairlines. */
299
+ --oasub-control-border: color-mix(in srgb, var(--dsw-alias-label-primary, #0f1115) 48%, transparent);
300
+ --oasub-control-border-hover: color-mix(in srgb, var(--dsw-alias-label-primary, #0f1115) 68%, transparent);
227
301
  box-sizing: border-box;
228
302
  display: flex;
229
303
  flex-direction: column;
@@ -232,6 +306,8 @@ window.__ModuleLoader__.load({
232
306
  color: var(--dsw-alias-label-primary, #0f1115);
233
307
  }
234
308
  .oasub-wrap, .oasub-wrap * { box-sizing: border-box; }
309
+ /* The header stays flush with the card's outer frame. */
310
+ .oasub-header { padding-inline: 0; }
235
311
  .oasub-header { display: flex; align-items: center; gap: 12px; }
236
312
  .oasub-mark {
237
313
  display: grid;
@@ -242,19 +318,17 @@ window.__ModuleLoader__.load({
242
318
  border-radius: 13px;
243
319
  color: var(--dsw-alias-label-primary-inverted, #fff);
244
320
  background: var(--dsw-alias-brand-primary, #4176e6);
245
- font-size: 20px;
246
- font-weight: 700;
247
- line-height: 1;
248
321
  }
249
- .oasub-heading { min-width: 0; }
322
+ .oasub-mark > svg { display: block; flex: none; }
323
+ .oasub-heading { min-width: 0; overflow-wrap: anywhere; }
250
324
  .oasub-title { margin: 0; font-size: 18px; font-weight: 600; line-height: 26px; }
251
325
  .oasub-subtitle { margin: 2px 0 0; color: var(--dsw-alias-label-tertiary, #81858c); font-size: 13px; line-height: 20px; }
252
326
  .oasub-card {
253
327
  display: flex;
254
328
  flex-direction: column;
255
329
  gap: 16px;
256
- padding: 20px;
257
- border: .5px solid var(--dsw-alias-border-l2, rgba(15, 17, 21, .14));
330
+ padding: var(--oasub-section-inset);
331
+ border: var(--oasub-card-border-width) solid var(--dsw-alias-border-l2, rgba(15, 17, 21, .14));
258
332
  border-radius: 16px;
259
333
  background: var(--dsw-alias-bg-layer-1, #fff);
260
334
  box-shadow: var(--dsw-shadow-lv1, 0 2px 8px rgba(0, 0, 0, .04));
@@ -289,11 +363,21 @@ window.__ModuleLoader__.load({
289
363
  .oasub-model-title { font-size: 13px; font-weight: 500; line-height: 19px; }
290
364
  .oasub-model-detail { color: var(--dsw-alias-label-tertiary, #81858c); font-size: 11px; line-height: 17px; }
291
365
  .oasub-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
366
+ .oasub-management { display: flex; flex-direction: column; gap: 16px; border-top: 1px solid var(--dsw-alias-border-l2, rgba(15, 17, 21, .14)); padding-top: 16px; }
367
+ .oasub-manage-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
368
+ .oasub-manage-row > .oasub-button { flex: 0 0 auto; }
369
+ .oasub-button.disclosure { border-color: transparent; border-radius: 8px; color: var(--dsw-alias-label-secondary, #61666b); padding: 4px 0; min-height: 32px; align-self: flex-start; }
370
+ .oasub-context { display: flex; flex-direction: column; gap: 10px; }
371
+ .oasub-field { display: flex; flex-direction: column; gap: 6px; flex: 1 1 180px; min-width: 0; font-size: 12px; }
372
+ .oasub-context > .oasub-field { flex: none; }
373
+ .oasub-select { width: 100%; min-height: 36px; border: 1px solid var(--oasub-control-border); border-radius: 8px; padding: 7px 10px; font: inherit; color: inherit; background: var(--dsw-alias-bg-layer-1, #fff); }
374
+ .oasub-select:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #4176e6); outline-offset: 2px; }
375
+ .oasub-select:disabled { opacity: .55; }
292
376
  .oasub-button {
293
377
  appearance: none;
294
378
  min-height: 36px;
295
379
  padding: 7px 15px;
296
- border: .5px solid var(--dsw-alias-border-l1, rgba(15, 17, 21, .2));
380
+ border: 1px solid var(--oasub-control-border);
297
381
  border-radius: 999px;
298
382
  color: var(--dsw-alias-label-primary, #0f1115);
299
383
  background: transparent;
@@ -306,7 +390,7 @@ window.__ModuleLoader__.load({
306
390
  text-align: center;
307
391
  transition: background .16s ease, border-color .16s ease, opacity .16s ease, transform .16s ease;
308
392
  }
309
- .oasub-button:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(15, 17, 21, .06)); }
393
+ .oasub-button:not(.primary):not(.danger):hover:not(:disabled) { border-color: var(--oasub-control-border-hover); background: var(--dsw-alias-interactive-bg-hover, rgba(15, 17, 21, .06)); }
310
394
  .oasub-button:active:not(:disabled) { transform: translateY(1px); }
311
395
  .oasub-button:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #4176e6); outline-offset: 2px; }
312
396
  .oasub-button:disabled { cursor: default; opacity: .45; }
@@ -357,10 +441,19 @@ window.__ModuleLoader__.load({
357
441
  .oasub-dialog p { margin: 8px 0 18px; color: var(--dsw-alias-label-secondary, #61666b); font-size: 13px; line-height: 20px; }
358
442
  .oasub-dialog .oasub-actions { justify-content: flex-end; }
359
443
  @media (max-width: 520px) {
360
- .oasub-card { padding: 16px; }
444
+ .oasub-wrap { --oasub-section-inset: 16px; }
361
445
  .oasub-status-line { flex-direction: column; }
362
446
  .oasub-button { flex: 1 1 auto; }
363
447
  .oasub-button.quiet { margin-left: 0; }
448
+ .oasub-manage-row { align-items: flex-start; flex-direction: column; gap: 8px; }
449
+ }
450
+ @media (forced-colors: active) {
451
+ .oasub-button { border-color: ButtonText; }
452
+ .oasub-button.primary, .oasub-button.danger { border-color: ButtonText; color: ButtonText; background: ButtonFace; }
453
+ .oasub-button:not(.primary):not(.danger):hover:not(:disabled),
454
+ .oasub-button.primary:hover:not(:disabled), .oasub-button.danger:hover:not(:disabled) { border-color: Highlight; filter: none; }
455
+ .oasub-button:focus-visible { outline-color: Highlight; }
456
+ .oasub-button:disabled { border-color: GrayText; color: GrayText; opacity: 1; }
364
457
  }
365
458
  @media (prefers-reduced-motion: reduce) {
366
459
  .oasub-button { transition: none; }
@@ -388,6 +481,7 @@ window.__ModuleLoader__.load({
388
481
  'timeout', 'invalid-response', 'process-exited', 'credential-write-failed', 'credential-changed',
389
482
  'settings-unavailable', 'models-unavailable', 'models-empty', 'models-confirmation-required',
390
483
  'settings-conflict', 'settings-write-failed', 'ownership-save-failed', 'cancelled', 'unknown',
484
+ 'invalid-context-window', 'context-window-exceeded', 'model-not-found',
391
485
  ]);
392
486
  function allowedCode(value) {
393
487
  return typeof value === 'string' && ERROR_CODES.has(value) ? value : undefined;
@@ -534,6 +628,25 @@ window.__ModuleLoader__.load({
534
628
  }
535
629
  return { synced: true, count: raw.count, warningCode: raw.warningCode == null ? undefined : allowedCode(raw.warningCode) ?? 'unknown' };
536
630
  }
631
+ function positiveInteger(value) {
632
+ return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : undefined;
633
+ }
634
+ function parseModelContexts(value) {
635
+ const raw = recordOf(value);
636
+ if (raw === null || typeof raw.revision !== 'number' || !Number.isSafeInteger(raw.revision) || raw.revision < 0 || !Array.isArray(raw.models)) {
637
+ throw new ClientFailure('invalid-response');
638
+ }
639
+ const models = [];
640
+ for (const item of raw.models) {
641
+ const row = recordOf(item);
642
+ if (!row || typeof row.id !== 'string' || !row.id || models.some((model) => model.id === row.id))
643
+ throw new ClientFailure('invalid-response');
644
+ models.push({ id: row.id, name: typeof row.name === 'string' ? row.name : undefined,
645
+ contextWindow: positiveInteger(row.contextWindow), defaultContextWindow: positiveInteger(row.defaultContextWindow),
646
+ maxContextWindow: positiveInteger(row.maxContextWindow), customized: row.customized === true });
647
+ }
648
+ return { revision: raw.revision, models };
649
+ }
537
650
  function safeVerificationUrl(value) {
538
651
  if (typeof value !== 'string' || value !== value.trim())
539
652
  return null;
@@ -565,6 +678,14 @@ window.__ModuleLoader__.load({
565
678
  const [cancelPending, setCancelPending] = React.useState(false);
566
679
  const [confirm, setConfirm] = React.useState(null);
567
680
  const [statusRevision, setStatusRevision] = React.useState(0);
681
+ const [managementOpen, setManagementOpen] = React.useState(false);
682
+ const [contexts, setContexts] = React.useState(null);
683
+ const [contextLoading, setContextLoading] = React.useState(false);
684
+ const [contextError, setContextError] = React.useState(null);
685
+ const [modelId, setModelId] = React.useState('');
686
+ // null means no pending edit; '' selects the synchronized model default.
687
+ const [contextDraft, setContextDraft] = React.useState(null);
688
+ const [contextChoice, setContextChoice] = React.useState(null);
568
689
  const actionLock = React.useRef(false);
569
690
  const cancelAcknowledged = React.useRef(false);
570
691
  const flowObserved = React.useRef(false);
@@ -623,6 +744,32 @@ window.__ModuleLoader__.load({
623
744
  controller.abort();
624
745
  };
625
746
  }, [connection, statusRevision]);
747
+ React.useEffect(() => {
748
+ if (!configured) {
749
+ setContexts(null);
750
+ setContextDraft(null);
751
+ setContextChoice(null);
752
+ return;
753
+ }
754
+ const controller = new AbortController();
755
+ setContextLoading(true);
756
+ setContextError(null);
757
+ remoteCall(connection, 'getModelContexts', {}, controller.signal).then(parseModelContexts).then((result) => {
758
+ if (controller.signal.aborted)
759
+ return;
760
+ setContexts(result);
761
+ setModelId((id) => result.models.some((model) => model.id === id) ? id : result.models[0]?.id ?? '');
762
+ setContextDraft(null);
763
+ setContextChoice(null);
764
+ }).catch((error) => {
765
+ if (!controller.signal.aborted) {
766
+ setContexts(null);
767
+ setContextError(errorKey(error, 'context.error'));
768
+ }
769
+ }).finally(() => { if (!controller.signal.aborted)
770
+ setContextLoading(false); });
771
+ return () => controller.abort();
772
+ }, [connection, configured, statusRevision]);
626
773
  React.useEffect(() => {
627
774
  if (props.subscribeReset === undefined)
628
775
  return;
@@ -941,6 +1088,41 @@ window.__ModuleLoader__.load({
941
1088
  }
942
1089
  });
943
1090
  };
1091
+ const selectedModel = contexts?.models.find((model) => model.id === modelId);
1092
+ const savedContext = selectedModel?.customized && selectedModel.contextWindow ? String(selectedModel.contextWindow) : '';
1093
+ const draft = contextDraft ?? savedContext;
1094
+ const contextMode = contextChoice ?? (draft === '' ? 'default' : draft === '1000000' ? 'million' : 'custom');
1095
+ const requestedContext = draft === '' ? null : /^\d+$/.test(draft) ? positiveInteger(Number(draft)) : undefined;
1096
+ const contextValidation = requestedContext === undefined ? 'error.invalid-context-window'
1097
+ : requestedContext !== null && selectedModel?.maxContextWindow !== undefined && requestedContext > selectedModel.maxContextWindow
1098
+ ? 'error.context-window-exceeded' : null;
1099
+ const contextDirty = contextDraft !== null && (draft !== savedContext || (draft === '' && selectedModel?.customized === true));
1100
+ const saveContext = () => {
1101
+ const signal = lifetime.current?.signal;
1102
+ if (phaseRef.current !== 'idle' || actionLock.current || !signal || signal.aborted || !contexts || !selectedModel || !contextDirty || contextValidation || contextLoading || statusLoading)
1103
+ return;
1104
+ actionLock.current = true;
1105
+ changePhase('saving-context');
1106
+ setNotice(null);
1107
+ remoteCall(connection, 'setModelContext', {
1108
+ modelId: selectedModel.id, contextWindow: requestedContext, revision: contexts.revision,
1109
+ }, signal, MUTATION_TIMEOUT_MS).then((result) => {
1110
+ if (signal.aborted)
1111
+ return;
1112
+ if (result?.saved !== true)
1113
+ throw new ClientFailure('invalid-response');
1114
+ setNotice({ tone: 'success', key: 'context.saved' });
1115
+ }).catch((error) => {
1116
+ if (!signal.aborted)
1117
+ setNotice({ tone: 'error', key: failureCode(error) === 'timeout' ? 'error.long-action' : errorKey(error, 'error.settings-write-failed') });
1118
+ }).finally(() => {
1119
+ if (!signal.aborted) {
1120
+ actionLock.current = false;
1121
+ changePhase('idle');
1122
+ reloadStatus();
1123
+ }
1124
+ });
1125
+ };
944
1126
  const selectCode = () => {
945
1127
  codeRef.current?.focus();
946
1128
  codeRef.current?.select();
@@ -988,6 +1170,7 @@ window.__ModuleLoader__.load({
988
1170
  type: 'button',
989
1171
  className: 'oasub-button ' + (info.modelsSynced ? '' : 'primary'),
990
1172
  disabled: busy,
1173
+ 'aria-describedby': 'oasub-sync-help',
991
1174
  onClick: requestModelSync,
992
1175
  }, phase === 'syncing' ? t('action.syncing') : t(info.modelsSynced ? 'action.updateModels' : 'action.sync'))
993
1176
  : el('button', {
@@ -995,25 +1178,32 @@ window.__ModuleLoader__.load({
995
1178
  className: 'oasub-button primary',
996
1179
  disabled: busy || !ready,
997
1180
  onClick: () => startAuthorization('device_code'),
998
- }, phase === 'starting' || phase === 'authorizing' ? t('action.connecting') : t('action.connect')), configured
1181
+ }, phase === 'starting' || phase === 'authorizing' ? t('action.connecting') : t('action.connect')), configured && info.credentialState !== 'valid'
999
1182
  ? el('button', {
1000
- type: 'button',
1001
- className: 'oasub-button',
1002
- disabled: busy || !ready,
1183
+ type: 'button', className: 'oasub-button primary', disabled: busy || !ready,
1184
+ title: t(info.refreshable ? 'manage.refresh.help' : 'manage.reconnect.help'),
1003
1185
  onClick: () => startAuthorization(info.refreshable ? 'refresh' : 'device_code'),
1004
- }, phase === 'starting' || phase === 'authorizing'
1005
- ? t('action.refreshing')
1006
- : t(info.refreshable ? 'action.refresh' : 'action.reconnect'))
1007
- : null, info.cleanupAvailable
1008
- ? el('button', {
1009
- type: 'button',
1010
- className: 'oasub-button danger quiet',
1011
- disabled: busy,
1012
- onClick: () => openConfirm('disconnect'),
1013
- }, phase === 'disconnecting' ? t('action.disconnecting') : t('action.disconnect'))
1014
- : null));
1015
- return el('div', { className: 'oasub-wrap' }, el('header', { className: 'oasub-header' }, el('div', { className: 'oasub-mark', 'aria-hidden': true }, '✦'), el('div', { className: 'oasub-heading' }, el('h2', { className: 'oasub-title' }, t('title')), el('p', { className: 'oasub-subtitle' }, t('subtitle')))), statusCard, statusError !== null
1016
- ? el('div', { className: 'oasub-notice error', role: 'alert' }, t(statusError), info !== null ? ' ' + t('status.stale') : null, el('button', { type: 'button', className: 'oasub-button quiet', disabled: statusLoading, onClick: reloadStatus }, t('action.retry'))) : null, el('div', { className: 'oasub-actions' }, el('button', { ref: reloadRef, type: 'button', className: 'oasub-button', disabled: statusLoading, onClick: reloadStatus }, t(statusLoading ? 'status.reloading' : 'action.reload'))), device !== null
1186
+ }, phase === 'starting' || phase === 'authorizing' ? t('action.refreshing') : t(info.refreshable ? 'action.refresh' : 'action.reconnect'))
1187
+ : null), configured ? el('div', { id: 'oasub-sync-help', className: 'oasub-status-detail' }, t('model.sync.help')) : null);
1188
+ return el('div', { className: 'oasub-wrap' }, el('header', { className: 'oasub-header' }, el('div', { className: 'oasub-mark', 'aria-hidden': true }, el(OpenAIIcon, { size: 24 })), el('div', { className: 'oasub-heading' }, el('h2', { className: 'oasub-title' }, t('title')), el('p', { className: 'oasub-subtitle' }, t('subtitle')))), statusCard, configured ? el('section', { className: 'oasub-card oasub-context', 'aria-labelledby': 'oasub-context-title' }, el('div', { id: 'oasub-context-title', className: 'oasub-status-title' }, t('context.title')), el('div', { className: 'oasub-status-detail' }, t('context.help')), contextLoading ? el('div', { role: 'status', className: 'oasub-status-detail' }, t('context.loading')) : null, contextError ? el('div', { role: 'alert', className: 'oasub-notice error' }, t(contextError), el('button', { type: 'button', className: 'oasub-button quiet', disabled: contextLoading, onClick: reloadStatus }, t('action.retry'))) : null, !contextLoading && contexts?.models.length === 0 ? el('div', { className: 'oasub-status-detail' }, t('context.empty')) : null, selectedModel ? el('div', { className: 'oasub-context' }, el('div', { className: 'oasub-actions' }, el('label', { className: 'oasub-field' }, t('context.model'), el('select', { className: 'oasub-select', 'aria-label': t('context.model'), value: modelId, disabled: busy || contextLoading || statusLoading,
1189
+ onChange: (event) => { setModelId(event.currentTarget.value); setContextDraft(null); setContextChoice(null); } }, contexts?.models.map((model) => el('option', { key: model.id, value: model.id }, model.name && model.name !== model.id ? model.name + ' (' + model.id + ')' : model.id)))), el('label', { className: 'oasub-field' }, t('context.window'), el('select', { className: 'oasub-select', 'aria-label': t('context.window'), value: contextMode, disabled: busy || contextLoading || statusLoading,
1190
+ 'aria-describedby': 'oasub-context-limit',
1191
+ onChange: (event) => {
1192
+ const choice = event.currentTarget.value === 'default' ? 'default' : event.currentTarget.value === 'million' ? 'million' : 'custom';
1193
+ setContextChoice(choice);
1194
+ setContextDraft(choice === 'default' ? '' : choice === 'million' ? '1000000' : '-');
1195
+ } }, el('option', { value: 'default' }, t('context.default') + (selectedModel.defaultContextWindow ? ' · ' + selectedModel.defaultContextWindow.toLocaleString() : '')), el('option', { value: 'million', disabled: selectedModel.maxContextWindow !== undefined && selectedModel.maxContextWindow < 1_000_000 }, t('context.million')), el('option', { value: 'custom' }, t('context.custom'))))), contextMode === 'custom' ? el('label', { className: 'oasub-field' }, t('context.tokens'), el('input', { className: 'oasub-select', type: 'text', inputMode: 'numeric', 'aria-label': t('context.tokens'),
1196
+ value: draft === '-' ? '' : draft, disabled: busy || contextLoading || statusLoading,
1197
+ 'aria-invalid': contextValidation !== null, 'aria-describedby': 'oasub-context-limit',
1198
+ onChange: (event) => setContextDraft(event.currentTarget.value || '-') })) : null, selectedModel.contextWindow ? el('div', { className: 'oasub-status-detail' }, t('context.current', { count: selectedModel.contextWindow.toLocaleString() })) : null, el('div', { id: 'oasub-context-limit', className: 'oasub-status-detail' }, selectedModel.maxContextWindow ? t('context.maximum', { count: selectedModel.maxContextWindow.toLocaleString() }) + ' · ' + t('context.limit.help') : t('context.unknown')), contextDirty && contextValidation ? el('div', { role: 'alert', className: 'oasub-status-detail' }, t(contextValidation)) : null, contextDirty ? el('div', { className: 'oasub-actions' }, el('button', { type: 'button', className: 'oasub-button primary', disabled: busy || contextLoading || statusLoading || contextValidation !== null,
1199
+ onClick: saveContext }, t(phase === 'saving-context' ? 'context.saving' : 'context.save'))) : null) : null) : null, statusError !== null
1200
+ ? el('div', { className: 'oasub-notice error', role: 'alert' }, t(statusError), info !== null ? ' ' + t('status.stale') : null, el('button', { type: 'button', className: 'oasub-button quiet', disabled: statusLoading, onClick: reloadStatus }, t('action.retry'))) : null, el('button', { ref: reloadRef, type: 'button', className: 'oasub-button disclosure',
1201
+ 'aria-expanded': managementOpen, 'aria-controls': managementOpen ? 'oasub-management' : undefined,
1202
+ onClick: () => setManagementOpen((open) => !open) }, t(managementOpen ? 'action.less' : 'action.more')), managementOpen ? el('section', { id: 'oasub-management', className: 'oasub-management', 'aria-label': t('action.more') }, configured && info?.credentialState === 'valid' ? el('div', { className: 'oasub-manage-row' }, el('div', { className: 'oasub-status-detail', id: 'oasub-refresh-help' }, t(info.refreshable ? 'manage.refresh.help' : 'manage.reconnect.help')), el('button', { type: 'button', className: 'oasub-button', disabled: busy || !ready,
1203
+ 'aria-describedby': 'oasub-refresh-help',
1204
+ onClick: () => startAuthorization(info.refreshable ? 'refresh' : 'device_code') }, phase === 'starting' || phase === 'authorizing' ? t('action.refreshing') : t(info.refreshable ? 'action.refresh' : 'action.reconnect'))) : null, el('div', { className: 'oasub-manage-row' }, el('div', { className: 'oasub-status-detail', id: 'oasub-reload-help' }, t('manage.reload.help')), el('button', { type: 'button', className: 'oasub-button', disabled: statusLoading,
1205
+ 'aria-describedby': 'oasub-reload-help', onClick: reloadStatus }, t(statusLoading ? 'status.reloading' : 'action.reload'))), info?.cleanupAvailable ? el('div', { className: 'oasub-manage-row' }, el('div', { className: 'oasub-status-detail', id: 'oasub-disconnect-help' }, t('manage.disconnect.help')), el('button', { type: 'button', className: 'oasub-button', disabled: busy,
1206
+ 'aria-describedby': 'oasub-disconnect-help', onClick: () => openConfirm('disconnect') }, t(phase === 'disconnecting' ? 'action.disconnecting' : 'action.disconnect'))) : null) : null, device !== null
1017
1207
  ? el('section', { className: 'oasub-card oasub-device', 'aria-label': t('device.title') }, el('div', { className: 'oasub-device-head' }, el('div', { className: 'oasub-status-title' }, t('device.title')), el('div', { className: 'oasub-status-detail' }, t('device.detail'))), el('input', { ref: codeRef, className: 'oasub-code', type: 'text', readOnly: true, value: device.code,
1018
1208
  'aria-label': t('device.code'), autoComplete: 'off', spellCheck: false, onFocus: (event) => event.currentTarget.select() }), el('div', { className: 'oasub-actions' }, device.url !== null
1019
1209
  ? el('a', { className: 'oasub-button primary', href: device.url, target: '_blank', rel: 'noopener noreferrer',
@@ -1060,6 +1250,7 @@ window.__ModuleLoader__.load({
1060
1250
  id: 'openai-subscription',
1061
1251
  order: 25,
1062
1252
  label: () => t('nav'),
1253
+ icon: OpenAIIcon,
1063
1254
  locale: NS,
1064
1255
  }, () => React.createElement(Section, { connection: ctx.connection, timer: ctx.timer, t, subscribeReset })));
1065
1256
  }
package/dist/errors.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Stable, non-sensitive failures shared by the host's public boundaries. */
2
- export declare const FAILURE_CODES: readonly ['credentials-unavailable', 'shell-unavailable', 'timer-unavailable', 'component-unavailable', 'runtime-unsupported', 'busy', 'invalid-method', 'not-connected', 'not-refreshable', 'device-auth-disabled', 'access-denied', 'authorization-expired', 'rate-limited', 'network', 'timeout', 'invalid-response', 'process-exited', 'credential-write-failed', 'credential-changed', 'settings-unavailable', 'models-unavailable', 'models-empty', 'models-confirmation-required', 'settings-conflict', 'settings-write-failed', 'ownership-save-failed', 'cancelled', 'unknown'];
2
+ export declare const FAILURE_CODES: readonly ['credentials-unavailable', 'shell-unavailable', 'timer-unavailable', 'component-unavailable', 'runtime-unsupported', 'busy', 'invalid-method', 'not-connected', 'not-refreshable', 'device-auth-disabled', 'access-denied', 'authorization-expired', 'rate-limited', 'network', 'timeout', 'invalid-response', 'process-exited', 'credential-write-failed', 'credential-changed', 'settings-unavailable', 'models-unavailable', 'models-empty', 'models-confirmation-required', 'settings-conflict', 'settings-write-failed', 'ownership-save-failed', 'invalid-context-window', 'context-window-exceeded', 'model-not-found', 'cancelled', 'unknown'];
3
3
  export type FailureCode = typeof FAILURE_CODES[number];
4
4
  /** The marker survives RPC implementations which serialize only Error.message. */
5
5
  export declare class SubscriptionError extends Error {
package/dist/errors.js CHANGED
@@ -6,7 +6,8 @@ export const FAILURE_CODES = [
6
6
  'network', 'timeout', 'invalid-response', 'process-exited', 'credential-write-failed',
7
7
  'credential-changed', 'settings-unavailable', 'models-unavailable', 'models-empty',
8
8
  'models-confirmation-required', 'settings-conflict', 'settings-write-failed',
9
- 'ownership-save-failed', 'cancelled', 'unknown',
9
+ 'ownership-save-failed', 'invalid-context-window', 'context-window-exceeded', 'model-not-found',
10
+ 'cancelled', 'unknown',
10
11
  ];
11
12
  /** The marker survives RPC implementations which serialize only Error.message. */
12
13
  export class SubscriptionError extends Error {