dsh-bundle-antigravity-host 0.1.31 → 0.1.36

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.
Files changed (68) hide show
  1. package/README.md +17 -9
  2. package/cordis.patch.yml +7 -2
  3. package/dist/adapter/antigravity-adapter.js +13 -3
  4. package/dist/adapter/antigravity-adapter.js.map +1 -1
  5. package/dist/adapter/session-reuse.d.ts +4 -1
  6. package/dist/adapter/session-reuse.js +3 -1
  7. package/dist/adapter/session-reuse.js.map +1 -1
  8. package/dist/client.js +563 -245
  9. package/dist/index.js +92 -7
  10. package/dist/index.js.map +1 -1
  11. package/dist/proxy/account-remote.d.ts +34 -0
  12. package/dist/proxy/account-remote.js +92 -0
  13. package/dist/proxy/account-remote.js.map +1 -0
  14. package/dist/proxy/account-service.d.ts +78 -0
  15. package/dist/proxy/account-service.js +364 -0
  16. package/dist/proxy/account-service.js.map +1 -0
  17. package/dist/proxy/account-store.d.ts +56 -0
  18. package/dist/proxy/account-store.js +182 -0
  19. package/dist/proxy/account-store.js.map +1 -0
  20. package/dist/proxy/adapter.d.ts +72 -0
  21. package/dist/proxy/adapter.js +812 -0
  22. package/dist/proxy/adapter.js.map +1 -0
  23. package/dist/proxy/image.d.ts +27 -0
  24. package/dist/proxy/image.js +82 -0
  25. package/dist/proxy/image.js.map +1 -0
  26. package/dist/proxy/index.d.ts +19 -0
  27. package/dist/proxy/index.js +118 -0
  28. package/dist/proxy/index.js.map +1 -0
  29. package/dist/proxy/mode-bus.d.ts +50 -0
  30. package/dist/proxy/mode-bus.js +38 -0
  31. package/dist/proxy/mode-bus.js.map +1 -0
  32. package/dist/proxy/oauth.d.ts +87 -0
  33. package/dist/proxy/oauth.js +430 -0
  34. package/dist/proxy/oauth.js.map +1 -0
  35. package/dist/proxy/payload.d.ts +67 -0
  36. package/dist/proxy/payload.js +540 -0
  37. package/dist/proxy/payload.js.map +1 -0
  38. package/dist/proxy/quota-source.d.ts +15 -0
  39. package/dist/proxy/quota-source.js +115 -0
  40. package/dist/proxy/quota-source.js.map +1 -0
  41. package/dist/proxy/replay.d.ts +34 -0
  42. package/dist/proxy/replay.js +238 -0
  43. package/dist/proxy/replay.js.map +1 -0
  44. package/dist/proxy/transport.d.ts +55 -0
  45. package/dist/proxy/transport.js +723 -0
  46. package/dist/proxy/transport.js.map +1 -0
  47. package/dist/proxy/wire-identity.d.ts +25 -0
  48. package/dist/proxy/wire-identity.js +124 -0
  49. package/dist/proxy/wire-identity.js.map +1 -0
  50. package/dist/quota/control.d.ts +10 -0
  51. package/dist/quota/control.js +17 -2
  52. package/dist/quota/control.js.map +1 -1
  53. package/dist/quota/quota-remote.d.ts +36 -3
  54. package/dist/quota/quota-remote.js +93 -12
  55. package/dist/quota/quota-remote.js.map +1 -1
  56. package/dist/runtime/proxy-env.d.ts +2 -0
  57. package/dist/runtime/proxy-env.js +8 -0
  58. package/dist/runtime/proxy-env.js.map +1 -1
  59. package/dist/runtime/quota.d.ts +3 -0
  60. package/dist/runtime/quota.js.map +1 -1
  61. package/dist/settings.d.ts +21 -1
  62. package/dist/settings.js +5 -1
  63. package/dist/settings.js.map +1 -1
  64. package/docs/changelog.md +53 -0
  65. package/docs/configuration.md +23 -6
  66. package/docs/development.md +19 -8
  67. package/docs/reference.md +33 -7
  68. package/package.json +4 -2
package/dist/client.js CHANGED
@@ -23,19 +23,32 @@ window.__ModuleLoader__.load({
23
23
  const React = require('react')
24
24
  const h = React.createElement
25
25
  const PROVIDER = 'antigravity-acp'
26
+ const PROXY_PROVIDER = 'antigravity-proxy'
26
27
  /** Settings namespace projected from Config's volatile fields in src/settings.ts. */
27
28
  const ENTRY_ID = 'llm-antigravity-acp'
28
29
  /** Live fields owned by this card; ordinary fields stay in the host config editor. */
29
30
  const FIELDS = [
30
- { key: 'autoWarmup', group: 'general', checkbox: true, defaultChecked: true, label: '启动时自动预热', hint: '提前启动并保留连接,不发送模型消息。保存后下次启用插件生效;本次可点击“立即预热”。' },
31
- { key: 'reuseSessions', group: 'general', checkbox: true, defaultChecked: true, label: '复用 ACP 会话(增量发送)', hint: '校验历史与实际输出后只发新增消息;变化的上下文仍发送,不一致时自动重建。' },
32
- { key: 'proxyUrl', group: 'network', label: '出网代理', placeholder: 'http://127.0.0.1:7890', hint: '此处配置优先;留空则继承宿主代理环境变量,宿主也未配置时才直连。支持 HTTP、HTTPS 与 SOCKS5。' },
33
- { key: 'proxyBypass', group: 'network', label: '代理排除列表', placeholder: '*.internal,example.internal', hint: '逗号分隔,支持域名与通配后缀,不支持 CIDR。localhost/127.0.0.1/::1 始终自动排除(私有 MCP 桥在 loopback)。' },
34
- { key: 'agyCliPath', group: 'network', label: '官方 CLI 路径(可选)', placeholder: '留空自动查找 agy', hint: '用于模型目录、套餐额度和 AI Credits。自定义安装位置可填写完整可执行文件路径;CLI 登录与 ACP 登录独立。' },
35
- { key: 'resumeSessions', group: 'advanced', checkbox: true, label: '重启后恢复 ACP 会话', hint: '尝试加载本地映射,失败则重建;仅保存 ID 与指纹。需要同时开启会话复用。' },
36
- { key: 'keepProcessAfterCancel', group: 'advanced', checkbox: true, label: '取消后保留已停止的进程', hint: '仅在 agent 确认停止后保留;等待期间新轮排队,超时或通信失败回收。' },
37
- { key: 'cancelGraceMs', group: 'advanced', numeric: true, defaultValue: 30000, min: 0, max: 60000, label: '取消确认宽限(毫秒)', hint: '默认 30000;0 表示不等待并立即回收,不会复用未确认连接。仅开启取消后保留进程时生效。' },
38
- { key: 'idleRecycleMs', group: 'advanced', numeric: true, defaultValue: 300000, min: 0, max: 3600000, label: '进程空闲回收(毫秒)', hint: '默认 300000(5 分钟),0 禁用。聊天、预热、目录发现结束后统一计时;等待工具不算空闲。' },
31
+ { key: 'proxyMode', group: 'mode', checkbox: true, label: '使用代理模式', hint: '直连上游;需启用代理路由并连接代理账户。' },
32
+ { key: 'cliQuotaEnabled', group: 'quota', checkbox: true, label: '使用 agy CLI 补充额度', hint: '默认不启动 agy;开启后才读取 AI Credits。' },
33
+ { key: 'autoWarmup', group: 'general', checkbox: true, defaultChecked: true, acpOnly: true, label: '启动时自动预热', hint: '不发送消息;下次启用插件时生效。' },
34
+ { key: 'reuseSessions', group: 'general', checkbox: true, defaultChecked: true, acpOnly: true, label: '复用 ACP 会话', hint: '只发送新增消息;上下文不一致时自动重建。' },
35
+ { key: 'proxyUrl', group: 'network', label: '出网代理', placeholder: 'http://127.0.0.1:7890', hint: '留空继承宿主代理环境。' },
36
+ { key: 'agyCliPath', group: 'network', label: '官方 CLI 路径', placeholder: '留空自动查找 agy', hint: '仅用于模型目录和可选的 CLI 额度查询。' },
37
+ { key: 'resumeSessions', group: 'advanced', checkbox: true, acpOnly: true, label: '重启后恢复 ACP 会话', hint: '需开启会话复用;恢复失败时自动重建。' },
38
+ { key: 'keepProcessAfterCancel', group: 'advanced', checkbox: true, acpOnly: true, label: '取消后保留进程', hint: '仅在官方进程确认停止后保留。' },
39
+ { key: 'cancelGraceMs', group: 'advanced', numeric: true, defaultValue: 30000, min: 0, max: 60000, acpOnly: true, label: '取消确认宽限(毫秒)', hint: '0 表示立即回收。' },
40
+ { key: 'idleRecycleMs', group: 'advanced', numeric: true, defaultValue: 300000, min: 0, max: 3600000, acpOnly: true, label: '进程空闲回收(毫秒)', hint: '默认 5 分钟;0 表示禁用。' },
41
+ { key: 'binaryPath', group: 'launch', acpOnly: true, label: 'ACP Server 路径', placeholder: '留空自动解析或下载', hint: '优先于环境变量与缓存。' },
42
+ { key: 'autoDownload', group: 'launch', checkbox: true, defaultChecked: true, acpOnly: true, label: '自动下载 ACP Server', hint: '仅从官方 dl.google.com 下载。' },
43
+ { key: 'acpVersion', group: 'launch', acpOnly: true, label: 'ACP Server 版本', placeholder: '1.4.0', hint: '程序版本,不是 ACP 协议版本。' },
44
+ { key: 'extraArgs', group: 'launch', lines: true, acpOnly: true, label: 'ACP 额外启动参数', placeholder: '每行一个参数', hint: '按行原样传给官方进程。' },
45
+ { key: 'maxConcurrentSessions', group: 'pool', numeric: true, defaultValue: 2, min: 1, max: 4, acpOnly: true, label: '最大 ACP 连接数', hint: '范围 1–4。' },
46
+ { key: 'queueTimeoutMs', group: 'pool', numeric: true, defaultValue: 120000, min: 1000, acpOnly: true, label: '连接排队超时(毫秒)', hint: '最小 1000。' },
47
+ { key: 'requestTimeoutMs', group: 'timeouts', numeric: true, defaultValue: 30000, min: 0, acpOnly: true, label: '普通请求超时(毫秒)' },
48
+ { key: 'handshakeTimeoutMs', group: 'timeouts', numeric: true, defaultValue: 120000, min: 1000, acpOnly: true, label: '握手超时(毫秒)', hint: '冷启动建议至少 120000。' },
49
+ { key: 'authTimeoutMs', group: 'timeouts', numeric: true, defaultValue: 300000, min: 0, acpOnly: true, label: '浏览器登录超时(毫秒)' },
50
+ { key: 'promptTimeoutMs', group: 'timeouts', numeric: true, defaultValue: 1800000, min: 0, acpOnly: true, label: '单轮生成超时(毫秒)', hint: '默认 30 分钟。' },
51
+ { key: 'bridgeParkSettleMs', group: 'timeouts', numeric: true, defaultValue: 50, min: 0, acpOnly: true, label: '工具调用聚合窗口(毫秒)', hint: '不是工具执行超时。' },
39
52
  ]
40
53
 
41
54
  /** 逐模型设置:键是 DSH 选择器里的模型 ID,值是开关、默认档位、显示名与官方 ID 映射。 */
@@ -137,7 +150,7 @@ window.__ModuleLoader__.load({
137
150
  : account?.runtime?.phase === 'error'
138
151
  ? account.runtime.error ?? account?.message ?? 'ACP 连接异常,请重试。'
139
152
  : registered === false ? '请确认插件已启用,Host 与 Client 均已加载。'
140
- : startupHint(account) ?? account?.message ?? '连接后可验证账户并读取模型目录,不会发送聊天消息。'
153
+ : startupHint(account) ?? account?.message ?? '连接后可读取模型目录,不发送聊天消息。'
141
154
  return { label, description, ready, busy, error, color: error ? '#d97757' : ready ? '#25b879' : busy ? '#d99b28' : '#8b909a' }
142
155
  }
143
156
  function StatusDot({ color }) {
@@ -146,7 +159,7 @@ window.__ModuleLoader__.load({
146
159
  function StatusNotice({ status }) {
147
160
  if (!status) return null
148
161
  const error = status.kind === 'error'
149
- return h('p', { role: error ? 'alert' : 'status', style: { margin: 0, padding: '10px 12px', borderRadius: '10px', fontSize: '12px', lineHeight: 1.6, overflowWrap: 'anywhere', color: error ? '#b42318' : status.kind === 'busy' ? '#946200' : '#177c55', background: error ? 'rgba(217,119,87,.10)' : status.kind === 'busy' ? 'rgba(217,155,40,.10)' : 'rgba(37,184,121,.10)' } }, status.text)
162
+ return h('p', { role: error ? 'alert' : 'status', style: { margin: 0, padding: '10px 12px', borderRadius: '10px', borderLeft: '3px solid ' + (error ? '#e5534b' : status.kind === 'busy' ? '#d99b28' : '#25b879'), fontSize: '12px', lineHeight: 1.6, overflowWrap: 'anywhere', color: 'inherit', background: error ? 'rgba(217,119,87,.10)' : status.kind === 'busy' ? 'rgba(217,155,40,.10)' : 'rgba(37,184,121,.10)' } }, status.text)
150
163
  }
151
164
  function RuntimeDetails({ runtime }) {
152
165
  if (!runtime) return null
@@ -170,104 +183,208 @@ window.__ModuleLoader__.load({
170
183
  runtime.error ? h('p', { role: 'alert', style: { margin: '4px 0 0', color: '#b42318', overflowWrap: 'anywhere' } }, runtime.error) : null)
171
184
  }
172
185
  function SectionHeading({ eyebrow, title, description, action }) {
173
- return h('div', { style: { display: 'flex', alignItems: 'flex-start', gap: '12px', marginBottom: '16px' } },
186
+ return h('div', { style: { display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' } },
174
187
  h('div', { style: { flex: 1, minWidth: 0 } },
175
- eyebrow ? h('div', { style: { fontSize: '10px', fontWeight: 650, letterSpacing: '.08em', opacity: .5, marginBottom: '4px' } }, eyebrow) : null,
188
+ eyebrow ? h('div', { style: { fontSize: '10px', fontWeight: 650, letterSpacing: '.08em', opacity: .5, marginBottom: '3px' } }, eyebrow) : null,
176
189
  h('h3', { style: { fontSize: '14px', lineHeight: 1.4, fontWeight: 650, margin: 0 } }, title),
177
- description ? h('p', { style: { fontSize: '12px', lineHeight: 1.6, opacity: .6, margin: '5px 0 0' } }, description) : null),
190
+ description ? h('p', { style: { fontSize: '11px', lineHeight: 1.5, opacity: .6, margin: '3px 0 0' } }, description) : null),
178
191
  action ?? null)
179
192
  }
180
193
  function TextField({ field, value, disabled, invalid, onChange }) {
181
194
  const inputId = 'agy-setting-' + (React.useId?.() ?? field.key) + '-' + field.key
182
- return h('div', { style: { display: 'grid', gap: '6px' } },
195
+ return h('div', { style: { display: 'grid', gap: '4px', minWidth: 0 } },
183
196
  h('label', { htmlFor: inputId, style: { fontSize: '12px', fontWeight: 550 } }, field.label),
184
- h('input', {
197
+ h(field.lines ? 'textarea' : 'input', {
185
198
  id: inputId, name: field.key, type: field.numeric ? 'number' : 'text',
186
199
  ...(field.numeric ? { min: field.min, max: field.max, step: 1 } : {}),
200
+ ...(field.lines ? { rows: 2 } : {}),
187
201
  disabled, value: value ?? '', placeholder: field.placeholder,
188
- 'aria-invalid': invalid || undefined, 'aria-describedby': inputId + '-hint',
202
+ 'aria-invalid': invalid || undefined, 'aria-describedby': field.hint ? inputId + '-hint' : undefined,
189
203
  style: { ...ACTION, cursor: disabled ? 'not-allowed' : 'text', width: '100%', boxSizing: 'border-box', background: 'rgba(128,128,128,.035)', opacity: disabled ? .6 : 1 },
190
204
  onChange: (event) => onChange(field.numeric && event.target.value !== '' ? Number(event.target.value) : event.target.value),
191
205
  }),
192
- h('small', { id: inputId + '-hint', style: { fontSize: '11px', lineHeight: 1.6, opacity: .6 } }, field.hint))
206
+ field.hint ? h('small', { id: inputId + '-hint', style: { fontSize: '10px', lineHeight: 1.45, opacity: .58 } }, field.hint) : null)
193
207
  }
194
208
  function SwitchField({ field, checked, disabled, onChange }) {
195
209
  return h('label', { style: { display: 'flex', alignItems: 'flex-start', gap: '10px', cursor: disabled ? 'default' : 'pointer', padding: '2px 0', opacity: disabled ? .6 : 1 } },
196
- h('input', { name: field.key, type: 'checkbox', checked: checked === true, disabled, onChange: (event) => onChange(event.target.checked), style: { width: '16px', height: '16px', flex: '0 0 auto', margin: '2px 0 0', accentColor: '#177c55', cursor: disabled ? 'not-allowed' : 'pointer' } }),
197
- h('span', { style: { display: 'grid', gap: '4px', minWidth: 0 } },
210
+ h('input', { name: field.key, type: 'checkbox', checked: checked === true, disabled, onChange: (event) => onChange(event.target.checked), style: { width: '16px', height: '16px', flex: '0 0 auto', margin: '1px 0 0', accentColor: '#177c55', cursor: disabled ? 'not-allowed' : 'pointer' } }),
211
+ h('span', { style: { display: 'grid', gap: '2px', minWidth: 0 } },
198
212
  h('span', { style: { fontSize: '12px', fontWeight: 550 } }, field.label),
199
- h('small', { style: { fontSize: '11px', lineHeight: 1.6, opacity: .65 } }, field.hint)))
213
+ field.hint ? h('small', { style: { fontSize: '10px', lineHeight: 1.45, opacity: .6 } }, field.hint) : null))
200
214
  }
201
215
  function SettingsFields({ fields, draft, dirtyKeys, conflicts, disabled, editField }) {
202
- return h('div', { style: { display: 'grid', gap: '16px' } }, ...fields.map((field) => {
216
+ const numericGrid = fields.length > 1 && fields.every((field) => field.numeric)
217
+ return h('div', { style: { display: 'grid', gap: '12px', gridTemplateColumns: numericGrid ? 'repeat(auto-fit, minmax(180px, 1fr))' : undefined } }, ...fields.map((field) => {
203
218
  const onChange = (value) => editField(field.key, value)
204
219
  return field.checkbox
205
220
  ? h(SwitchField, { key: field.key, field, checked: draft[field.key], disabled, onChange })
206
221
  : h(TextField, { key: field.key, field, value: draft[field.key], disabled, invalid: dirtyKeys.includes(field.key) && field.numeric && !Number.isSafeInteger(draft[field.key]), onChange })
207
222
  }))
208
223
  }
224
+ function SettingsGroup({ title, fields, fieldProps, open = false }) {
225
+ return h('details', { style: PANEL, 'aria-label': title + '设置', open: open || undefined },
226
+ h('summary', { style: SUMMARY },
227
+ h('span', { style: { fontSize: '12px', fontWeight: 650 } }, title),
228
+ h('small', { style: { fontSize: '10px', opacity: .55 } }, fields.length + ' 项')),
229
+ h('div', { style: { marginTop: '14px', paddingTop: '12px', borderTop: '1px solid rgba(128,128,128,.12)' } },
230
+ h(SettingsFields, { ...fieldProps, fields })))
231
+ }
209
232
 
210
- /** 单行:显示开关 + 默认思考档位 + 显示名 + 官方 ID 映射。 */
233
+ /** 紧凑列表行;完整 ID、输入能力、改名和映射集中在单条详情里。 */
211
234
  function ModelOverrideRow({ row, override, disabled, onEdit }) {
212
235
  const enabled = override.enabled !== false
213
236
  const inputId = 'agy-model-' + row.id.replace(/[^\w.-]/g, '_')
214
237
  const labelStyle = { fontSize: '11px', opacity: .6 }
215
238
  const inputStyle = { ...ACTION, width: '100%', minWidth: 0, boxSizing: 'border-box', background: 'rgba(128,128,128,.035)', cursor: disabled ? 'not-allowed' : 'text' }
216
- return h('div', { style: { display: 'grid', gap: '8px', padding: '12px', borderRadius: '10px', border: '1px solid rgba(128,128,128,.16)', opacity: enabled ? 1 : .62 } },
217
- h('div', { style: { display: 'flex', alignItems: 'flex-start', gap: '10px' } },
218
- h('label', { style: { display: 'flex', alignItems: 'flex-start', gap: '8px', flex: 1, minWidth: 0, cursor: disabled ? 'default' : 'pointer' } },
239
+ const displayName = override.name?.trim() || row.name
240
+ return h('div', { role: 'listitem', style: { padding: '9px 0', borderBottom: '1px solid rgba(128,128,128,.12)', opacity: enabled ? 1 : .58 } },
241
+ h('div', { style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) auto', alignItems: 'center', gap: '8px' } },
242
+ h('label', { title: displayName, style: { display: 'flex', alignItems: 'center', gap: '8px', minWidth: 0, cursor: disabled ? 'default' : 'pointer' } },
219
243
  h('input', { name: 'model-enabled-' + row.id, type: 'checkbox', checked: enabled, disabled,
220
244
  onChange: (event) => onEdit(row.id, { enabled: event.target.checked }),
221
- style: { width: '15px', height: '15px', flex: '0 0 auto', margin: '2px 0 0', accentColor: '#177c55', cursor: disabled ? 'not-allowed' : 'pointer' } }),
222
- h('span', { style: { display: 'grid', gap: '2px', minWidth: 0 } },
223
- h('span', { style: { fontSize: '12px', fontWeight: 600, overflowWrap: 'anywhere' } }, row.name),
224
- h('code', { style: { fontSize: '11px', opacity: .55, overflowWrap: 'anywhere' } }, row.id))),
225
- row.inCatalog
226
- ? (Array.isArray(row.inputModalities) && row.inputModalities.length
227
- ? h('span', { style: { flex: '0 0 auto', fontSize: '10px', padding: '2px 7px', borderRadius: '99px', background: TRACK } }, row.inputModalities.map((modality) => MODALITY_LABELS[modality] ?? modality).join('、'))
228
- : null)
229
- : h('span', { title: '该 ID 来自已保存的设置,当前目录里没有;仍然可以修改或清除', style: { flex: '0 0 auto', fontSize: '10px', padding: '2px 7px', borderRadius: '99px', color: '#946200', background: 'rgba(217,155,40,.12)' } }, '不在当前目录')),
230
- h('div', { style: { display: 'grid', gap: '6px', gridTemplateColumns: 'minmax(120px, 160px) minmax(0, 1fr)' } },
231
- h('div', { style: { display: 'grid', gap: '4px', minWidth: 0 } },
232
- h('label', { htmlFor: inputId + '-effort', style: labelStyle }, '默认思考档位'),
245
+ style: { width: '15px', height: '15px', flex: '0 0 auto', margin: 0, accentColor: '#177c55', cursor: disabled ? 'not-allowed' : 'pointer' } }),
246
+ h('span', { style: { minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: '12px', fontWeight: 600 } }, displayName)),
247
+ h('label', { htmlFor: inputId + '-effort', style: { display: 'flex', alignItems: 'center', gap: '5px', fontSize: '11px' } },
248
+ h('span', { style: { opacity: .65 } }, '思考'),
233
249
  h('select', { id: inputId + '-effort', name: 'model-effort-' + row.id, disabled, value: EFFORT_IDS.has(override.effort) ? override.effort : '',
250
+ 'aria-label': displayName + ' 默认思考档位',
234
251
  onChange: (event) => onEdit(row.id, { effort: event.target.value }),
235
- style: { ...inputStyle, cursor: disabled ? 'not-allowed' : 'pointer' } },
236
- ...EFFORT_OPTIONS.map((option) => h('option', { key: option.value, value: option.value }, option.label)))),
237
- h('div', { style: { display: 'grid', gap: '4px', minWidth: 0 } },
238
- h('label', { htmlFor: inputId + '-target', style: labelStyle }, '映射到官方模型 ID'),
239
- h('input', { id: inputId + '-target', name: 'model-target-' + row.id, type: 'text', disabled, value: override.target ?? '',
240
- placeholder: '留空按家族 + 档位自动路由', onChange: (event) => onEdit(row.id, { target: event.target.value }), style: inputStyle }))),
241
- h('div', { style: { display: 'grid', gap: '4px' } },
242
- h('label', { htmlFor: inputId + '-name', style: labelStyle }, '显示名'),
243
- h('input', { id: inputId + '-name', name: 'model-name-' + row.id, type: 'text', disabled, value: override.name ?? '',
244
- placeholder: row.inCatalog ? '留空使用「' + row.name + '」' : '留空显示模型 ID', onChange: (event) => onEdit(row.id, { name: event.target.value }), style: inputStyle })))
252
+ style: { ...inputStyle, width: 'auto', minHeight: '29px', padding: '3px 7px', cursor: disabled ? 'not-allowed' : 'pointer' } },
253
+ ...EFFORT_OPTIONS.map((option) => h('option', { key: option.value, value: option.value }, option.label))))),
254
+ h('details', { style: { marginLeft: '23px', marginTop: '5px' } },
255
+ h('summary', { style: { ...SUMMARY, justifyContent: 'flex-start', fontSize: '10px', opacity: .6 } }, '模型设置'),
256
+ h('div', { style: { display: 'grid', gap: '10px', marginTop: '10px', padding: '10px', borderRadius: '8px', background: 'rgba(128,128,128,.04)' } },
257
+ h('div', { style: { display: 'grid', gap: '3px' } },
258
+ h('span', { style: labelStyle }, '模型 ID'),
259
+ h('code', { style: { fontSize: '10px', overflowWrap: 'anywhere', opacity: .75 } }, row.id)),
260
+ h('div', { style: { display: 'grid', gap: '10px', gridTemplateColumns: 'repeat(auto-fit, minmax(170px, 1fr))' } },
261
+ h('div', { style: { display: 'grid', gap: '4px', minWidth: 0 } },
262
+ h('label', { htmlFor: inputId + '-name', style: labelStyle }, '显示名'),
263
+ h('input', { id: inputId + '-name', name: 'model-name-' + row.id, type: 'text', disabled, value: override.name ?? '',
264
+ placeholder: row.name, onChange: (event) => onEdit(row.id, { name: event.target.value }), style: inputStyle })),
265
+ h('div', { style: { display: 'grid', gap: '4px', minWidth: 0 } },
266
+ h('label', { htmlFor: inputId + '-target', style: labelStyle }, '官方模型 ID'),
267
+ h('input', { id: inputId + '-target', name: 'model-target-' + row.id, type: 'text', disabled, value: override.target ?? '',
268
+ placeholder: '留空自动路由', onChange: (event) => onEdit(row.id, { target: event.target.value }), style: inputStyle }))),
269
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '4px 12px', fontSize: '10px', opacity: .6 } },
270
+ h('span', null, row.inCatalog ? '当前目录' : '不在当前目录'),
271
+ Array.isArray(row.inputModalities) && row.inputModalities.length
272
+ ? h('span', null, row.inputModalities.map((modality) => MODALITY_LABELS[modality] ?? modality).join(' · ')) : null))))
245
273
  }
246
274
 
247
- /**
248
- * 模型列表:一次列出目录中的每个模型,改动在保存时合并成一份
249
- * `modelOverrides` 提交,空项自动清除,不留空壳配置。
250
- */
251
275
  function ModelOverrideList({ rows, overrides, disabled, onEdit }) {
276
+ const [query, setQuery] = React.useState('')
277
+ const [filter, setFilter] = React.useState('all')
252
278
  if (rows.length === 0) {
253
- return h('p', { style: { margin: 0, fontSize: '12px', lineHeight: 1.6, opacity: .6 } },
254
- '还没有模型目录。连接 / 登录 ACP 或刷新模型目录后,可在此逐项设置显示开关、默认思考档位与映射。')
279
+ return h('p', { role: 'status', style: { margin: 0, fontSize: '12px', lineHeight: 1.5, opacity: .6 } }, '暂无模型。请先连接当前账户,再刷新目录。')
255
280
  }
256
281
  const hidden = rows.filter((row) => overrideFor(overrides, row.id).enabled === false).length
257
- return h('div', { style: { display: 'grid', gap: '10px' } },
258
- h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '4px 12px', fontSize: '11px', opacity: .62 } },
259
- h('span', null, rows.length + ' 个模型'),
260
- hidden ? h('span', null, hidden + ' 个已从选择器隐藏') : null,
261
- h('span', null, '映射与显示名保存后生效;当前会话已选模型不变')),
262
- // 全部停用时本 provider 会从 DSH 选择器里消失,这里必须说清原因。
282
+ const keyword = query.trim().toLowerCase()
283
+ const visible = rows.filter((row) => {
284
+ const override = overrideFor(overrides, row.id)
285
+ if (filter === 'enabled' && override.enabled === false) return false
286
+ if (filter === 'hidden' && override.enabled !== false) return false
287
+ return !keyword || (row.name + ' ' + row.id + ' ' + (override.name ?? '')).toLowerCase().includes(keyword)
288
+ })
289
+ const setVisible = (enabled) => { for (const row of visible) onEdit(row.id, { enabled }) }
290
+ return h('div', { style: { display: 'grid', gap: '8px' } },
291
+ h('div', { style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) auto', gap: '8px' } },
292
+ h('input', { type: 'search', value: query, onChange: (event) => setQuery(event.target.value), placeholder: '搜索模型', 'aria-label': '搜索模型',
293
+ style: { ...ACTION, width: '100%', minWidth: 0, boxSizing: 'border-box', cursor: 'text' } }),
294
+ h('select', { value: filter, onChange: (event) => setFilter(event.target.value), 'aria-label': '筛选模型', style: ACTION },
295
+ h('option', { value: 'all' }, '全部 ' + rows.length),
296
+ h('option', { value: 'enabled' }, '已启用 ' + (rows.length - hidden)),
297
+ h('option', { value: 'hidden' }, '已隐藏 ' + hidden))),
298
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '10px', opacity: .7 } },
299
+ h('span', { style: { marginRight: 'auto' } }, '显示 ' + visible.length + ' / ' + rows.length),
300
+ h('button', { type: 'button', disabled: disabled || visible.length === 0, onClick: () => setVisible(true), style: { ...ACTION, minHeight: '26px', padding: '2px 7px', fontSize: '10px' } }, '全部启用'),
301
+ h('button', { type: 'button', disabled: disabled || visible.length === 0, onClick: () => setVisible(false), style: { ...ACTION, minHeight: '26px', padding: '2px 7px', fontSize: '10px' } }, '全部隐藏')),
263
302
  hidden === rows.length
264
- ? h('p', { role: 'status', style: { margin: 0, fontSize: '11px', lineHeight: 1.6, color: '#946200' } },
265
- '所有模型都已隐藏:保存后 Antigravity 将不出现在 DSH 模型选择器中,重新启用任一项即可恢复。')
303
+ ? h('p', { role: 'status', style: { margin: 0, fontSize: '11px', lineHeight: 1.5, color: '#946200' } }, '全部隐藏后,Antigravity 将从模型选择器消失。')
266
304
  : null,
267
- ...rows.map((row) => h(ModelOverrideRow, {
268
- key: row.id, row, override: overrideFor(overrides, row.id), disabled,
269
- onEdit: (id, patch) => onEdit(id, patch),
270
- })))
305
+ visible.length
306
+ ? h('div', { role: 'list', style: { display: 'grid' } }, ...visible.map((row) => h(ModelOverrideRow, {
307
+ key: row.id, row, override: overrideFor(overrides, row.id), disabled, onEdit,
308
+ })))
309
+ : h('p', { role: 'status', style: { margin: 0, fontSize: '12px', opacity: .6 } }, '没有符合条件的模型。'))
310
+ }
311
+
312
+ /**
313
+ * 代理账户面板:代理模式的独立登录态(与 ACP / 官方 CLI 登录相互独立)。
314
+ * 状态与授权 URL 经 Host 的 remote.agyProxyAccount 读取;授权 URL 只在
315
+ * 点击「连接代理账户」后返回一次,展示为可打开的链接。
316
+ */
317
+ function ProxyAccountPanel({ control, disabled, connectDisabled, onAccountChanged }) {
318
+ const [snapshot, setSnapshot] = React.useState(undefined)
319
+ const [authUrl, setAuthUrl] = React.useState(undefined)
320
+ const [message, setMessage] = React.useState('')
321
+ const [busy, setBusy] = React.useState(false)
322
+ const alive = React.useRef(true)
323
+ const version = React.useRef(0)
324
+ const polling = React.useRef(false)
325
+ const actionVersion = React.useRef(0)
326
+ const status = React.useCallback(async () => {
327
+ if (!control || polling.current) return
328
+ polling.current = true
329
+ const current = ++version.current
330
+ try {
331
+ const value = unwrap(await control('account', { action: 'status' }))
332
+ if (alive.current && current === version.current) setSnapshot(value.snapshot)
333
+ } catch (error) {
334
+ if (alive.current && current === version.current) setSnapshot({ phase: 'signed-out', message: error instanceof Error ? error.message : String(error), updatedAt: new Date().toISOString() })
335
+ } finally { polling.current = false }
336
+ }, [control])
337
+ React.useEffect(() => {
338
+ alive.current = true
339
+ void status()
340
+ const timer = setInterval(() => {
341
+ if (typeof document === 'undefined' || document.visibilityState !== 'hidden') void status()
342
+ }, 3000)
343
+ return () => { alive.current = false; ++version.current; clearInterval(timer) }
344
+ }, [status])
345
+ React.useEffect(() => {
346
+ if (!snapshot) return
347
+ setMessage('')
348
+ if (snapshot.phase !== 'pending-login') setAuthUrl(undefined)
349
+ onAccountChanged?.(snapshot)
350
+ }, [snapshot?.phase, snapshot?.connectionStage, snapshot?.message, snapshot?.revision, onAccountChanged])
351
+ const act = async (action) => {
352
+ if (!control || (busy && action === 'login')) return
353
+ const operation = ++actionVersion.current
354
+ ++version.current
355
+ setBusy(true)
356
+ setMessage(action === 'login' ? '正在准备授权…' : '')
357
+ try {
358
+ const value = unwrap(await control('account', { action }))
359
+ if (!alive.current || operation !== actionVersion.current) return
360
+ setSnapshot(value.snapshot)
361
+ setMessage(value.message ?? '')
362
+ if (value.authorizationUrl) setAuthUrl(value.authorizationUrl)
363
+ else if (action !== 'login') setAuthUrl(undefined)
364
+ } catch (error) {
365
+ if (alive.current && operation === actionVersion.current) setMessage(error instanceof Error ? error.message : String(error))
366
+ } finally { if (alive.current && operation === actionVersion.current) setBusy(false) }
367
+ }
368
+ const phase = snapshot?.phase
369
+ const stage = snapshot?.connectionStage
370
+ const connecting = busy || stage === 'preparing' || stage === 'finishing' || phase === 'pending-login'
371
+ const dotColor = phase === 'signed-in' ? '#25b879' : connecting ? '#d99b28' : phase === 'login-failed' ? '#e5534b' : '#808080'
372
+ const phaseLabel = phase === 'signed-in' ? '代理账户已连接' : stage === 'finishing' ? '正在完成登录…' : stage === 'preparing' ? '正在检查登录态…' : phase === 'pending-login' ? '等待浏览器授权…' : phase === 'login-failed' ? '登录失败' : busy ? '正在连接…' : '未连接'
373
+ return h('div', { style: { display: 'grid', gap: '10px', padding: '12px', borderRadius: '10px', background: 'rgba(128,128,128,.045)' } },
374
+ h('div', { style: { display: 'flex', alignItems: 'flex-start', gap: '10px' } },
375
+ h(StatusDot, { color: dotColor }),
376
+ h('div', { style: { minWidth: 0 } },
377
+ h('div', { style: { fontSize: '12px', fontWeight: 650 } }, phaseLabel + (snapshot?.email ? `(${snapshot.email})` : '') + (snapshot?.source === 'cli-import' ? ' · 导入自官方 CLI' : '')),
378
+ h('p', { role: 'status', style: { fontSize: '12px', lineHeight: 1.6, opacity: .7, margin: '3px 0 0', overflowWrap: 'anywhere' } }, message || snapshot?.message || ''))),
379
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '8px' } },
380
+ h('button', { type: 'button', style: phase === 'signed-in' ? ACTION : PRIMARY, disabled: disabled || connectDisabled || connecting || phase === 'signed-in', onClick: () => act('login') }, connecting ? '连接中…' : '连接代理账户'),
381
+ connecting ? h('button', { type: 'button', style: ACTION, disabled, onClick: () => act('cancel') }, '取消登录') : null,
382
+ phase === 'signed-in' ? h('button', { type: 'button', style: ACTION, disabled: disabled || busy, onClick: () => act('logout') }, '退出登录') : null),
383
+ authUrl && stage !== 'finishing' ? h('div', { style: { display: 'grid', gap: '6px', justifyItems: 'start' } },
384
+ h('a', { href: authUrl, target: '_blank', rel: 'noopener noreferrer', style: { ...PRIMARY, textDecoration: 'none', display: 'inline-flex', alignItems: 'center' } }, '打开 Google 授权页'),
385
+ h('small', { style: { fontSize: '10px', opacity: .65, lineHeight: 1.5 } }, '请在运行 DSH 的设备上完成授权(5 分钟内有效)。')) : null,
386
+ phase === 'pending-login' && stage !== 'finishing' && !authUrl && !busy ? h('p', { role: 'status', style: { fontSize: '11px', margin: 0, opacity: .7 } }, '授权页已打开;找不到时请取消后重试。') : null,
387
+ h('p', { style: { opacity: .55, fontSize: '10px', lineHeight: 1.5, margin: 0 } }, '生成与套餐额度使用此账户 · 无需 agy CLI · ACP 已停用'))
271
388
  }
272
389
 
273
390
  function AntigravitySettingsCard(props) {
@@ -291,7 +408,7 @@ window.__ModuleLoader__.load({
291
408
  const value = unwrap(await props.control('models', force ? { force: true } : undefined))
292
409
  if (!alive.current || version !== modelVersion.current) return
293
410
  setModels(value.models ?? [])
294
- setModelMessage(value.status === 'error' ? value.message : (value.models?.length ? '目录已更新;不会自动替换当前会话的已选模型。' : '暂无模型目录。连接 / 登录 ACP 可验证会话并读取目录;也可从已登录的官方 CLI 拉取。'))
411
+ setModelMessage(value.status === 'error' ? value.message : '')
295
412
  } catch (error) { if (alive.current && version === modelVersion.current) setModelMessage(error instanceof Error ? error.message : String(error)) }
296
413
  finally {
297
414
  if (force) manualModelPull.current = false
@@ -333,6 +450,9 @@ window.__ModuleLoader__.load({
333
450
  const [saving, setSaving] = React.useState(false)
334
451
  const [dirtyKeys, setDirtyKeys] = React.useState([])
335
452
  const [conflicts, setConflicts] = React.useState([])
453
+ const [savedProxyMode, setSavedProxyMode] = React.useState(false)
454
+ // 设置分页。非活动页只隐藏、不卸载:草稿、保存与控件覆盖测试都依赖字段始终挂载。
455
+ const [settingsPage, setSettingsPage] = React.useState('connection')
336
456
  const settingsVersion = React.useRef(0)
337
457
  const savingRef = React.useRef(false)
338
458
  const refreshAfterSave = React.useRef(false)
@@ -340,6 +460,29 @@ window.__ModuleLoader__.load({
340
460
  const dirty = React.useRef(new Set())
341
461
  const draftRef = React.useRef(draft)
342
462
  draftRef.current = draft
463
+ const effectiveProxyMode = account?.generationMode ? account.generationMode.requested === 'proxy' : savedProxyMode
464
+ const activeModeRef = React.useRef(effectiveProxyMode)
465
+ activeModeRef.current = effectiveProxyMode
466
+ React.useEffect(() => { props.readQuota?.observeMode?.(effectiveProxyMode ? PROXY_PROVIDER : PROVIDER) }, [effectiveProxyMode, props.readQuota])
467
+ const proxyUnavailable = savedProxyMode && account?.generationMode?.proxyRouterReady === false
468
+ const previousProxyAccount = React.useRef(undefined)
469
+ const onProxyAccountChanged = React.useCallback((snapshot) => {
470
+ const previous = previousProxyAccount.current
471
+ previousProxyAccount.current = snapshot
472
+ const identityChanged = previous?.revision !== snapshot.revision
473
+ const connected = snapshot.phase === 'signed-in' && previous?.phase !== 'signed-in'
474
+ const signedOut = snapshot.phase === 'signed-out' && previous?.phase === 'signed-in'
475
+ props.readQuota?.observeProxyAccount?.(snapshot)
476
+ if (savedProxyMode && (connected || signedOut || (identityChanged && !snapshot.connectionStage))) void pullModels(true)
477
+ void accountAction('status')
478
+ }, [savedProxyMode, pullModels, accountAction, props.readQuota])
479
+ React.useEffect(() => {
480
+ let current = true
481
+ Promise.resolve().then(() => remote?.llm?.listProviders?.()).then(unwrap).then((providers) => {
482
+ if (current && alive.current && Array.isArray(providers)) setRegistered(providers.some((row) => row?.id === (effectiveProxyMode ? PROXY_PROVIDER : PROVIDER)))
483
+ }).catch(() => { if (current && alive.current) setRegistered(false) })
484
+ return () => { current = false }
485
+ }, [effectiveProxyMode, remote])
343
486
 
344
487
  const editField = React.useCallback((key, value) => {
345
488
  if (savingRef.current) return
@@ -397,7 +540,12 @@ window.__ModuleLoader__.load({
397
540
  const found = namespaces.find((item) => item?.ns === ENTRY_ID)
398
541
  setView(found ?? null)
399
542
  const current = found?.value && typeof found.value === 'object' ? found.value : {}
400
- const server = Object.fromEntries(FIELDS.map((field) => [field.key, field.checkbox ? (field.defaultChecked ? current[field.key] !== false : current[field.key] === true) : current[field.key] ?? field.defaultValue ?? '']))
543
+ const activeProvider = activeModeRef.current ? PROXY_PROVIDER : PROVIDER
544
+ setSavedProxyMode(current.proxyMode === true)
545
+ const server = Object.fromEntries(FIELDS.map((field) => [field.key, field.checkbox
546
+ ? (field.defaultChecked ? current[field.key] !== false : current[field.key] === true)
547
+ : field.lines ? (Array.isArray(current[field.key]) ? current[field.key].join('\n') : current[field.key] ?? '')
548
+ : current[field.key] ?? field.defaultValue ?? '']))
401
549
  // 整份 map 作为一个字段参与脏值比较;规范化后再比,避免键序或
402
550
  // 「显式写回默认值」被误判为未保存修改。
403
551
  server[OVERRIDES_KEY] = canonicalOverrides(current[OVERRIDES_KEY])
@@ -422,7 +570,7 @@ window.__ModuleLoader__.load({
422
570
  setDirtyKeys([...dirty.current])
423
571
  setConflicts((previous) => [...new Set([...previous.filter((key) => dirty.current.has(key)), ...changed])])
424
572
  setWritable(settings?.writable === true)
425
- setRegistered(rows.some((row) => row?.id === PROVIDER))
573
+ setRegistered(rows.some((row) => row?.id === activeProvider))
426
574
  if (!found) setStatus({ kind: 'error', text: '宿主尚未提供 Antigravity ACP 的可编辑设置。请确认 Host 与 Client 插件版本一致,并重新加载插件后刷新配置。' })
427
575
  else if (!preserveStatus) setStatus(null)
428
576
  } catch (cause) {
@@ -435,9 +583,9 @@ window.__ModuleLoader__.load({
435
583
  const save = React.useCallback(async () => {
436
584
  if (!view || !writable || loading || savingRef.current || dirty.current.size === 0) return
437
585
  const invalid = FIELDS.find((field) => field.numeric && dirty.current.has(field.key)
438
- && (!Number.isSafeInteger(draftRef.current[field.key]) || draftRef.current[field.key] < field.min || draftRef.current[field.key] > field.max))
586
+ && (!Number.isSafeInteger(draftRef.current[field.key]) || draftRef.current[field.key] < (field.min ?? 0) || (field.max !== undefined && draftRef.current[field.key] > field.max)))
439
587
  if (invalid) {
440
- setStatus({ kind: 'error', text: invalid.label + '必须是 ' + invalid.min + '–' + invalid.max + ' 的整数。' })
588
+ setStatus({ kind: 'error', text: invalid.label + '必须是' + (invalid.max === undefined ? '不小于 ' + (invalid.min ?? 0) : ' ' + invalid.min + '–' + invalid.max) + ' 的整数。' })
441
589
  return
442
590
  }
443
591
  savingRef.current = true
@@ -447,18 +595,28 @@ window.__ModuleLoader__.load({
447
595
  // modelOverrides 是整份 map 一个字段,按同样方式提交。
448
596
  const ops = [...FIELDS.map((field) => field.key), OVERRIDES_KEY]
449
597
  .filter((key) => dirty.current.has(key))
450
- .map((key) => ({ op: 'set', path: [key], value: draftRef.current[key] }))
598
+ .map((key) => {
599
+ const field = FIELDS.find((entry) => entry.key === key)
600
+ const value = field?.lines ? String(draftRef.current[key] ?? '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : draftRef.current[key]
601
+ return { op: 'set', path: [key], value }
602
+ })
451
603
  try {
452
604
  unwrap(await remote.settings.mutate(ENTRY_ID, ops, view.revision))
453
605
  if (!alive.current) return
454
606
  for (const operation of ops) {
455
607
  const key = operation.path[0]
456
- if (draftRef.current[key] === operation.value) dirty.current.delete(key)
608
+ const field = FIELDS.find((entry) => entry.key === key)
609
+ const currentValue = field?.lines ? String(draftRef.current[key] ?? '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : draftRef.current[key]
610
+ if (JSON.stringify(currentValue) === JSON.stringify(operation.value)) dirty.current.delete(key)
457
611
  }
458
612
  setDirtyKeys([...dirty.current])
459
613
  setConflicts([])
460
- setStatus({ kind: 'ok', text: '已保存。会话偏好从下一轮生效;代理用于后续请求及新启动的 ACP 进程,已有进程需重新加载插件才能切换代理。自动预热在下次启用插件时生效;当前已选模型保持不变。' })
614
+ setStatus({ kind: 'ok', text: ops.some((operation) => operation.path[0] === 'proxyMode')
615
+ ? '已保存并切换模式,请在会话中选择对应模型。'
616
+ : '已保存;进行中的会话不会被中断。' })
461
617
  await reload(true)
618
+ void accountAction('status')
619
+ void pullModels(true)
462
620
  } catch (cause) {
463
621
  if (!alive.current) return
464
622
  setStatus({ kind: 'error', text: cause instanceof Error ? cause.message : String(cause) })
@@ -471,7 +629,7 @@ window.__ModuleLoader__.load({
471
629
  if (refreshAfterSave.current) { refreshAfterSave.current = false; void reload(true) }
472
630
  }
473
631
  }
474
- }, [loading, writable, remote, reload, view])
632
+ }, [loading, writable, remote, reload, view, accountAction, pullModels])
475
633
 
476
634
  React.useEffect(() => {
477
635
  reload()
@@ -490,6 +648,7 @@ window.__ModuleLoader__.load({
490
648
  // 目录条目 + 已配置但目录里暂时没有的 ID:正在编辑的映射不能因为目录刷新而消失。
491
649
  const rows = modelRows(models, draft[OVERRIDES_KEY])
492
650
  const editingDisabled = loading || saving || !view || !writable
651
+ const connectionSettingsDirty = dirtyKeys.some(key => key === 'proxyUrl' || key === 'proxyBypass')
493
652
  const discardDraft = () => {
494
653
  if (savingRef.current) return
495
654
  dirty.current.clear()
@@ -497,79 +656,94 @@ window.__ModuleLoader__.load({
497
656
  setDraft(draftRef.current)
498
657
  setDirtyKeys([])
499
658
  setConflicts([])
500
- setStatus({ kind: 'ok', text: '已撤销未保存的修改,恢复为当前宿主设置。' })
659
+ setStatus({ kind: 'ok', text: '已撤销未保存修改。' })
501
660
  }
502
661
 
503
662
  const connection = connectionStatus(account, registered)
504
- const fieldsFor = (group) => FIELDS.filter((field) => field.group === group)
663
+ const fieldsFor = (group) => FIELDS.filter((field) => field.group === group && (!effectiveProxyMode || !field.acpOnly))
505
664
  const fieldProps = { draft, dirtyKeys, conflicts, disabled: editingDisabled, editField }
665
+ const pageStyle = (id) => ({ display: settingsPage === id ? 'flex' : 'none', flexDirection: 'column', gap: '14px' })
666
+ const pageButton = (id, label) => h('button', {
667
+ type: 'button', role: 'tab', 'aria-selected': settingsPage === id, 'aria-controls': 'agy-settings-page-' + id,
668
+ onClick: () => setSettingsPage(id),
669
+ style: { ...ACTION, minHeight: '32px', padding: '5px 12px', fontWeight: settingsPage === id ? 650 : 450, borderColor: settingsPage === id ? '#177c55' : 'rgba(128,128,128,.22)', background: settingsPage === id ? 'rgba(23,124,85,.12)' : 'transparent' },
670
+ }, label)
506
671
  const busy = ['connecting', 'cancelling'].includes(account?.state) || runtimeBusy(account?.runtime)
507
672
  const readonly = !loading && view && !writable
508
- const advancedSummary = h('summary', { style: SUMMARY },
509
- h('span', { style: { display: 'grid', gap: '4px' } },
510
- h('span', { style: { fontSize: '13px', fontWeight: 650 } }, '高级设置'),
511
- h('small', { style: { fontSize: '11px', fontWeight: 400, opacity: .6 } }, '网络、CLI、会话恢复与进程生命周期')))
512
-
513
- return h('div', { style: { padding: '20px', display: 'flex', flexDirection: 'column', gap: '14px', maxWidth: '720px', boxSizing: 'border-box', lineHeight: 1.5 } },
514
- h('header', { style: { display: 'flex', alignItems: 'flex-start', gap: '12px', padding: '0 2px 2px' } },
515
- h('div', { style: { flex: 1, minWidth: 0 } },
516
- h('div', { style: { fontSize: '10px', fontWeight: 650, letterSpacing: '.1em', opacity: .5, marginBottom: '5px' } }, 'MODEL PROVIDER'),
517
- h('h2', { style: { margin: 0, fontSize: '20px', lineHeight: 1.35, fontWeight: 650, letterSpacing: '-.015em' } }, 'Antigravity ACP'),
518
- h('p', { style: { margin: '6px 0 0', fontSize: '12px', opacity: .6 } }, '连接官方 Antigravity,模型与工具仍由 DSH 管理。')),
519
- h('span', { title: 'Provider 路由:' + PROVIDER, style: { flex: '0 0 auto', padding: '4px 9px', borderRadius: '99px', fontSize: '11px', color: registered === false ? '#b42318' : registered === true ? '#177c55' : 'inherit', background: registered === false ? 'rgba(217,119,87,.10)' : registered === true ? 'rgba(37,184,121,.10)' : TRACK } }, registered === false ? '未注册' : registered === true ? 'Provider 已注册' : '正在检查路由')),
520
-
521
- readonly ? h(StatusNotice, { status: { kind: 'busy', text: '当前设置为只读。你仍可查看连接状态与诊断;请在有写入权限的宿主中修改配置。' } }) : null,
522
-
523
- h('section', { style: PANEL, 'aria-label': '账户连接' },
524
- h(SectionHeading, { eyebrow: '01 · 连接', title: '账户与连接', description: '先预热减少等待,或验证账户并读取模型目录。' }),
525
- h('div', { style: { display: 'flex', alignItems: 'flex-start', gap: '10px', padding: '12px', borderRadius: '10px', background: 'rgba(128,128,128,.045)', marginBottom: '14px' } },
526
- h(StatusDot, { color: connection.color }),
527
- h('div', { style: { minWidth: 0 } },
528
- h('div', { style: { fontSize: '12px', fontWeight: 650, color: connection.color } }, connection.label),
529
- h('p', { role: 'status', style: { fontSize: '12px', lineHeight: 1.6, opacity: .7, overflowWrap: 'anywhere', margin: '3px 0 0' } }, connection.description))),
530
- h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '8px' } },
531
- h('button', { type: 'button', style: PRIMARY, disabled: !props.control || busy, onClick: () => accountAction('login') }, account?.state === 'connecting' ? '正在连接…' : '连接 / 登录 ACP'),
532
- h('button', { type: 'button', style: ACTION, disabled: !props.control || busy || connection.ready, onClick: () => accountAction('warmup') }, connection.ready ? '已预热' : '立即预热'),
533
- ['connecting', 'cancelling'].includes(account?.state) ? h('button', { type: 'button', style: ACTION, disabled: account?.state === 'cancelling', onClick: () => accountAction('cancel') }, account?.state === 'cancelling' ? '正在取消…' : '取消连接') : null),
534
- h('p', { style: { opacity: .55, fontSize: '11px', lineHeight: 1.6, margin: '10px 0 0' } }, '预热不会发送聊天消息。仅在需要登录时打开官方授权页面;思考强度仍由会话菜单设置。')),
535
-
536
- h('section', { style: PANEL, 'aria-label': '模型列表' },
537
- h(SectionHeading, { eyebrow: '02 · 模型', title: '模型列表', description: '目录中的模型是否出现在 DSH 选择器,以及各自的默认思考档位与官方 ID 映射。',
538
- action: h('button', { type: 'button', title: '从官方 CLI 或 ACP 会话重新读取模型目录', 'aria-busy': modelBusy || undefined, style: { ...ACTION, flex: '0 0 auto', whiteSpace: 'nowrap' }, disabled: modelBusy || !props.control, onClick: () => pullModels(true) }, modelBusy ? '正在刷新…' : '刷新模型目录') }),
539
- h(ModelOverrideList, { rows: rows, overrides: draft[OVERRIDES_KEY], disabled: editingDisabled, onEdit: editOverride }),
540
- modelMessage ? h('p', { role: 'status', style: { fontSize: '11px', lineHeight: 1.6, opacity: .65, margin: '10px 0 0', overflowWrap: 'anywhere' } }, modelMessage) : null),
541
-
542
- h('section', { style: PANEL, 'aria-label': '常用设置' },
543
- h(SectionHeading, { eyebrow: '03 · 偏好', title: '常用设置', description: '适合大多数使用场景;其它选项保持默认即可。' }),
544
- h(SettingsFields, { ...fieldProps, fields: fieldsFor('general') })),
545
-
546
- h('details', { style: PANEL, 'aria-label': '高级设置' }, advancedSummary,
547
- h('div', { style: { display: 'grid', gap: '24px', marginTop: '20px', paddingTop: '18px', borderTop: '1px solid rgba(128,128,128,.14)' } },
548
- h('section', { 'aria-label': '网络与 CLI 设置', style: { display: 'grid', gap: '16px' } },
549
- h('h4', { style: { margin: 0, fontSize: '12px', fontWeight: 650 } }, '网络与 CLI'),
550
- h(SettingsFields, { ...fieldProps, fields: fieldsFor('network') })),
551
- h('section', { 'aria-label': '会话与进程设置', style: { display: 'grid', gap: '16px' } },
552
- h('h4', { style: { margin: 0, fontSize: '12px', fontWeight: 650 } }, '会话与进程'),
553
- h(SettingsFields, { ...fieldProps, fields: fieldsFor('advanced') })))),
554
-
555
- h('details', { style: PANEL, 'aria-label': '连接诊断' },
556
- h('summary', { style: SUMMARY },
557
- h('span', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
558
- h(StatusDot, { color: connection.color }),
559
- h('span', { style: { fontSize: '13px', fontWeight: 650 } }, '连接诊断')),
560
- h('small', { style: { fontSize: '11px', opacity: .6 } }, '状态、并发与耗时')),
561
- h('div', { style: { marginTop: '18px', paddingTop: '16px', borderTop: '1px solid rgba(128,128,128,.14)' } },
562
- account?.runtime ? RuntimeDetails({ runtime: account.runtime }) : h('p', { style: { margin: 0, fontSize: '12px', opacity: .6 } }, '连接后显示生命周期、并发容量与分阶段耗时。'))),
563
-
564
- h('footer', { style: { display: 'grid', gap: '10px', padding: '4px 2px 0' } },
565
- conflicts.length ? h('p', { role: 'alert', style: { margin: 0, fontSize: '12px', lineHeight: 1.6, color: '#946200' } }, '其他位置更新了以下设置,已保留你的输入:' + conflicts.map((key) => key === OVERRIDES_KEY ? OVERRIDES_LABEL : FIELDS.find((field) => field.key === key)?.label ?? key).join('、') + '。请核对后再保存。') : null,
566
- h(StatusNotice, { status }),
567
- h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center' } },
568
- h('button', { type: 'button', onClick: save, disabled: editingDisabled || dirtyKeys.length === 0, style: PRIMARY }, saving ? '保存中…' : '保存'),
569
- dirtyKeys.length ? h('button', { type: 'button', disabled: loading || saving, onClick: discardDraft, style: ACTION }, '撤销未保存修改') : null,
570
- h('button', { type: 'button', title: '重新读取宿主配置,保留未保存的输入', disabled: loading || saving, onClick: () => reload(), style: { ...ACTION, borderColor: 'transparent', background: 'transparent', opacity: .7, marginLeft: 'auto' } }, loading ? '刷新中…' : '刷新配置')),
571
- dirtyKeys.length ? h('p', { role: 'status', style: { margin: 0, fontSize: '11px', opacity: .65 } }, '有 ' + dirtyKeys.length + ' 项未保存的修改;刷新会保留这些输入。') : null,
572
- h('p', { style: { margin: '2px 0 0', paddingTop: '12px', borderTop: '1px solid rgba(128,128,128,.12)', fontSize: '11px', lineHeight: 1.6, opacity: .55 } }, '套餐额度与 AI Credits 请在对话输入框旁的额度药丸中查看,此处不再重复展示。')))
673
+ const saveVisible = dirtyKeys.length > 0 || loading || readonly || conflicts.length > 0 || Boolean(status)
674
+
675
+ return h('div', { className: 'antigravity-settings-card', style: { padding: '12px', display: 'flex', flexDirection: 'column', gap: '10px', maxWidth: '760px', boxSizing: 'border-box', lineHeight: 1.5 } },
676
+ h('div', { role: 'region', 'aria-label': '设置保存操作', style: { position: 'sticky', top: 0, zIndex: 20, flexShrink: 0, background: 'var(--dsh-surface, Canvas)', color: 'var(--dsh-text, CanvasText)', padding: '4px 0', display: saveVisible ? 'grid' : 'none', gap: '6px' } },
677
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '7px', alignItems: 'center' } },
678
+ h('button', { type: 'button', onClick: save, disabled: editingDisabled || dirtyKeys.length === 0, style: { ...PRIMARY, minHeight: '30px', padding: '4px 10px' } }, saving ? '保存中…' : '保存'),
679
+ dirtyKeys.length ? h('button', { type: 'button', disabled: loading || saving, onClick: discardDraft, style: { ...ACTION, minHeight: '30px', padding: '4px 9px' } }, '撤销') : null,
680
+ h('span', { role: 'status', 'aria-live': 'polite', style: { fontSize: '11px', opacity: .7 } }, dirtyKeys.length ? dirtyKeys.length + ' 项未保存' : loading ? '正在读取…' : readonly ? '只读设置' : ''),
681
+ h('button', { type: 'button', title: '重新读取配置并保留草稿', disabled: loading || saving, onClick: () => reload(), style: { ...ACTION, minHeight: '30px', padding: '4px 8px', borderColor: 'transparent', background: 'transparent', opacity: .7, marginLeft: 'auto' } }, loading ? '刷新中…' : '刷新')),
682
+ conflicts.length ? h('p', { role: 'alert', style: { margin: 0, fontSize: '12px', lineHeight: 1.5 } }, '以下内容已在其他位置更新,已保留你的输入:' + conflicts.map((key) => key === OVERRIDES_KEY ? OVERRIDES_LABEL : FIELDS.find((field) => field.key === key)?.label ?? key).join('、')) : null,
683
+ h(StatusNotice, { status })),
684
+
685
+ connectionSettingsDirty ? h(StatusNotice, { status: { kind: 'busy', text: '请先保存网络设置,再连接或登录。' } }) : null,
686
+ proxyUnavailable ? h(StatusNotice, { status: { kind: 'error', text: '代理路由未就绪,ACP 保持停用。请启用 Antigravity Proxy Router 组件。' } }) : null,
687
+ h('div', { role: 'tablist', 'aria-label': '设置分页', style: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '6px' } },
688
+ pageButton('connection', '连接'),
689
+ pageButton('models', '模型'),
690
+ pageButton('quota', '额度'),
691
+ pageButton('advanced', '高级'),
692
+ h('span', { style: { marginLeft: 'auto', padding: '3px 7px', borderRadius: '99px', fontSize: '10px', color: registered === false ? '#b42318' : effectiveProxyMode ? '#946200' : 'inherit', background: effectiveProxyMode ? 'rgba(217,159,4,.12)' : TRACK } }, registered === false ? '路由未注册' : effectiveProxyMode ? '代理模式' : 'ACP 模式')),
693
+
694
+ h('div', { id: 'agy-settings-page-connection', role: 'tabpanel', style: pageStyle('connection') },
695
+ h('section', { style: PANEL, 'aria-label': '运行模式' },
696
+ h(SettingsFields, { ...fieldProps, fields: fieldsFor('mode') }),
697
+ dirtyKeys.includes('proxyMode') ? h('p', { role: 'status', style: { margin: '8px 0 0', fontSize: '11px', color: '#946200' } }, '保存后切换到' + (draft.proxyMode ? '代理模式' : 'ACP 模式')) : null),
698
+ effectiveProxyMode
699
+ ? h('section', { style: PANEL, 'aria-label': '代理账户' },
700
+ h(SectionHeading, { title: '代理账户' }),
701
+ h(ProxyAccountPanel, { control: props.proxyControl, onAccountChanged: onProxyAccountChanged, disabled: !props.proxyControl || !view || !writable, connectDisabled: editingDisabled || connectionSettingsDirty }))
702
+ : h('section', { style: PANEL, 'aria-label': '账户连接' },
703
+ h(SectionHeading, { title: 'ACP 账户与连接' }),
704
+ h('div', { style: { display: 'flex', alignItems: 'flex-start', gap: '10px', marginBottom: '12px' } },
705
+ h(StatusDot, { color: connection.color }),
706
+ h('div', { style: { minWidth: 0 } },
707
+ h('div', { style: { fontSize: '12px', fontWeight: 650, color: connection.color } }, connection.label),
708
+ h('p', { role: 'status', style: { fontSize: '11px', lineHeight: 1.5, opacity: .7, overflowWrap: 'anywhere', margin: '3px 0 0' } }, connection.description))),
709
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '8px' } },
710
+ h('button', { type: 'button', style: PRIMARY, disabled: !props.control || busy || connectionSettingsDirty, onClick: () => accountAction('login') }, account?.state === 'connecting' ? '正在连接…' : '连接 / 登录 ACP'),
711
+ h('button', { type: 'button', style: ACTION, disabled: !props.control || busy || connection.ready || connectionSettingsDirty, onClick: () => accountAction('warmup') }, connection.ready ? '已预热' : '立即预热'),
712
+ ['connecting', 'cancelling'].includes(account?.state) ? h('button', { type: 'button', style: ACTION, disabled: account?.state === 'cancelling', onClick: () => accountAction('cancel') }, account?.state === 'cancelling' ? '正在取消…' : '取消连接') : null))),
713
+
714
+ h('div', { id: 'agy-settings-page-models', role: 'tabpanel', style: pageStyle('models') },
715
+ h('section', { style: PANEL, 'aria-label': '模型列表' },
716
+ h(SectionHeading, { title: '模型',
717
+ action: h('button', { type: 'button', title: '刷新模型目录', 'aria-label': '刷新模型目录', 'aria-busy': modelBusy || undefined, style: { ...ACTION, flex: '0 0 auto', whiteSpace: 'nowrap' }, disabled: modelBusy || !props.control, onClick: () => pullModels(true) }, modelBusy ? '正在刷新…' : '刷新') }),
718
+ h(ModelOverrideList, { rows, overrides: draft[OVERRIDES_KEY], disabled: editingDisabled, onEdit: editOverride }),
719
+ modelMessage ? h('p', { role: 'status', style: { fontSize: '11px', lineHeight: 1.5, opacity: .65, margin: 0, overflowWrap: 'anywhere' } }, modelMessage) : null)),
720
+
721
+ h('div', { id: 'agy-settings-page-quota', role: 'tabpanel', style: pageStyle('quota') },
722
+ h(SettingsQuotaPanel, { readQuota: props.readQuota, source: effectiveProxyMode ? PROXY_PROVIDER : PROVIDER,
723
+ accountRevision: effectiveProxyMode ? previousProxyAccount.current?.revision : undefined,
724
+ cliQuotaOn: (dirtyKeys.includes('cliQuotaEnabled') ? baseline.current.cliQuotaEnabled : draft.cliQuotaEnabled) === true,
725
+ cliQuotaDirty: dirtyKeys.includes('cliQuotaEnabled') }),
726
+ h('section', { style: PANEL, 'aria-label': '额度取数方式' },
727
+ h(SectionHeading, { title: 'CLI 额度(可选)' }),
728
+ h(SettingsFields, { ...fieldProps, fields: fieldsFor('quota') }))),
729
+
730
+ h('div', { id: 'agy-settings-page-advanced', role: 'tabpanel', style: pageStyle('advanced') },
731
+ !effectiveProxyMode ? h('section', { style: PANEL, 'aria-label': '常用设置' },
732
+ h(SectionHeading, { title: '会话偏好' }),
733
+ h(SettingsFields, { ...fieldProps, fields: fieldsFor('general') })) : null,
734
+ h(SettingsGroup, { title: '网络与 CLI', fields: fieldsFor('network'), fieldProps, open: effectiveProxyMode }),
735
+ !effectiveProxyMode ? h(SettingsGroup, { title: 'ACP 启动', fields: fieldsFor('launch'), fieldProps }) : null,
736
+ !effectiveProxyMode ? h(SettingsGroup, { title: '连接池', fields: fieldsFor('pool'), fieldProps }) : null,
737
+ !effectiveProxyMode ? h(SettingsGroup, { title: '请求与工具超时', fields: fieldsFor('timeouts'), fieldProps }) : null,
738
+ !effectiveProxyMode ? h(SettingsGroup, { title: '会话与进程', fields: fieldsFor('advanced'), fieldProps }) : null,
739
+ !effectiveProxyMode ? h('details', { style: PANEL, 'aria-label': '连接诊断' },
740
+ h('summary', { style: SUMMARY },
741
+ h('span', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
742
+ h(StatusDot, { color: connection.color }),
743
+ h('span', { style: { fontSize: '13px', fontWeight: 650 } }, '连接诊断')),
744
+ h('small', { style: { fontSize: '11px', opacity: .6 } }, '状态、并发与耗时')),
745
+ h('div', { style: { marginTop: '16px', paddingTop: '14px', borderTop: '1px solid rgba(128,128,128,.14)' } },
746
+ account?.runtime ? RuntimeDetails({ runtime: account.runtime }) : h('p', { style: { margin: 0, fontSize: '12px', opacity: .6 } }, '连接后显示运行详情。'))) : null))
573
747
  }
574
748
 
575
749
  /**
@@ -581,7 +755,7 @@ window.__ModuleLoader__.load({
581
755
 
582
756
  const TRACK = 'rgba(128,128,128,.18)'
583
757
  const CARD = { padding: '12px', borderRadius: '12px', border: '1px solid rgba(128,128,128,.18)', background: 'rgba(128,128,128,.05)' }
584
- const PANEL = { padding: '18px', borderRadius: '14px', border: '1px solid rgba(128,128,128,.16)', background: 'rgba(128,128,128,.025)' }
758
+ const PANEL = { padding: '14px', borderRadius: '12px', border: '1px solid rgba(128,128,128,.16)', background: 'rgba(128,128,128,.025)' }
585
759
  const SUMMARY = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px', cursor: 'pointer', listStyle: 'none' }
586
760
  const ACTION = { padding: '7px 12px', minHeight: '34px', borderRadius: '8px', border: '1px solid rgba(128,128,128,.22)', background: 'rgba(128,128,128,.06)', color: 'inherit', font: 'inherit', fontSize: '12px', cursor: 'pointer' }
587
761
  const PRIMARY = { ...ACTION, background: '#177c55', border: '1px solid #177c55', color: '#fff', fontWeight: 600 }
@@ -594,7 +768,7 @@ window.__ModuleLoader__.load({
594
768
  return undefined
595
769
  }
596
770
  function modelGroup(model) {
597
- const id = String(model ?? '').toLowerCase()
771
+ const id = String(model ?? '').toLowerCase().replace(/^antigravity-/, '')
598
772
  if (/^gemini-/.test(id) && /(?:^|-)argon(?:-|$)/.test(id)) return 'argon'
599
773
  if (/^gemini-/.test(id)) return 'gemini'
600
774
  if (/^(claude|gpt)-/.test(id)) return 'claude'
@@ -627,26 +801,29 @@ window.__ModuleLoader__.load({
627
801
  if (minutes >= 60) return Math.floor(minutes / 60) + '小时 ' + minutes % 60 + '分钟后重置'
628
802
  return minutes + '分钟后重置'
629
803
  }
630
- function remainingBar(bucket, compact, stale, label) {
804
+ function remainingBar(bucket, compact, stale, label, compactHeight = 4) {
631
805
  const pct = percentage(bucket)
632
806
  const unknown = pct === undefined || bucket.disabled
633
807
  return h('span', {
634
808
  role: 'progressbar', 'aria-label': label,
635
809
  'aria-valuemin': 0, 'aria-valuemax': 100,
636
810
  ...(unknown ? { 'aria-valuetext': bucket.disabled ? '已停用' : '额度未知' } : { 'aria-valuenow': pct }),
637
- style: { display: 'block', width: compact ? '30px' : '100%', height: compact ? '4px' : '6px', borderRadius: '99px', overflow: 'hidden', background: TRACK },
811
+ style: { display: 'block', width: compact ? '34px' : '100%', height: compact ? compactHeight + 'px' : '4px', borderRadius: '99px', overflow: 'hidden', background: TRACK },
638
812
  }, h('span', { style: { display: 'block', height: '100%', width: unknown ? '100%' : pct + '%', opacity: unknown ? .35 : 1, borderRadius: '99px', background: tone(bucket, stale) } }))
639
813
  }
640
- function useQuota(readQuota, enabled = true) {
814
+ function useQuota(readQuota, enabled = true, source, model) {
641
815
  const [data, setData] = React.useState(null)
642
816
  const [refreshing, setRefreshing] = React.useState(false)
643
817
  const version = React.useRef(0)
818
+ const currentSource = React.useRef(source)
819
+ const currentModel = React.useRef(model)
820
+ currentModel.current = model
644
821
  const update = React.useCallback((value) => setData(value), [])
645
822
  const load = React.useCallback((force = false) => {
646
823
  if (!enabled || !readQuota) return Promise.resolve()
647
824
  const current = ++version.current
648
825
  setRefreshing(true)
649
- return Promise.resolve().then(() => readQuota(force)).then(unwrap).then((value) => {
826
+ return Promise.resolve().then(() => readQuota(force, source, model)).then(unwrap).then((value) => {
650
827
  if (current === version.current) update(value)
651
828
  }).catch((error) => {
652
829
  if (current === version.current) {
@@ -656,95 +833,155 @@ window.__ModuleLoader__.load({
656
833
  : { status: 'error', groups: [], error: { message } })
657
834
  }
658
835
  }).finally(() => { if (current === version.current) setRefreshing(false) })
659
- }, [readQuota, enabled, update])
836
+ }, [readQuota, enabled, update, source, model])
660
837
  React.useEffect(() => {
838
+ currentSource.current = source
839
+ setData(null)
661
840
  if (!enabled || !readQuota) { setData(null); return undefined }
662
841
  void load(false)
663
- const off = readQuota.subscribe?.((value) => { ++version.current; update(value) })
842
+ const off = readQuota.subscribe?.((value, origin) => {
843
+ if (origin !== undefined && origin !== source) return
844
+ if (value?.requestedModel !== undefined && value.requestedModel !== currentModel.current) return
845
+ if (value === null) {
846
+ ++version.current; update(null); setRefreshing(false)
847
+ void load(true)
848
+ return
849
+ }
850
+ ++version.current; update(value); setRefreshing(false)
851
+ })
664
852
  const timer = setInterval(() => { if (typeof document === 'undefined' || document.visibilityState !== 'hidden') void load(false) }, 300_000)
665
853
  return () => { ++version.current; clearInterval(timer); off?.() }
666
- }, [load, enabled, readQuota, update])
667
- return { data, refreshing, load }
854
+ }, [load, enabled, readQuota, update, source, model])
855
+ return { data: currentSource.current === source ? data : null, refreshing, load }
668
856
  }
669
857
  function CreditsDetails({ credits, pending }) {
670
858
  const valid = credits?.status === 'ok' && Number.isSafeInteger(credits.remainingCredits) && credits.remainingCredits >= 0
671
- const label = valid ? credits.remainingCredits.toLocaleString() : pending ? '正在读取…' : credits?.status === 'error' ? '查询失败' : '未提供'
672
- return h('section', { style: CARD, 'aria-label': 'AI Credits 余额' },
859
+ const label = valid ? credits.remainingCredits.toLocaleString() : pending ? '正在读取…' : credits?.status === 'error' ? '查询失败' : credits?.status === 'unavailable' ? '未启用' : '未提供'
860
+ return h('section', { style: { padding: '9px 0', borderTop: '1px solid rgba(128,128,128,.16)' }, 'aria-label': 'AI Credits 余额' },
673
861
  h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: '12px' } },
674
- h('strong', null, 'AI Credits 余额'),
675
- h('strong', { style: { fontSize: valid ? '20px' : '13px', fontVariantNumeric: 'tabular-nums', color: credits?.stale ? '#d99b28' : 'inherit' } }, label)),
676
- credits?.planName ? h('div', { style: { fontSize: '11px', opacity: .7, marginTop: '6px' } }, '套餐:' + credits.planName) : null,
677
- credits?.stale ? h('p', { role: 'status', style: { margin: '6px 0 0', fontSize: '11px', color: '#d99b28' } }, '余额缓存 · ' + (credits.staleReason ?? '余额刷新失败')) : null,
678
- credits?.error?.message ? h('p', { role: credits.status === 'error' ? 'alert' : 'status', style: { margin: '6px 0 0', fontSize: '11px', overflowWrap: 'anywhere' } }, credits.error.message) : null,
679
- credits?.updatedAt ? h('div', { style: { marginTop: '6px', fontSize: '10px', opacity: .55 } }, (valid ? '余额更新于 ' : '最近查询 ') + new Date(credits.updatedAt).toLocaleString()) : null)
680
- }
681
- function QuotaDetails({ data, refreshing, load, model }) {
682
- return h('div', { style: { display: 'grid', gap: '12px' } },
683
- h('div', { style: { display: 'flex', gap: '8px', alignItems: 'center' } },
684
- h('strong', { style: { fontSize: '14px', flex: 1 } }, 'Antigravity · 套餐额度'),
685
- data?.plan ? h('span', { style: { fontSize: '11px', padding: '2px 8px', background: TRACK, borderRadius: '99px' } }, data.plan) : null),
686
- data?.stale ? h('p', { role: 'status', style: { ...CARD, margin: 0, color: '#d99b28', fontSize: '12px' } }, '缓存数据 · ' + (data.staleReason ?? '刷新失败')) : null,
687
- data?.status === 'error' ? h('p', { role: 'alert', style: { margin: 0, color: '#ef4444', overflowWrap: 'anywhere' } }, data.error?.message ?? data.error?.text ?? '额度读取失败') : null,
688
- ...quotaGroups(data).map((group) => h('section', { key: group.key, style: CARD, 'aria-label': group.name + '额度' },
689
- h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '10px' } },
690
- h('strong', { style: { fontSize: '13px' } }, group.name),
691
- group.key === modelGroup(model) ? h('span', { style: { fontSize: '10px', padding: '2px 6px', borderRadius: '6px', color: '#25b879', background: 'rgba(37,184,121,.12)' } }, '当前模型') : null),
692
- group.buckets.length ? h('div', { style: { display: 'grid', gap: '12px' } }, ...group.buckets.map((bucket, index) => {
693
- const pct = percentage(bucket)
694
- return h('div', { key: index },
695
- h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: '12px', marginBottom: '6px' } },
696
- h('span', { style: { fontSize: '12px', opacity: .75 } }, windowName(bucket)),
697
- h('strong', { style: { fontSize: '16px', fontVariantNumeric: 'tabular-nums' } }, bucket.disabled ? '已停用' : pct === undefined ? '未知' : Math.round(pct) + '%', h('small', { style: { fontSize: '10px', fontWeight: 400, opacity: .6, marginLeft: '4px' } }, '剩余'))),
698
- remainingBar(bucket, false, data?.stale, group.name + ' ' + windowName(bucket) + '剩余额度'),
699
- h('div', { title: bucket.resetTime, style: { fontSize: '10px', opacity: .6, marginTop: '5px' } }, bucket.disabled ? bucket.description ?? '此窗口不可用' : resetCountdown(bucket.resetTime)))
700
- })) : h('span', { style: { opacity: .55, fontSize: '12px' } }, data ? '未提供额度数据' : '正在读取…'))),
701
- CreditsDetails({ credits: data?.credits, pending: !data }),
702
- h('div', { style: { display: 'flex', alignItems: 'center', gap: '10px' } },
703
- h('small', { style: { flex: 1, opacity: .55, minWidth: 0 } }, data?.updatedAt ? '更新于 ' + new Date(data.updatedAt).toLocaleString() : '尚无成功快照'),
704
- h('button', { type: 'button', disabled: refreshing || !load, onClick: () => load(true), style: ACTION }, refreshing ? '刷新中…' : '刷新额度')))
862
+ h('strong', { style: { fontSize: '12px' } }, 'AI Credits'),
863
+ h('strong', { style: { fontSize: valid ? '17px' : '12px', fontVariantNumeric: 'tabular-nums', color: credits?.stale ? '#d99b28' : 'inherit' } }, label)),
864
+ credits?.planName || credits?.updatedAt ? h('div', { style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: '3px 10px', fontSize: '10px', opacity: .55, marginTop: '3px' } },
865
+ credits?.planName ? h('span', null, credits.planName) : null,
866
+ credits?.updatedAt ? h('span', null, (valid ? '余额更新于 ' : '最近查询 ') + new Date(credits.updatedAt).toLocaleString()) : null) : null,
867
+ credits?.stale ? h('p', { role: 'status', style: { margin: '4px 0 0', fontSize: '10px', color: '#d99b28' } }, '余额缓存 · ' + (credits.staleReason ?? '余额刷新失败')) : null,
868
+ credits?.error?.message ? h('p', { role: credits.status === 'error' ? 'alert' : 'status', style: { margin: '4px 0 0', fontSize: '10px', overflowWrap: 'anywhere' } }, credits.error.message) : null)
705
869
  }
706
- function useAcpWarmup(control, enabled) {
707
- const [account, setAccount] = React.useState(undefined)
708
- const [pending, setPending] = React.useState(false)
709
- const alive = React.useRef(false)
870
+ function SettingsQuotaPanel({ readQuota, source, accountRevision, cliQuotaOn, cliQuotaDirty }) {
871
+ const [data, setData] = React.useState(null)
872
+ const [refreshing, setRefreshing] = React.useState(false)
710
873
  const version = React.useRef(0)
711
- const polling = React.useRef(false)
712
- const run = React.useCallback(async (action = 'status') => {
713
- if (!control || !enabled || (action === 'status' && polling.current)) return
874
+ const requested = React.useRef(false)
875
+ const pending = React.useRef(false)
876
+ React.useEffect(() => {
877
+ ++version.current
878
+ requested.current = false
879
+ pending.current = false
880
+ setData(null)
881
+ setRefreshing(false)
882
+ const off = readQuota?.subscribe?.((value, origin) => {
883
+ if (origin !== source) return
884
+ if (value === null) {
885
+ ++version.current; requested.current = false; pending.current = false
886
+ setData(null); setRefreshing(false)
887
+ } else if (requested.current) setData(value)
888
+ })
889
+ return () => { ++version.current; off?.() }
890
+ }, [readQuota, source, accountRevision])
891
+ const load = async () => {
892
+ if (!readQuota || pending.current) return
893
+ pending.current = true
894
+ requested.current = true
714
895
  const current = ++version.current
715
- polling.current = true
716
- if (action === 'warmup') setPending(true)
896
+ setRefreshing(true)
717
897
  try {
718
- const value = unwrap(await control('account', { action }))
719
- if (alive.current && current === version.current) setAccount(value)
898
+ const value = unwrap(await readQuota(true, source))
899
+ if (current === version.current) setData(value)
720
900
  } catch (error) {
721
- if (alive.current && current === version.current) setAccount({ state: 'error', message: error instanceof Error ? error.message : String(error) })
901
+ if (current === version.current) {
902
+ const message = error instanceof Error ? error.message : String(error)
903
+ setData(previous => previous?.status === 'ok' ? { ...previous, stale: true, staleReason: message }
904
+ : { status: 'error', groups: [], error: { message } })
905
+ }
722
906
  } finally {
723
- polling.current = false
724
- if (alive.current && action === 'warmup') setPending(false)
907
+ if (current === version.current) { pending.current = false; setRefreshing(false) }
725
908
  }
726
- }, [control, enabled])
727
- React.useEffect(() => {
728
- alive.current = true
729
- if (!enabled) return () => { alive.current = false; ++version.current }
730
- void run()
731
- const timer = setInterval(() => {
732
- if (typeof document === 'undefined' || document.visibilityState !== 'hidden') void run()
733
- }, 2000)
734
- return () => { alive.current = false; ++version.current; clearInterval(timer) }
735
- }, [run, enabled])
736
- return { account, pending, run }
909
+ }
910
+ return h('section', { style: PANEL, 'aria-label': '套餐额度查询' },
911
+ h(SectionHeading, { title: '套餐额度', description: (source === PROXY_PROVIDER ? '代理账户' : 'ACP 账户') + (cliQuotaDirty ? ' · CLI 开关未保存' : cliQuotaOn ? ' · CLI 补充取数' : ''),
912
+ action: h('button', { type: 'button', style: ACTION, disabled: !readQuota || refreshing, 'aria-busy': refreshing || undefined, onClick: load }, refreshing ? '正在拉取…' : '拉取额度') }),
913
+ data ? QuotaDetails({ data, refreshing, source }) : h('p', { role: 'status', style: { margin: 0, fontSize: '12px', lineHeight: 1.5, opacity: .65 } }, refreshing ? '正在读取…' : '尚未查询'))
737
914
  }
738
- function AgYQuotaPill({ store, readQuota, control }) {
915
+ function QuotaDetails({ data, refreshing, load, model, source, onClose }) {
916
+ return h('div', { style: { display: 'grid', gap: 0 } },
917
+ h('div', { style: { display: 'flex', gap: '8px', alignItems: 'center', paddingBottom: '9px', borderBottom: '1px solid rgba(128,128,128,.16)' } },
918
+ h('strong', { style: { fontSize: '14px', flex: 1 } }, 'Antigravity · 套餐额度'),
919
+ data?.plan ? h('span', { style: { fontSize: '11px', padding: '2px 8px', background: TRACK, borderRadius: '99px' } }, data.plan) : null,
920
+ onClose ? h('button', { type: 'button', 'aria-label': '关闭额度详情', onClick: onClose, style: { ...ACTION, padding: '2px 7px', minHeight: '26px', borderColor: 'transparent' } }, '×') : null),
921
+ data?.stale ? h('p', { role: 'status', style: { margin: '8px 0', padding: '6px 8px', borderRadius: '7px', background: 'rgba(217,155,40,.10)', color: '#d99b28', fontSize: '11px' } }, '缓存数据 · ' + (data.staleReason ?? '刷新失败')) : null,
922
+ data?.status === 'error' ? h('p', { role: 'alert', style: { margin: '8px 0', color: '#ef4444', fontSize: '11px', overflowWrap: 'anywhere' } }, data.error?.message ?? data.error?.text ?? '额度读取失败') : null,
923
+ ...quotaGroups(data).map((group) => h('section', { key: group.key, style: { padding: '9px 0', borderBottom: '1px solid rgba(128,128,128,.14)' }, 'aria-label': group.name + '额度' },
924
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: '7px', marginBottom: '7px' } },
925
+ h('strong', { style: { fontSize: '12px' } }, group.name),
926
+ group.key === modelGroup(model) ? h('span', { style: { fontSize: '9px', padding: '1px 5px', borderRadius: '5px', color: '#25b879', background: 'rgba(37,184,121,.12)' } }, '当前模型') : null),
927
+ group.buckets.length ? h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))', gap: '9px 12px' } }, ...group.buckets.map((bucket, index) => {
928
+ const pct = percentage(bucket)
929
+ return h('div', { key: index, style: { minWidth: 0 } },
930
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: '8px', marginBottom: '4px' } },
931
+ h('span', { style: { fontSize: '11px', opacity: .7 } }, windowName(bucket)),
932
+ h('strong', { style: { fontSize: bucket.disabled ? '12px' : '15px', fontVariantNumeric: 'tabular-nums' } }, bucket.disabled ? '已停用' : pct === undefined ? '未知' : Math.round(pct) + '%')),
933
+ remainingBar(bucket, false, data?.stale, group.name + ' ' + windowName(bucket) + '剩余额度'),
934
+ h('div', { title: bucket.disabled ? bucket.description : bucket.resetTime, style: { display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden', fontSize: '10px', lineHeight: 1.35, opacity: .58, marginTop: '4px' } }, bucket.disabled ? bucket.description ?? '此窗口不可用' : resetCountdown(bucket.resetTime)))
935
+ })) : h('span', { style: { opacity: .55, fontSize: '11px' } }, data ? '未提供额度数据' : '正在读取…'))),
936
+ CreditsDetails({ credits: data?.credits, pending: !data }),
937
+ source === PROXY_PROVIDER ? h('small', { style: { opacity: .55, fontSize: '10px', lineHeight: 1.4, padding: '7px 0', borderBottom: '1px solid rgba(128,128,128,.14)' } }, '套餐额度属于代理账户。AI Credits 默认不查询;打开「用 agy CLI 查询额度」后才读取,且来自官方 CLI 账户,可能不是同一账户。') : null,
938
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', paddingTop: '8px' } },
939
+ h('small', { style: { flex: 1, opacity: .5, minWidth: 0, fontSize: '10px' } }, data?.updatedAt ? '更新于 ' + new Date(data.updatedAt).toLocaleString() : '尚无成功快照'),
940
+ load ? h('button', { type: 'button', disabled: refreshing, onClick: () => load(true), style: { ...ACTION, minHeight: '27px', padding: '3px 9px', fontSize: '11px' } }, refreshing ? '刷新中…' : '刷新额度') : null))
941
+ }
942
+ /** 紧凑余量条与单词状态的交互参考 dsh-subusage;取数仍由本插件负责。 */
943
+ function quotaPillStatus(data, buckets) {
944
+ if (!data) return { short: '', icon: '…', color: '#8b909a' }
945
+ if (data.status === 'error') return { short: '失败', icon: '!', color: '#e5534b', tint: true }
946
+ if (data.stale) return { short: '缓存', icon: '!', color: '#d99b28', tint: true }
947
+ const usable = buckets.filter(bucket => !bucket.disabled && percentage(bucket) !== undefined)
948
+ if (!usable.length) return { short: buckets.some(bucket => bucket.disabled) ? '受限' : '未知', icon: '?', color: '#8b909a' }
949
+ const minimum = Math.min(...usable.map(percentage))
950
+ if (minimum === 0) return { short: '已用尽', color: '#e5534b', tint: true }
951
+ if (minimum <= 10) return { short: '紧张', color: '#e5534b', tint: true }
952
+ if (minimum <= 25) return { short: '偏少', color: '#d99b28', tint: true }
953
+ return { short: '', color: '#25b879' }
954
+ }
955
+ function AgYQuotaPill({ store, readQuota, proxyControl }) {
739
956
  const state = React.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot)
740
- const enabled = state?.current?.provider === PROVIDER
957
+ const isAcp = state?.current?.provider === PROVIDER
958
+ const isProxy = state?.current?.provider === PROXY_PROVIDER
959
+ const enabled = isAcp || isProxy
741
960
  const model = state?.current?.model
742
- const quota = useQuota(readQuota, enabled)
961
+ const quota = useQuota(readQuota, enabled, state?.current?.provider, model)
743
962
  const [expanded, setExpanded] = React.useState(false)
744
963
  const [position, setPosition] = React.useState(null)
745
964
  const root = React.useRef(null)
746
965
  const button = React.useRef(null)
747
- const warmup = useAcpWarmup(control, enabled && Boolean(control))
966
+ React.useEffect(() => {
967
+ if (!isProxy || !proxyControl) return
968
+ let active = true
969
+ let pending = false
970
+ const check = async () => {
971
+ if (pending) return
972
+ pending = true
973
+ try {
974
+ const value = unwrap(await proxyControl('account', { action: 'status' }))
975
+ if (active) readQuota?.observeProxyAccount?.(value.snapshot)
976
+ } catch { /* 账户轮询失败不覆盖额度查询自身的结果。 */ }
977
+ finally { pending = false }
978
+ }
979
+ void check()
980
+ const timer = setInterval(() => {
981
+ if (typeof document === 'undefined' || document.visibilityState !== 'hidden') void check()
982
+ }, 3000)
983
+ return () => { active = false; clearInterval(timer) }
984
+ }, [isProxy, proxyControl, readQuota])
748
985
  React.useEffect(() => { setExpanded(false) }, [enabled])
749
986
  React.useEffect(() => {
750
987
  if (!expanded) { setPosition(null); return undefined }
@@ -774,36 +1011,26 @@ window.__ModuleLoader__.load({
774
1011
  }, [expanded])
775
1012
  if (!enabled) return null
776
1013
  const groups = quotaGroups(quota.data)
777
- const family = modelGroup(model)
1014
+ const family = modelGroup(quota.data?.routeModel ?? model)
778
1015
  // 官方 /usage 可能只报告 Gemini 共享额度;只有真实 Argon 分组存在时,
779
1016
  // 才把 Argon 型号绑定到独立组,不能命中一个预建的空组而误报未知。
780
1017
  const group = groups.find((g) => g.key === family && (family !== 'argon' || g.buckets.length > 0))
781
1018
  ?? (family === 'argon' ? groups.find((g) => g.key === 'gemini') : undefined)
782
1019
  const buckets = group?.buckets ?? []
1020
+ const pillStatus = quotaPillStatus(quota.data, buckets)
1021
+ const miniBuckets = buckets.slice(0, 5)
1022
+ const miniHeight = miniBuckets.length <= 1 ? 6 : miniBuckets.length === 2 ? 5 : miniBuckets.length === 3 ? 4 : miniBuckets.length === 4 ? 3 : 2
1023
+ const modeTag = isProxy ? '代理' : 'ACP'
783
1024
  const label = quota.data?.status === 'error' ? '额度读取失败' : !quota.data ? '正在读取额度' : !group ? '额度未匹配,请选择具体模型' : !buckets.length ? '额度未知' : buckets.map((b) => windowName(b) + ' ' + (b.disabled ? '已停用' : percentage(b) === undefined ? '额度未知' : Math.round(percentage(b)) + '% 剩余')).join(' · ')
784
- const title = String(model ?? '未选择模型') + ' · ' + label + (quota.data?.stale ? ' · 缓存数据' : '')
785
- const runtime = warmup.account?.runtime
786
- const warming = warmup.pending || ['connecting', 'cancelling'].includes(warmup.account?.state)
787
- || ['binary', 'credentials', 'spawn', 'initialize', 'authenticate'].includes(runtime?.phase)
788
- const working = !warming && runtimeBusy(runtime)
789
- const ready = runtime?.phase === 'ready'
790
- const failed = warmup.account?.state === 'error' || runtime?.phase === 'error'
791
- const warmupLabel = warming ? '预热中…' : runtime?.queue?.waiting > 0 ? '排队 ' + runtime.queue.waiting : working ? '运行中' : ready ? '已预热' : failed ? '重试预热' : '预热'
792
- const warmupTitle = runtime ? (RUNTIME_PHASES[runtime.phase] ?? runtime.phase) + ' · ' + duration(runtime.phaseElapsedMs)
793
- + (runtime.queue ? ' · 运行 ' + runtime.queue.active + ' · 等待工具 ' + runtime.queue.parked + ' · 排队 ' + runtime.queue.waiting : '')
794
- + (runtime.error ? ' · ' + runtime.error : '') : warmup.account?.message ?? '提前启动 ACP 连接,不发送模型消息'
795
- return h('span', { ref: root, style: { position: 'relative', display: 'inline-flex', alignItems: 'center', gap: '6px' } },
1025
+ const title = modeTag + ' · ' + String(model ?? '未选择模型') + ' · ' + label + (quota.data?.stale ? ' · 缓存数据' : '')
1026
+ return h('span', { ref: root, className: 'antigravity-usage-pill', 'data-expanded': String(expanded), style: { position: 'relative', display: 'inline-flex', alignItems: 'center' } },
796
1027
  h('button', { ref: button, type: 'button', title, 'aria-label': title, 'aria-expanded': expanded, 'aria-haspopup': 'dialog', onClick: () => setExpanded((value) => !value),
797
- style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: '6px 10px', minHeight: '26px', borderRadius: '99px', border: '1px solid rgba(128,128,128,.28)', background: 'transparent', color: 'inherit', cursor: 'pointer' } },
798
- buckets.length ? h('span', { style: { display: 'grid', gap: '3px' } }, ...buckets.map((b, i) => h('span', { key: i }, remainingBar(b, true, quota.data?.stale, group.name + ' ' + windowName(b) + '剩余额度')))) : h('span', { 'aria-hidden': true }, quota.data?.status === 'error' ? '!' : quota.data ? '?' : '…')),
799
- control ? h('button', {
800
- type: 'button', title: warmupTitle, 'aria-label': '预热 ACP', 'aria-busy': warming,
801
- disabled: Boolean(warming || working || ready), onClick: () => warmup.run('warmup'),
802
- style: { ...ACTION, padding: '4px 9px', minHeight: '26px', borderRadius: '99px', fontSize: '11px', whiteSpace: 'nowrap',
803
- color: ready ? '#25b879' : failed ? '#e5534b' : 'inherit', cursor: warming || working || ready ? 'default' : 'pointer' },
804
- }, warmupLabel) : null,
805
- expanded ? h('div', { role: 'dialog', 'aria-label': 'Antigravity 额度详情', style: { position: 'absolute', bottom: 'calc(100% + 8px)', right: 0, zIndex: 50, width: 'min(340px, calc(100vw - 48px))', maxHeight: 'min(75vh, calc(100vh - 72px))', overflowY: 'auto', boxSizing: 'border-box', overflowWrap: 'anywhere', padding: '16px', borderRadius: '16px', background: 'var(--dsh-surface, Canvas)', color: 'var(--dsh-text, CanvasText)', border: '1px solid rgba(128,128,128,.25)', boxShadow: '0 14px 40px rgba(0,0,0,.22)', fontSize: '12px', ...position } },
806
- QuotaDetails({ ...quota, model })) : null)
1028
+ style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '8px', padding: pillStatus.short ? '4px 11px 4px 9px' : '5px 10px', minHeight: '26px', boxSizing: 'border-box', borderRadius: '99px', border: '1px solid ' + (pillStatus.tint ? pillStatus.color + '73' : 'rgba(128,128,128,.3)'), background: pillStatus.tint ? pillStatus.color + '1a' : 'transparent', color: 'inherit', cursor: 'pointer', font: 'inherit', fontSize: '12px', fontWeight: 600, whiteSpace: 'nowrap', maxWidth: 'min(46vw, 240px)' } },
1029
+ miniBuckets.length ? h('span', { style: { display: 'grid', gap: miniBuckets.length <= 2 ? '3px' : '2px', opacity: quota.data?.stale ? .5 : 1 } }, ...miniBuckets.map((b, i) => h('span', { key: i }, remainingBar(b, true, quota.data?.stale, group.name + ' ' + windowName(b) + '剩余额度', miniHeight))))
1030
+ : h('span', { 'aria-hidden': true, style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: '16px', height: '16px', borderRadius: '50%', background: pillStatus.color, color: '#fff', fontSize: '11px' } }, pillStatus.icon),
1031
+ pillStatus.short ? h('span', null, pillStatus.short) : null),
1032
+ expanded ? h('div', { role: 'dialog', 'aria-label': 'Antigravity 额度详情', style: { position: 'absolute', bottom: 'calc(100% + 8px)', right: 0, zIndex: 50, width: 'min(340px, calc(100vw - 24px))', maxHeight: 'min(70vh, calc(100vh - 72px))', overflowY: 'auto', boxSizing: 'border-box', overflowWrap: 'anywhere', padding: '12px', borderRadius: '12px', background: 'var(--dsh-surface, Canvas)', color: 'var(--dsh-text, CanvasText)', border: '1px solid rgba(128,128,128,.28)', boxShadow: '0 12px 32px rgba(0,0,0,.28)', fontSize: '12px', ...position } },
1033
+ QuotaDetails({ ...quota, model, source: state?.current?.provider, onClose: () => { setExpanded(false); button.current?.focus?.() } })) : null)
807
1034
  }
808
1035
 
809
1036
  /** 给 promise 加超时:Host 侧异常时界面也要落到可读错误,而不是永远 pending。 */
@@ -820,7 +1047,11 @@ window.__ModuleLoader__.load({
820
1047
  /** 组装额度读取器:remote.agyQuota 由 Host 注册后才出现,短轮询等待就绪。 */
821
1048
  function makeQuotaReader(scope) {
822
1049
  const listeners = new Set()
823
- const read = async (force) => {
1050
+ const epochs = new Map()
1051
+ const modelsBySource = new Map()
1052
+ let proxyAccountKey
1053
+ let mode
1054
+ const read = async (force, source, model) => {
824
1055
  const deadline = Date.now() + 20_000
825
1056
  for (;;) {
826
1057
  // **只能用 `get()`**:`scope.remote.agyQuota` 这种属性访问在未 inject 的
@@ -832,7 +1063,11 @@ window.__ModuleLoader__.load({
832
1063
  // Host 侧会起官方 agy CLI 取数(其启动含登录刷新)。万一它挂在网络上,
833
1064
  // 也要让药丸显示「读取失败 + 原因」,而不是一直停在「额度…」。
834
1065
  return await withTimeout(
835
- service.read(force ? { force: true } : undefined),
1066
+ service.read({
1067
+ ...(force ? { force: true } : {}),
1068
+ ...(source === PROVIDER ? { source: 'acp' } : source === PROXY_PROVIDER ? { source: 'proxy' } : {}),
1069
+ ...(model ? { model } : {}),
1070
+ }),
836
1071
  60_000,
837
1072
  '额度读取超时(Host 侧 agy CLI 60 秒内没有返回)',
838
1073
  )
@@ -843,12 +1078,38 @@ window.__ModuleLoader__.load({
843
1078
  await new Promise((resolve) => setTimeout(resolve, 500))
844
1079
  }
845
1080
  }
846
- const reader = async (force) => {
847
- const value = unwrap(await read(force))
848
- for (const listener of listeners) listener(value)
1081
+ const reader = async (force, source, model) => {
1082
+ if (model) modelsBySource.set(source, model)
1083
+ const requestedModel = model ?? modelsBySource.get(source)
1084
+ const epoch = epochs.get(source) ?? 0
1085
+ const value = unwrap(await read(force, source, requestedModel))
1086
+ if (epoch !== (epochs.get(source) ?? 0)) throw new Error('账户已变化,请重新拉取额度')
1087
+ for (const listener of listeners) {
1088
+ if (source === undefined) listener(value)
1089
+ else listener(value, source)
1090
+ }
849
1091
  return value
850
1092
  }
851
1093
  reader.subscribe = (listener) => { listeners.add(listener); return () => listeners.delete(listener) }
1094
+ reader.invalidate = (source) => {
1095
+ epochs.set(source, (epochs.get(source) ?? 0) + 1)
1096
+ for (const listener of listeners) listener(null, source)
1097
+ }
1098
+ reader.observeProxyAccount = (snapshot) => {
1099
+ if (!snapshot) return
1100
+ const key = JSON.stringify([snapshot.revision, snapshot.phase === 'signed-in', snapshot.email])
1101
+ if (proxyAccountKey !== undefined && key !== proxyAccountKey) {
1102
+ proxyAccountKey = key
1103
+ reader.invalidate(PROXY_PROVIDER)
1104
+ } else proxyAccountKey = key
1105
+ }
1106
+ reader.observeMode = (source) => {
1107
+ if (mode !== undefined && mode !== source) {
1108
+ mode = source
1109
+ reader.invalidate(PROVIDER)
1110
+ reader.invalidate(PROXY_PROVIDER)
1111
+ } else mode = source
1112
+ }
852
1113
  return reader
853
1114
  }
854
1115
 
@@ -869,6 +1130,22 @@ window.__ModuleLoader__.load({
869
1130
  }
870
1131
  }
871
1132
 
1133
+ /** 代理账户控制读取器:remote.agyProxyAccount 由 Host 的代理行注册。 */
1134
+ function makeProxyControlReader(scope) {
1135
+ return async (method, request) => {
1136
+ if (method !== 'account') throw new Error('不支持的代理账户操作')
1137
+ const deadline = Date.now() + 20_000
1138
+ for (;;) {
1139
+ const service = scope.get('remote.agyProxyAccount')
1140
+ if (typeof service?.account === 'function') {
1141
+ return await withTimeout(service.account(request), 60_000, '代理账户操作超时,请重试')
1142
+ }
1143
+ if (Date.now() >= deadline) throw new Error('代理账户服务未就绪:请确认已启用本插件的代理路由组件(antigravity-proxy-router)')
1144
+ await new Promise((resolve) => setTimeout(resolve, 500))
1145
+ }
1146
+ }
1147
+ }
1148
+
872
1149
  /**
873
1150
  * 客户端侧的额度 remote 声明。
874
1151
  *
@@ -882,16 +1159,31 @@ window.__ModuleLoader__.load({
882
1159
  */
883
1160
  const AGY_QUOTA_PACKAGE = 'dsh-bundle-antigravity-host'
884
1161
  const AGY_QUOTA_SERVICE = 'agyQuota'
1162
+ const AGY_PROXY_ACCOUNT_SERVICE = 'agyProxyAccount'
885
1163
 
886
1164
  function strictCodec(typeSymbol, parse) {
887
1165
  const schema = { parse }
888
1166
  return { mode: 'strict', typeSymbol, create: () => schema }
889
1167
  }
890
1168
 
1169
+ function parseProxyAccountRequest(value) {
1170
+ if (value === undefined || value === null) return {}
1171
+ if (typeof value !== 'object' || Array.isArray(value)) throw new TypeError('Invalid AgYProxyAccountRequest')
1172
+ if (value.action !== undefined && !['status', 'login', 'cancel', 'logout'].includes(value.action)) throw new TypeError('Invalid AgYProxyAccountRequest.action')
1173
+ return value
1174
+ }
1175
+
1176
+ function parseProxyAccountResult(value) {
1177
+ if (!value || typeof value !== 'object' || typeof value.message !== 'string' || !value.snapshot || typeof value.snapshot !== 'object') throw new TypeError('Invalid AgYProxyAccountResult')
1178
+ return value
1179
+ }
1180
+
891
1181
  function parseQuotaRequest(value) {
892
1182
  if (value === undefined || value === null) return undefined
893
1183
  if (typeof value !== 'object' || Array.isArray(value)) throw new TypeError('Invalid AgYQuotaRequest')
894
1184
  if (value.force !== undefined && typeof value.force !== 'boolean') throw new TypeError('Invalid AgYQuotaRequest.force')
1185
+ if (value.source !== undefined && !['acp', 'proxy'].includes(value.source)) throw new TypeError('Invalid AgYQuotaRequest.source')
1186
+ if (value.model !== undefined && (typeof value.model !== 'string' || !value.model.trim())) throw new TypeError('Invalid AgYQuotaRequest.model')
895
1187
  return value
896
1188
  }
897
1189
 
@@ -942,18 +1234,44 @@ window.__ModuleLoader__.load({
942
1234
  ],
943
1235
  }
944
1236
 
1237
+ const AGY_PROXY_REMOTE = {
1238
+ package: AGY_QUOTA_PACKAGE,
1239
+ descriptors: [
1240
+ {
1241
+ id: `${AGY_QUOTA_PACKAGE}#${AGY_PROXY_ACCOUNT_SERVICE}/account`,
1242
+ service: AGY_PROXY_ACCOUNT_SERVICE,
1243
+ namespace: AGY_PROXY_ACCOUNT_SERVICE,
1244
+ method: 'account',
1245
+ invocation: { kind: 'direct' },
1246
+ parameters: [{ name: 'request', wire: 'request', source: 'json', acceptsUndefined: true, codec: strictCodec(`${AGY_QUOTA_PACKAGE}#AgYProxyAccountRequest`, parseProxyAccountRequest) }],
1247
+ result: strictCodec(`${AGY_QUOTA_PACKAGE}#AgYProxyAccountResult`, parseProxyAccountResult),
1248
+ },
1249
+ ],
1250
+ }
1251
+
945
1252
  return {
946
1253
  name: 'dsh-bundle-antigravity-host',
947
1254
  // remote.settings / remote.llm 必须逐个声明:remote 各命名空间是独立服务,
948
1255
  // 漏声明会在读取属性时抛「without inject」并击穿 settings.section 卡片。
949
1256
  inject: ['slots', 'remote', 'remote.settings', 'remote.llm'],
950
1257
  apply(ctx) {
1258
+ ctx.effect(() => {
1259
+ if (typeof document === 'undefined' || !document.head || !document.createElement) return () => {}
1260
+ const style = document.createElement('style')
1261
+ style.textContent = '.antigravity-usage-pill>button:hover{filter:brightness(1.08)}.antigravity-usage-pill button:focus-visible,.antigravity-settings-card button:focus-visible,.antigravity-settings-card a:focus-visible{outline:2px solid #4a90e2;outline-offset:3px}.antigravity-settings-card button:disabled{cursor:not-allowed;opacity:.55}'
1262
+ document.head.appendChild(style)
1263
+ return () => style.remove()
1264
+ }, 'antigravity: scoped control styles')
951
1265
  const readQuota = makeQuotaReader(ctx)
952
1266
  const control = makeControlReader(ctx)
1267
+ const proxyControl = makeProxyControlReader(ctx)
953
1268
  // 挂载本包贡献的 remote(幂等,随 fiber 一起 dispose)。挂载失败不影响其它卡片:
954
1269
  // 药丸会自行超时并显示「额度服务未就绪」的原因。
955
1270
  try {
956
- const mounted = ctx.remote.$mount(AGY_QUOTA_REMOTE)
1271
+ const mounted = ctx.remote.$mount({
1272
+ package: AGY_QUOTA_PACKAGE,
1273
+ descriptors: [...AGY_QUOTA_REMOTE.descriptors, ...AGY_PROXY_REMOTE.descriptors],
1274
+ })
957
1275
  ctx.effect(() => mounted, 'antigravity-acp: mount quota remote')
958
1276
  } catch (error) {
959
1277
  console.error('[dsh-bundle-antigravity-host] 额度 remote 客户端挂载失败:', error)
@@ -967,7 +1285,7 @@ window.__ModuleLoader__.load({
967
1285
  id: 'antigravity-acp',
968
1286
  order: 42,
969
1287
  label: () => 'Antigravity ACP',
970
- inject: () => ({ remote: ctx.remote, control }),
1288
+ inject: () => ({ remote: ctx.remote, control, proxyControl, readQuota }),
971
1289
  },
972
1290
  AntigravitySettingsCard,
973
1291
  ),
@@ -995,7 +1313,7 @@ window.__ModuleLoader__.load({
995
1313
  inject: (sessionId) => ({
996
1314
  store: scope.modelDirectories.directoryFor(sessionId).store,
997
1315
  readQuota,
998
- control,
1316
+ proxyControl,
999
1317
  }),
1000
1318
  },
1001
1319
  AgYQuotaPill,