dsh-bundle-antigravity-host 0.1.27 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -23,20 +23,20 @@ window.__ModuleLoader__.load({
23
23
  const React = require('react')
24
24
  const h = React.createElement
25
25
  const PROVIDER = 'antigravity-acp'
26
- /** Settings namespace injected by `settings.configure({ auto: true })` in src/index.ts. */
26
+ /** Settings namespace projected from Config's volatile fields in src/settings.ts. */
27
27
  const ENTRY_ID = 'llm-antigravity-acp'
28
- /** Editable fields surfaced in this card; everything else stays in the host form. */
28
+ /** Live fields owned by this card; ordinary fields stay in the host config editor. */
29
29
  const FIELDS = [
30
- { key: 'autoWarmup', checkbox: true, defaultChecked: true, label: '启动时自动预热 ACP', hint: '提前启动并保留连接,不发送模型消息。保存后下次启用插件生效;本次可点击“立即预热”。' },
31
- { key: 'reuseSessions', checkbox: true, defaultChecked: true, label: '复用 ACP 会话(增量发送)', hint: '校验历史与实际输出后只发新增消息;变化的上下文仍发送,不一致时自动重建。' },
32
- { key: 'resumeSessions', checkbox: true, label: '重启后恢复 ACP 会话', hint: '尝试加载本地映射,失败则重建;仅保存 ID 与指纹。需要开启会话复用。' },
33
- { key: 'keepProcessAfterCancel', checkbox: true, label: '取消后保留已停止的进程', hint: '仅在 agent 确认停止后保留;等待期间新轮排队,超时或通信失败回收。' },
34
- { key: 'cancelGraceMs', numeric: true, defaultValue: 30000, min: 0, max: 60000, label: '取消确认宽限(毫秒)', hint: '默认 30000;0 表示不等待并立即回收,不会复用未确认连接。' },
35
- { key: 'idleRecycleMs', numeric: true, defaultValue: 300000, min: 0, max: 3600000, label: '进程空闲回收(毫秒)', hint: '默认 300000(5 分钟),0 禁用。聊天、预热、目录发现结束后统一计时;等待工具不算空闲。' },
36
- { key: 'preferredModel', label: '默认 / 自定义模型 ID', placeholder: '选择目录中的模型,或输入自定义 ID', hint: '保存后优先列出此模型;目录之外的 ID 标为自定义、未验证。不会替换当前会话已选模型。留空不指定默认项。' },
37
- { key: 'proxyUrl', label: '出网代理', placeholder: 'http://127.0.0.1:7890', hint: '此处配置优先;留空则继承宿主代理环境变量,宿主也未配置时才直连。' },
38
- { key: 'proxyBypass', label: '代理排除列表', placeholder: '10.0.0.0/8,*.internal', hint: 'localhost/127.0.0.1/::1 始终自动排除(私有 MCP 桥在 loopback)。' },
39
- { key: 'agyCliPath', label: '官方 CLI 路径(可选)', placeholder: '留空自动查找 agy', hint: '用于模型目录、套餐额度和 AI Credits。自定义安装位置可填写完整可执行文件路径;CLI 登录与 ACP 登录独立。' },
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: 'preferredModel', group: 'model', label: '默认 / 自定义模型 ID', placeholder: '选择目录中的模型,或输入自定义 ID', hint: '保存后优先列出此模型;目录之外的 ID 标为自定义、未验证。不会替换当前会话已选模型。留空不指定默认项。' },
33
+ { key: 'proxyUrl', group: 'network', label: '出网代理', placeholder: 'http://127.0.0.1:7890', hint: '此处配置优先;留空则继承宿主代理环境变量,宿主也未配置时才直连。支持 HTTP、HTTPS 与 SOCKS5。' },
34
+ { key: 'proxyBypass', group: 'network', label: '代理排除列表', placeholder: '*.internal,example.internal', hint: '逗号分隔,支持域名与通配后缀,不支持 CIDR。localhost/127.0.0.1/::1 始终自动排除(私有 MCP 桥在 loopback)。' },
35
+ { key: 'agyCliPath', group: 'network', label: '官方 CLI 路径(可选)', placeholder: '留空自动查找 agy', hint: '用于模型目录、套餐额度和 AI Credits。自定义安装位置可填写完整可执行文件路径;CLI 登录与 ACP 登录独立。' },
36
+ { key: 'resumeSessions', group: 'advanced', checkbox: true, label: '重启后恢复 ACP 会话', hint: '尝试加载本地映射,失败则重建;仅保存 ID 与指纹。需要同时开启会话复用。' },
37
+ { key: 'keepProcessAfterCancel', group: 'advanced', checkbox: true, label: '取消后保留已停止的进程', hint: '仅在 agent 确认停止后保留;等待期间新轮排队,超时或通信失败回收。' },
38
+ { key: 'cancelGraceMs', group: 'advanced', numeric: true, defaultValue: 30000, min: 0, max: 60000, label: '取消确认宽限(毫秒)', hint: '默认 30000;0 表示不等待并立即回收,不会复用未确认连接。仅开启取消后保留进程时生效。' },
39
+ { key: 'idleRecycleMs', group: 'advanced', numeric: true, defaultValue: 300000, min: 0, max: 3600000, label: '进程空闲回收(毫秒)', hint: '默认 300000(5 分钟),0 禁用。聊天、预热、目录发现结束后统一计时;等待工具不算空闲。' },
40
40
  ]
41
41
 
42
42
  function unwrap(result) {
@@ -71,13 +71,40 @@ window.__ModuleLoader__.load({
71
71
  const slow = ['spawn', 'initialize'].includes(runtime.phase) ? ';官方进程冷启动首次可能需要约 1 分钟,请耐心等待' : ''
72
72
  return (RUNTIME_PHASES[runtime.phase] ?? runtime.phase) + '… 已用 ' + duration(runtime.phaseElapsedMs) + slow
73
73
  }
74
+ function connectionStatus(account, registered) {
75
+ const runtime = account?.runtime
76
+ const ready = runtime?.phase === 'ready'
77
+ const busy = runtimeBusy(runtime) || ['connecting', 'cancelling'].includes(account?.state)
78
+ const error = account?.state === 'error' || runtime?.phase === 'error' || registered === false
79
+ const label = registered === false ? 'Provider 未注册'
80
+ : error ? '连接需要检查'
81
+ : busy ? account?.state === 'cancelling' ? '正在取消连接' : '正在准备连接'
82
+ : ready ? account?.state === 'connected' ? '账户已验证' : 'ACP 已预热'
83
+ : account?.state === 'cancelled' || runtime?.phase === 'stopped' ? '连接已停止'
84
+ : account?.state === 'connected' ? '账户已验证' : '尚未验证连接'
85
+ const description = account?.runtime?.phase === 'stopped'
86
+ ? 'ACP 连接已停止,可以重新预热。'
87
+ : account?.runtime?.phase === 'error'
88
+ ? account.runtime.error ?? account?.message ?? 'ACP 连接异常,请重试。'
89
+ : registered === false ? '请确认插件已启用,Host 与 Client 均已加载。'
90
+ : startupHint(account) ?? account?.message ?? '连接后可验证账户并读取模型目录,不会发送聊天消息。'
91
+ return { label, description, ready, busy, error, color: error ? '#d97757' : ready ? '#25b879' : busy ? '#d99b28' : '#8b909a' }
92
+ }
93
+ function StatusDot({ color }) {
94
+ return h('span', { 'aria-hidden': true, style: { width: '8px', height: '8px', flex: '0 0 auto', borderRadius: '50%', background: color, boxShadow: '0 0 0 4px rgba(128,128,128,.10)' } })
95
+ }
96
+ function StatusNotice({ status }) {
97
+ if (!status) return null
98
+ const error = status.kind === 'error'
99
+ 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)
100
+ }
74
101
  function RuntimeDetails({ runtime }) {
75
102
  if (!runtime) return null
76
- const row = (label, value) => h('div', { key: label, style: { display: 'flex', justifyContent: 'space-between', gap: '12px' } },
77
- h('span', { style: { opacity: .65 } }, label), h('span', { style: { fontVariantNumeric: 'tabular-nums', overflowWrap: 'anywhere' } }, value))
103
+ const row = (label, value) => h('div', { key: label, style: { display: 'flex', justifyContent: 'space-between', gap: '16px' } },
104
+ h('span', { style: { opacity: .65 } }, label), h('span', { style: { textAlign: 'right', fontVariantNumeric: 'tabular-nums', overflowWrap: 'anywhere' } }, value))
78
105
  const round = runtime.round
79
106
  const queue = runtime.queue
80
- return h('div', { 'aria-label': 'ACP 生命周期与计时', style: { marginTop: '12px', paddingTop: '10px', borderTop: '1px solid #8883', display: 'grid', gap: '6px', fontSize: '12px' } },
107
+ return h('div', { 'aria-label': 'ACP 生命周期与计时', style: { display: 'grid', gap: '8px', fontSize: '12px', lineHeight: 1.5 } },
81
108
  row('当前状态', (RUNTIME_PHASES[runtime.phase] ?? runtime.phase) + ' · ' + duration(runtime.phaseElapsedMs)),
82
109
  queue ? row('会话调度', '运行 ' + queue.active + ' · 等待工具 ' + queue.parked + ' · 排队 ' + queue.waiting) : null,
83
110
  queue ? row('并发连接', queue.workers + ' / ' + queue.capacity) : null,
@@ -90,12 +117,48 @@ window.__ModuleLoader__.load({
90
117
  round?.firstChunkMs !== undefined ? row('发送 prompt → 首块', duration(round.firstChunkMs)) : null,
91
118
  round?.firstToolMs !== undefined ? row('发送 prompt → 首个工具', duration(round.firstToolMs)) : null,
92
119
  round?.continuations ? row('工具续轮', round.continuations + ' 次 · 最近 ' + duration(round.lastContinuationMs)) : null,
93
- runtime.error ? h('p', { role: 'alert', style: { margin: '4px 0 0', color: '#e5534b', overflowWrap: 'anywhere' } }, runtime.error) : null)
120
+ runtime.error ? h('p', { role: 'alert', style: { margin: '4px 0 0', color: '#b42318', overflowWrap: 'anywhere' } }, runtime.error) : null)
121
+ }
122
+ function SectionHeading({ eyebrow, title, description, action }) {
123
+ return h('div', { style: { display: 'flex', alignItems: 'flex-start', gap: '12px', marginBottom: '16px' } },
124
+ h('div', { style: { flex: 1, minWidth: 0 } },
125
+ eyebrow ? h('div', { style: { fontSize: '10px', fontWeight: 650, letterSpacing: '.08em', opacity: .5, marginBottom: '4px' } }, eyebrow) : null,
126
+ h('h3', { style: { fontSize: '14px', lineHeight: 1.4, fontWeight: 650, margin: 0 } }, title),
127
+ description ? h('p', { style: { fontSize: '12px', lineHeight: 1.6, opacity: .6, margin: '5px 0 0' } }, description) : null),
128
+ action ?? null)
129
+ }
130
+ function TextField({ field, value, disabled, invalid, onChange }) {
131
+ const inputId = 'agy-setting-' + (React.useId?.() ?? field.key) + '-' + field.key
132
+ return h('div', { style: { display: 'grid', gap: '6px' } },
133
+ h('label', { htmlFor: inputId, style: { fontSize: '12px', fontWeight: 550 } }, field.label),
134
+ h('input', {
135
+ id: inputId, name: field.key, type: field.numeric ? 'number' : 'text',
136
+ ...(field.numeric ? { min: field.min, max: field.max, step: 1 } : {}),
137
+ disabled, value: value ?? '', placeholder: field.placeholder,
138
+ 'aria-invalid': invalid || undefined, 'aria-describedby': inputId + '-hint',
139
+ style: { ...ACTION, cursor: disabled ? 'not-allowed' : 'text', width: '100%', boxSizing: 'border-box', background: 'rgba(128,128,128,.035)', opacity: disabled ? .6 : 1 },
140
+ onChange: (event) => onChange(field.numeric && event.target.value !== '' ? Number(event.target.value) : event.target.value),
141
+ }),
142
+ h('small', { id: inputId + '-hint', style: { fontSize: '11px', lineHeight: 1.6, opacity: .6 } }, field.hint))
143
+ }
144
+ function SwitchField({ field, checked, disabled, onChange }) {
145
+ return h('label', { style: { display: 'flex', alignItems: 'flex-start', gap: '10px', cursor: disabled ? 'default' : 'pointer', padding: '2px 0', opacity: disabled ? .6 : 1 } },
146
+ 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' } }),
147
+ h('span', { style: { display: 'grid', gap: '4px', minWidth: 0 } },
148
+ h('span', { style: { fontSize: '12px', fontWeight: 550 } }, field.label),
149
+ h('small', { style: { fontSize: '11px', lineHeight: 1.6, opacity: .65 } }, field.hint)))
150
+ }
151
+ function SettingsFields({ fields, draft, dirtyKeys, conflicts, disabled, editField }) {
152
+ return h('div', { style: { display: 'grid', gap: '16px' } }, ...fields.map((field) => {
153
+ const onChange = (value) => editField(field.key, value)
154
+ return field.checkbox
155
+ ? h(SwitchField, { key: field.key, field, checked: draft[field.key], disabled, onChange })
156
+ : h(TextField, { key: field.key, field, value: draft[field.key], disabled, invalid: dirtyKeys.includes(field.key) && field.numeric && !Number.isSafeInteger(draft[field.key]), onChange })
157
+ }))
94
158
  }
95
159
 
96
160
  function AntigravitySettingsCard(props) {
97
161
  const remote = props.remote
98
- const quota = useQuota(props.readQuota)
99
162
  const [account, setAccount] = React.useState(undefined)
100
163
  const [models, setModels] = React.useState([])
101
164
  const [modelBusy, setModelBusy] = React.useState(false)
@@ -217,7 +280,8 @@ window.__ModuleLoader__.load({
217
280
  setConflicts((previous) => [...new Set([...previous.filter((key) => dirty.current.has(key)), ...changed])])
218
281
  setWritable(settings?.writable === true)
219
282
  setRegistered(rows.some((row) => row?.id === PROVIDER))
220
- if (!preserveStatus) setStatus(null)
283
+ if (!found) setStatus({ kind: 'error', text: '宿主尚未提供 Antigravity ACP 的可编辑设置。请确认 Host 与 Client 插件版本一致,并重新加载插件后刷新配置。' })
284
+ else if (!preserveStatus) setStatus(null)
221
285
  } catch (cause) {
222
286
  if (alive.current && version === settingsVersion.current) setStatus({ kind: 'error', text: cause instanceof Error ? cause.message : String(cause) })
223
287
  } finally {
@@ -247,7 +311,7 @@ window.__ModuleLoader__.load({
247
311
  }
248
312
  setDirtyKeys([...dirty.current])
249
313
  setConflicts([])
250
- setStatus({ kind: 'ok', text: '已保存。重新加载插件可应用运行配置;当前对话中的已选模型保持不变。' })
314
+ setStatus({ kind: 'ok', text: '已保存。会话偏好从下一轮生效;代理用于后续请求及新启动的 ACP 进程,已有进程需重新加载插件才能切换代理。自动预热在下次启用插件时生效;当前已选模型保持不变。' })
251
315
  await reload(true)
252
316
  } catch (cause) {
253
317
  if (!alive.current) return
@@ -291,73 +355,85 @@ window.__ModuleLoader__.load({
291
355
  setStatus({ kind: 'ok', text: '已撤销未保存的修改,恢复为当前宿主设置。' })
292
356
  }
293
357
 
294
- return h(
295
- 'div',
296
- { style: { padding: '16px', display: 'flex', flexDirection: 'column', gap: '12px', maxWidth: '40rem' } },
297
- h('h2', { style: { margin: 0, fontSize: '1.1rem' } }, 'Antigravity ACP'),
298
- h('p', { style: { margin: 0, opacity: 0.8 } }, 'Provider 路由:', h('code', null, PROVIDER)),
299
- h(
300
- 'p',
301
- { style: { margin: 0, opacity: 0.8 } },
302
- registered === true
303
- ? 'Provider 已注册,可在下方连接账户、拉取模型和查看额度。'
304
- : registered === false
305
- ? 'Provider 尚未注册,请检查插件是否已启用。'
306
- : '正在确认路由注册状态…',
307
- ),
308
- h('section', { style: CARD, 'aria-label': '账户连接' },
309
- h('h3', { style: { margin: '0 0 8px', fontSize: '14px' } }, '账户 · ACP 连接'),
310
- h('p', { role: 'status', style: { fontSize: '12px', margin: '0 0 10px' } }, account?.runtime?.phase === 'stopped'
311
- ? 'ACP 连接已停止,可以重新预热。' : account?.runtime?.phase === 'error'
312
- ? account.runtime.error ?? account?.message ?? 'ACP 连接异常,请重试。' : startupHint(account) ?? account?.message ?? '尚未验证 ACP 连接'),
358
+ const connection = connectionStatus(account, registered)
359
+ const fieldsFor = (group) => FIELDS.filter((field) => field.group === group)
360
+ const fieldProps = { draft, dirtyKeys, conflicts, disabled: editingDisabled, editField }
361
+ const busy = ['connecting', 'cancelling'].includes(account?.state) || runtimeBusy(account?.runtime)
362
+ const readonly = !loading && view && !writable
363
+ const advancedSummary = h('summary', { style: SUMMARY },
364
+ h('span', { style: { display: 'grid', gap: '4px' } },
365
+ h('span', { style: { fontSize: '13px', fontWeight: 650 } }, '高级设置'),
366
+ h('small', { style: { fontSize: '11px', fontWeight: 400, opacity: .6 } }, '网络、CLI、会话恢复与进程生命周期')))
367
+
368
+ return h('div', { style: { padding: '20px', display: 'flex', flexDirection: 'column', gap: '14px', maxWidth: '720px', boxSizing: 'border-box', lineHeight: 1.5 } },
369
+ h('header', { style: { display: 'flex', alignItems: 'flex-start', gap: '12px', padding: '0 2px 2px' } },
370
+ h('div', { style: { flex: 1, minWidth: 0 } },
371
+ h('div', { style: { fontSize: '10px', fontWeight: 650, letterSpacing: '.1em', opacity: .5, marginBottom: '5px' } }, 'MODEL PROVIDER'),
372
+ h('h2', { style: { margin: 0, fontSize: '20px', lineHeight: 1.35, fontWeight: 650, letterSpacing: '-.015em' } }, 'Antigravity ACP'),
373
+ h('p', { style: { margin: '6px 0 0', fontSize: '12px', opacity: .6 } }, '连接官方 Antigravity,模型与工具仍由 DSH 管理。')),
374
+ 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 已注册' : '正在检查路由')),
375
+
376
+ readonly ? h(StatusNotice, { status: { kind: 'busy', text: '当前设置为只读。你仍可查看连接状态与诊断;请在有写入权限的宿主中修改配置。' } }) : null,
377
+
378
+ h('section', { style: PANEL, 'aria-label': '账户连接' },
379
+ h(SectionHeading, { eyebrow: '01 · 连接', title: '账户与连接', description: '先预热减少等待,或验证账户并读取模型目录。' }),
380
+ h('div', { style: { display: 'flex', alignItems: 'flex-start', gap: '10px', padding: '12px', borderRadius: '10px', background: 'rgba(128,128,128,.045)', marginBottom: '14px' } },
381
+ h(StatusDot, { color: connection.color }),
382
+ h('div', { style: { minWidth: 0 } },
383
+ h('div', { style: { fontSize: '12px', fontWeight: 650, color: connection.color } }, connection.label),
384
+ h('p', { role: 'status', style: { fontSize: '12px', lineHeight: 1.6, opacity: .7, overflowWrap: 'anywhere', margin: '3px 0 0' } }, connection.description))),
313
385
  h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '8px' } },
314
- h('button', { type: 'button', style: ACTION, disabled: !props.control || ['connecting', 'cancelling'].includes(account?.state) || runtimeBusy(account?.runtime) || account?.runtime?.phase === 'ready', onClick: () => accountAction('warmup') }, account?.runtime?.phase === 'ready' ? '连接已预热' : '立即预热'),
315
- h('button', { type: 'button', style: ACTION, disabled: !props.control || ['connecting', 'cancelling'].includes(account?.state) || runtimeBusy(account?.runtime), onClick: () => accountAction('login') }, '连接 / 登录 ACP'),
316
- ['connecting', 'cancelling'].includes(account?.state) ? h('button', { type: 'button', style: ACTION, disabled: account?.state === 'cancelling', onClick: () => accountAction('cancel') }, '取消连接') : null),
317
- h('p', { style: { opacity: .6, fontSize: '11px', margin: '10px 0 0' } }, '预热准备连接;连接 / 登录 ACP 还会验证会话并读取模型目录。两者均不发送聊天消息,仅登录按钮会在需要时打开官方授权页面。'),
318
- RuntimeDetails({ runtime: account?.runtime })),
319
- h('section', { style: CARD, 'aria-label': '模型管理' },
320
- h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
321
- h('h3', { style: { fontSize: '14px', margin: 0, flex: 1 } }, '模型管理'),
322
- h('button', { type: 'button', style: ACTION, disabled: modelBusy || !props.control, onClick: () => pullModels(true) }, modelBusy ? '拉取中…' : '一键拉取模型')),
323
- h('p', { role: 'status', style: { fontSize: '12px', opacity: .7 } }, modelMessage || '可通过 ACP 会话验证读取目录,也可从官方 CLI 拉取。'),
324
- h('label', { style: { display: 'grid', gap: '6px' } }, '从目录选择默认模型',
325
- h('select', { 'aria-label': '从目录选择默认模型', disabled: editingDisabled, value: models.some((m) => m.id === draft.preferredModel) ? draft.preferredModel : '', onChange: (event) => { if (event.target.value) editField('preferredModel', event.target.value) }, style: { ...ACTION, width: '100%' } },
326
- h('option', { value: '' }, models.length ? '选择一个模型,或在下方手填 ID' : '暂无目录,可手填模型 ID'),
327
- ...models.map((model) => h('option', { key: model.id, value: model.id }, model.name + ' · ' + model.id)))),
328
- h('small', { style: { display: 'block', marginTop: '8px', opacity: .6 } }, models.length + ' 个模型 · 选择后点击保存。思考强度仍由会话菜单设置。'),
329
- Array.isArray(selectedModalities) && selectedModalities.length ? h('small', { style: { display: 'block', marginTop: '6px', opacity: .6 } }, '当前目录支持的输入:' + selectedModalities.map((modality) => modalityLabels[modality] ?? modality).join('、')) : null),
330
- ...FIELDS.map((field) => h(
331
- 'label',
332
- { key: field.key, style: { display: 'flex', flexDirection: 'column', gap: '4px' } },
333
- h('span', null, field.label),
334
- h('input', {
335
- name: field.key,
336
- type: field.checkbox ? 'checkbox' : field.numeric ? 'number' : 'text',
337
- ...(field.numeric ? { min: field.min, max: field.max, step: 1 } : {}),
338
- disabled: editingDisabled,
339
- ...(field.checkbox ? { checked: draft[field.key] === true } : { value: draft[field.key] ?? '' }),
340
- placeholder: field.placeholder,
341
- style: field.checkbox ? { alignSelf: 'flex-start' } : { ...ACTION, cursor: 'text', width: '100%', boxSizing: 'border-box' },
342
- onChange: (event) => editField(field.key, field.checkbox ? event.target.checked
343
- : field.numeric && event.target.value !== '' ? Number(event.target.value) : event.target.value),
344
- }),
345
- h('small', { style: { opacity: 0.7 } }, field.hint),
346
- )),
347
- h(
348
- 'div',
349
- { style: { display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center' } },
350
- h('button', { type: 'button', onClick: save, disabled: editingDisabled || dirtyKeys.length === 0, style: ACTION }, saving ? '保存中…' : '保存'),
351
- h('button', { type: 'button', disabled: loading || saving, onClick: () => reload(), style: ACTION }, loading ? '刷新中…' : '刷新配置'),
352
- dirtyKeys.length ? h('button', { type: 'button', disabled: loading || saving, onClick: discardDraft, style: ACTION }, '撤销未保存修改') : null,
353
- ),
354
- !loading && view && !writable ? h('p', { role: 'status', style: { margin: 0, fontSize: '12px', opacity: .7 } }, '当前设置为只读,请在有写入权限的宿主中修改。') : null,
355
- dirtyKeys.length ? h('p', { role: 'status', style: { margin: 0, fontSize: '12px', opacity: .7 } }, '有 ' + dirtyKeys.length + ' 项未保存的修改;刷新会保留这些输入。') : null,
356
- conflicts.length ? h('p', { role: 'alert', style: { margin: 0, fontSize: '12px', color: '#d99b28' } }, '其他位置更新了以下设置,已保留你的输入:' + conflicts.map((key) => FIELDS.find((field) => field.key === key)?.label ?? key).join('、') + '。请核对后再保存。') : null,
357
- status ? h('p', { role: status.kind === 'error' ? 'alert' : 'status', style: { margin: 0, color: status.kind === 'error' ? '#e5534b' : undefined } }, status.text) : null,
358
- h('section', { style: { marginTop: '8px' }, 'aria-label': '套餐额度管理' }, QuotaDetails({ ...quota, model: draft.preferredModel })),
386
+ h('button', { type: 'button', style: PRIMARY, disabled: !props.control || busy, onClick: () => accountAction('login') }, account?.state === 'connecting' ? '正在连接…' : '连接 / 登录 ACP'),
387
+ h('button', { type: 'button', style: ACTION, disabled: !props.control || busy || connection.ready, onClick: () => accountAction('warmup') }, connection.ready ? '已预热' : '立即预热'),
388
+ ['connecting', 'cancelling'].includes(account?.state) ? h('button', { type: 'button', style: ACTION, disabled: account?.state === 'cancelling', onClick: () => accountAction('cancel') }, account?.state === 'cancelling' ? '正在取消…' : '取消连接') : null),
389
+ h('p', { style: { opacity: .55, fontSize: '11px', lineHeight: 1.6, margin: '10px 0 0' } }, '预热不会发送聊天消息。仅在需要登录时打开官方授权页面;思考强度仍由会话菜单设置。')),
390
+
391
+ h('section', { style: PANEL, 'aria-label': '模型管理' },
392
+ h(SectionHeading, { eyebrow: '02 · 模型', title: '默认模型', description: '选择新对话优先使用的模型,不改变当前会话。',
393
+ 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 ? '正在刷新…' : '刷新模型目录') }),
394
+ h('div', { style: { display: 'grid', gap: '14px' } },
395
+ h('label', { style: { display: 'grid', gap: '6px', fontSize: '12px', fontWeight: 550 } }, '从模型目录选择',
396
+ h('select', { 'aria-label': '从目录选择默认模型', disabled: editingDisabled, value: models.some((m) => m.id === draft.preferredModel) ? draft.preferredModel : '', onChange: (event) => { if (event.target.value) editField('preferredModel', event.target.value) }, style: { ...ACTION, width: '100%', boxSizing: 'border-box', background: 'rgba(128,128,128,.035)' } },
397
+ h('option', { value: '' }, models.length ? '选择一个模型,或在下方输入 ID' : '暂无目录,可手动输入模型 ID'),
398
+ ...models.map((model) => h('option', { key: model.id, value: model.id }, model.name + ' · ' + model.id)))),
399
+ h(SettingsFields, { ...fieldProps, fields: fieldsFor('model') })),
400
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '5px 10px', marginTop: '10px', fontSize: '11px', opacity: .6 } },
401
+ h('span', null, models.length + ' 个模型'),
402
+ Array.isArray(selectedModalities) && selectedModalities.length ? h('span', null, '支持输入:' + selectedModalities.map((modality) => modalityLabels[modality] ?? modality).join('、')) : null,
403
+ draft.preferredModel && !selectedModel ? h('span', null, '自定义 ID · 尚未验证') : null),
404
+ modelMessage ? h('p', { role: 'status', style: { fontSize: '11px', lineHeight: 1.6, opacity: .65, margin: '8px 0 0', overflowWrap: 'anywhere' } }, modelMessage) : null),
359
405
 
360
- )
406
+ h('section', { style: PANEL, 'aria-label': '常用设置' },
407
+ h(SectionHeading, { eyebrow: '03 · 偏好', title: '常用设置', description: '适合大多数使用场景;其它选项保持默认即可。' }),
408
+ h(SettingsFields, { ...fieldProps, fields: fieldsFor('general') })),
409
+
410
+ h('details', { style: PANEL, 'aria-label': '高级设置' }, advancedSummary,
411
+ h('div', { style: { display: 'grid', gap: '24px', marginTop: '20px', paddingTop: '18px', borderTop: '1px solid rgba(128,128,128,.14)' } },
412
+ h('section', { 'aria-label': '网络与 CLI 设置', style: { display: 'grid', gap: '16px' } },
413
+ h('h4', { style: { margin: 0, fontSize: '12px', fontWeight: 650 } }, '网络与 CLI'),
414
+ h(SettingsFields, { ...fieldProps, fields: fieldsFor('network') })),
415
+ h('section', { 'aria-label': '会话与进程设置', style: { display: 'grid', gap: '16px' } },
416
+ h('h4', { style: { margin: 0, fontSize: '12px', fontWeight: 650 } }, '会话与进程'),
417
+ h(SettingsFields, { ...fieldProps, fields: fieldsFor('advanced') })))),
418
+
419
+ h('details', { style: PANEL, 'aria-label': '连接诊断' },
420
+ h('summary', { style: SUMMARY },
421
+ h('span', { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
422
+ h(StatusDot, { color: connection.color }),
423
+ h('span', { style: { fontSize: '13px', fontWeight: 650 } }, '连接诊断')),
424
+ h('small', { style: { fontSize: '11px', opacity: .6 } }, '状态、并发与耗时')),
425
+ h('div', { style: { marginTop: '18px', paddingTop: '16px', borderTop: '1px solid rgba(128,128,128,.14)' } },
426
+ account?.runtime ? RuntimeDetails({ runtime: account.runtime }) : h('p', { style: { margin: 0, fontSize: '12px', opacity: .6 } }, '连接后显示生命周期、并发容量与分阶段耗时。'))),
427
+
428
+ h('footer', { style: { display: 'grid', gap: '10px', padding: '4px 2px 0' } },
429
+ conflicts.length ? h('p', { role: 'alert', style: { margin: 0, fontSize: '12px', lineHeight: 1.6, color: '#946200' } }, '其他位置更新了以下设置,已保留你的输入:' + conflicts.map((key) => FIELDS.find((field) => field.key === key)?.label ?? key).join('、') + '。请核对后再保存。') : null,
430
+ h(StatusNotice, { status }),
431
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center' } },
432
+ h('button', { type: 'button', onClick: save, disabled: editingDisabled || dirtyKeys.length === 0, style: PRIMARY }, saving ? '保存中…' : '保存'),
433
+ dirtyKeys.length ? h('button', { type: 'button', disabled: loading || saving, onClick: discardDraft, style: ACTION }, '撤销未保存修改') : null,
434
+ h('button', { type: 'button', title: '重新读取宿主配置,保留未保存的输入', disabled: loading || saving, onClick: () => reload(), style: { ...ACTION, borderColor: 'transparent', background: 'transparent', opacity: .7, marginLeft: 'auto' } }, loading ? '刷新中…' : '刷新配置')),
435
+ dirtyKeys.length ? h('p', { role: 'status', style: { margin: 0, fontSize: '11px', opacity: .65 } }, '有 ' + dirtyKeys.length + ' 项未保存的修改;刷新会保留这些输入。') : null,
436
+ 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 请在对话输入框旁的额度药丸中查看,此处不再重复展示。')))
361
437
  }
362
438
 
363
439
  /**
@@ -369,7 +445,10 @@ window.__ModuleLoader__.load({
369
445
 
370
446
  const TRACK = 'rgba(128,128,128,.18)'
371
447
  const CARD = { padding: '12px', borderRadius: '12px', border: '1px solid rgba(128,128,128,.18)', background: 'rgba(128,128,128,.05)' }
372
- const ACTION = { padding: '6px 12px', borderRadius: '8px', border: '1px solid rgba(128,128,128,.25)', background: 'rgba(128,128,128,.08)', color: 'inherit', font: 'inherit', cursor: 'pointer' }
448
+ const PANEL = { padding: '18px', borderRadius: '14px', border: '1px solid rgba(128,128,128,.16)', background: 'rgba(128,128,128,.025)' }
449
+ const SUMMARY = { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px', cursor: 'pointer', listStyle: 'none' }
450
+ 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' }
451
+ const PRIMARY = { ...ACTION, background: '#177c55', border: '1px solid #177c55', color: '#fff', fontWeight: 600 }
373
452
  const GROUPS = [{ key: 'gemini', name: 'Gemini' }, { key: 'claude', name: 'Claude / GPT' }, { key: 'argon', name: 'Gemini 4 Argon' }]
374
453
  function groupKey(name) {
375
454
  const text = String(name ?? '').toLowerCase()
@@ -426,23 +505,29 @@ window.__ModuleLoader__.load({
426
505
  const [data, setData] = React.useState(null)
427
506
  const [refreshing, setRefreshing] = React.useState(false)
428
507
  const version = React.useRef(0)
508
+ const update = React.useCallback((value) => setData(value), [])
429
509
  const load = React.useCallback((force = false) => {
430
510
  if (!enabled || !readQuota) return Promise.resolve()
431
511
  const current = ++version.current
432
512
  setRefreshing(true)
433
513
  return Promise.resolve().then(() => readQuota(force)).then(unwrap).then((value) => {
434
- if (current === version.current) setData(value)
514
+ if (current === version.current) update(value)
435
515
  }).catch((error) => {
436
- if (current === version.current) setData({ status: 'error', groups: [], error: { message: error instanceof Error ? error.message : String(error) } })
516
+ if (current === version.current) {
517
+ const message = error instanceof Error ? error.message : String(error)
518
+ setData((previous) => previous?.status === 'ok'
519
+ ? { ...previous, stale: true, staleReason: message }
520
+ : { status: 'error', groups: [], error: { message } })
521
+ }
437
522
  }).finally(() => { if (current === version.current) setRefreshing(false) })
438
- }, [readQuota, enabled])
523
+ }, [readQuota, enabled, update])
439
524
  React.useEffect(() => {
440
525
  if (!enabled || !readQuota) { setData(null); return undefined }
441
526
  void load(false)
442
- const off = readQuota.subscribe?.((value) => setData(value))
527
+ const off = readQuota.subscribe?.((value) => { ++version.current; update(value) })
443
528
  const timer = setInterval(() => { if (typeof document === 'undefined' || document.visibilityState !== 'hidden') void load(false) }, 300_000)
444
529
  return () => { ++version.current; clearInterval(timer); off?.() }
445
- }, [load, enabled, readQuota])
530
+ }, [load, enabled, readQuota, update])
446
531
  return { data, refreshing, load }
447
532
  }
448
533
  function CreditsDetails({ credits, pending }) {
@@ -552,7 +637,12 @@ window.__ModuleLoader__.load({
552
637
  return () => { document.removeEventListener('pointerdown', outside); document.removeEventListener('keydown', key); window.removeEventListener('resize', reposition); window.removeEventListener('scroll', reposition, true) }
553
638
  }, [expanded])
554
639
  if (!enabled) return null
555
- const group = quotaGroups(quota.data).find((g) => g.key === modelGroup(model))
640
+ const groups = quotaGroups(quota.data)
641
+ const family = modelGroup(model)
642
+ // 官方 /usage 可能只报告 Gemini 共享额度;只有真实 Argon 分组存在时,
643
+ // 才把 Argon 型号绑定到独立组,不能命中一个预建的空组而误报未知。
644
+ const group = groups.find((g) => g.key === family && (family !== 'argon' || g.buckets.length > 0))
645
+ ?? (family === 'argon' ? groups.find((g) => g.key === 'gemini') : undefined)
556
646
  const buckets = group?.buckets ?? []
557
647
  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(' · ')
558
648
  const title = String(model ?? '未选择模型') + ' · ' + label + (quota.data?.stale ? ' · 缓存数据' : '')
@@ -741,7 +831,7 @@ window.__ModuleLoader__.load({
741
831
  id: 'antigravity-acp',
742
832
  order: 42,
743
833
  label: () => 'Antigravity ACP',
744
- inject: () => ({ remote: ctx.remote, readQuota, control }),
834
+ inject: () => ({ remote: ctx.remote, control }),
745
835
  },
746
836
  AntigravitySettingsCard,
747
837
  ),
package/dist/index.d.ts CHANGED
@@ -6,10 +6,15 @@ import type { AgentLinkHandlers } from './protocol/agent-link.js';
6
6
  import type { RuntimeDirectoryLease } from './runtime/binary.js';
7
7
  import type { AcpCredentialResult } from './runtime/credentials.js';
8
8
  import { AcpLifecycle } from './runtime/lifecycle.js';
9
- import type { AntigravityHostConfig } from './settings.js';
9
+ import type { AntigravityHostConfig, AntigravityPluginConfig } from './settings.js';
10
10
  export { AGY_PROVIDER_ID } from './adapter/antigravity-adapter.js';
11
11
  export { Config } from './settings.js';
12
12
  export type { AntigravityHostConfig } from './settings.js';
13
+ declare module '@deepseek-ai/cordis' {
14
+ interface Events {
15
+ 'loader/volatile-update'(paths: string[][]): void;
16
+ }
17
+ }
13
18
  export declare const name = "llm-antigravity-acp";
14
19
  export declare const inject: string[];
15
20
  /**
@@ -98,4 +103,4 @@ export interface AcpRuntimeDeps {
98
103
  export declare function ensureAcpRuntime(ctx: Context, config: AntigravityHostConfig, handlers: AgentLinkHandlers, active: ActiveAcpRuntime, deps?: AcpRuntimeDeps): Promise<AgentLink>;
99
104
  /** 设置页显式连接:独立运行时,只建空工具会话验证认证,不发送模型 prompt。 */
100
105
  export declare function connectAcpAccount(ctx: Context, config: AntigravityHostConfig, signal: AbortSignal, deps?: AcpRuntimeDeps): Promise<import("@agentclientprotocol/sdk").NewSessionResponse>;
101
- export declare function apply(ctx: Context, config: AntigravityHostConfig): void;
106
+ export declare function apply(ctx: Context, input: AntigravityPluginConfig): void;
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import { PooledAcpAdapter } from './pool/pooled-adapter.js';
15
15
  import { AgentLink, agentStreamFromSubprocess, AGY_CLIENT_INFO, AGY_LINK_ERROR, launchAcpAgent } from './protocol/agent-link.js';
16
16
  import { AGY_BINARY_ERROR, cleanupStaleRuntimeDirectories, cleanupStaleStagingDirs, DEFAULT_ACP_SERVER_VERSION, ensureAgentBinary, resolveCacheDir, resolveInstallRoot, createRuntimeDirectory, releaseRuntimeDirectory, defaultAgentArgs, } from './runtime/binary.js';
17
17
  import { resolveProxy } from './runtime/proxy-env.js';
18
- import { createProxiedFetch } from './runtime/proxied-fetch.js';
18
+ import { createLiveProxiedFetch, createProxiedFetch } from './runtime/proxied-fetch.js';
19
19
  import { ensureAcpCredentials } from './runtime/credentials.js';
20
20
  import { describeLowIntegrity, isLowIntegrityTarget } from './runtime/integrity.js';
21
21
  import { describeAuthWait, redactSensitiveText } from './runtime/redact.js';
@@ -25,6 +25,7 @@ import { agyCliCandidates, createSubprocessCliRun, fetchAgyModels } from './runt
25
25
  import { AgYQuotaService, registerAgYQuotaRemote } from './quota/quota-remote.js';
26
26
  import { SettingsControl } from './quota/control.js';
27
27
  import { AcpLifecycle } from './runtime/lifecycle.js';
28
+ import { liveHostConfig } from './settings.js';
28
29
  export { AGY_PROVIDER_ID } from './adapter/antigravity-adapter.js';
29
30
  export { Config } from './settings.js';
30
31
  export const name = 'llm-antigravity-acp';
@@ -376,7 +377,8 @@ export async function connectAcpAccount(ctx, config, signal, deps = {}) {
376
377
  await disposeAcpRuntime(runtime);
377
378
  }
378
379
  }
379
- export function apply(ctx, config) {
380
+ export function apply(ctx, input) {
381
+ const config = liveHostConfig(input);
380
382
  const cacheDir = resolveCacheDir();
381
383
  const acpVersion = config.acpVersion ?? DEFAULT_ACP_SERVER_VERSION;
382
384
  // 只回收带本插件所有权且已确认退出的运行目录;未知 / 活跃目录不按年龄删除。
@@ -389,9 +391,12 @@ export function apply(ctx, config) {
389
391
  // 代理规则与官方 ACP 进程一致:**配置的 proxyUrl 优先,其次宿主环境自身导出的
390
392
  // 代理**。CLI 每次启动都会做登录/配置刷新,缺代理会一直挂在网络上(实测),
391
393
  // 表现为额度药丸永远停在「额度…」。地址非法时以 AGY_INVALID_PROXY 明确失败。
392
- const cliProxy = resolveProxy({ url: config.proxyUrl, bypass: config.proxyBypass });
393
- const pluginFetch = createProxiedFetch(cliProxy);
394
- const cliRun = createSubprocessCliRun(ctx.subprocess, cliProxy);
394
+ const readProxy = () => resolveProxy({ url: config.proxyUrl, bypass: config.proxyBypass });
395
+ // Validate the initial route before registering resources. Subsequent operations
396
+ // capture the latest settings; existing ACP processes keep their launch env.
397
+ readProxy();
398
+ const pluginFetch = createLiveProxiedFetch(readProxy);
399
+ const cliRun = (request) => createSubprocessCliRun(ctx.subprocess, readProxy())(request);
395
400
  const findCli = () => agyCliCandidates(process.env, undefined, config.agyCliPath).find((candidate) => existsSync(candidate));
396
401
  let modelCatalogRevision = 0;
397
402
  const catalogChanged = () => {
@@ -402,6 +407,10 @@ export function apply(ctx, config) {
402
407
  }
403
408
  catch { /* Observers cannot veto catalog updates. */ }
404
409
  };
410
+ ctx.on('loader/volatile-update', (paths) => {
411
+ if (paths.some(([key]) => key === 'preferredModel'))
412
+ catalogChanged();
413
+ });
405
414
  let attachmentStore;
406
415
  /**
407
416
  * 官方模型清单:缓存先行(选择器即时可用),并在**后台**刷新一遍供下次生效。
@@ -442,7 +451,8 @@ export function apply(ctx, config) {
442
451
  await cliRefresh;
443
452
  return cliModelRows ?? [];
444
453
  };
445
- const sessionStore = config.resumeSessions === true ? createFileSessionMapStore(cacheDir, () => ctx.logger?.warn?.('antigravity-acp: 会话映射不可用或由另一 Host 占用,跨进程恢复已降级;不影响本地聊天')) : undefined;
454
+ // The store is lazy: no lock or file is opened until recovery is enabled and used.
455
+ const sessionStore = createFileSessionMapStore(cacheDir, () => ctx.logger?.warn?.('antigravity-acp: 会话映射不可用或由另一 Host 占用,跨进程恢复已降级;不影响本地聊天'));
446
456
  const workers = [];
447
457
  let metadata;
448
458
  // Serialize startup/credential preparation, not inference. This avoids competing OAuth imports.
@@ -458,7 +468,7 @@ export function apply(ctx, config) {
458
468
  getLink: (handlers, signal, purpose) => {
459
469
  const launch = startupBarrier.catch(() => undefined).then(() => {
460
470
  signal?.throwIfAborted();
461
- return ensureAcpRuntime(ctx, config, handlers, active, { signal, fetchImpl: pluginFetch, allowInteractiveAuth: purpose !== 'warmup' && workerIndex === 0 });
471
+ return ensureAcpRuntime(ctx, { ...config }, handlers, active, { signal, fetchImpl: pluginFetch, allowInteractiveAuth: purpose !== 'warmup' && workerIndex === 0 });
462
472
  });
463
473
  startupBarrier = launch.then(() => undefined, () => undefined);
464
474
  return launch;
@@ -470,13 +480,14 @@ export function apply(ctx, config) {
470
480
  promptTimeoutMs: config.promptTimeoutMs,
471
481
  parkSettleMs: config.bridgeParkSettleMs,
472
482
  preferredModel: config.preferredModel,
483
+ readSettings: () => ({ ...config, sessionStore: config.resumeSessions ? sessionStore : undefined }),
473
484
  // ACP 会话复用:同一条 DSH 对话的后续回合只发增量;重启恢复默认关闭。
474
485
  reuseSessions: config.reuseSessions,
475
486
  resumeSessions: config.resumeSessions,
476
487
  keepProcessAfterCancel: config.keepProcessAfterCancel,
477
488
  cancelGraceMs: config.cancelGraceMs,
478
489
  idleRecycleMs: config.idleRecycleMs,
479
- sessionStore,
490
+ sessionStore: config.resumeSessions ? sessionStore : undefined,
480
491
  canReadImages: () => attachmentStore !== undefined,
481
492
  readImage: (ref, signal) => {
482
493
  if (!attachmentStore)
@@ -505,12 +516,17 @@ export function apply(ctx, config) {
505
516
  if (metadata)
506
517
  worker.importMetadata(metadata);
507
518
  workers.push(worker);
508
- return { adapter: worker, read: () => lifecycle.read(), dispose: () => disposeAcpRuntime(active) };
519
+ return {
520
+ adapter: worker,
521
+ ensureImages: (signal) => worker.prepareImages(signal),
522
+ read: () => lifecycle.read(),
523
+ dispose: () => disposeAcpRuntime(active),
524
+ };
509
525
  }, {
510
526
  capacity: config.maxConcurrentSessions,
511
527
  queueTimeoutMs: config.queueTimeoutMs,
512
528
  // 复用会话时同一对话必须粘在同一个 worker;关闭复用时回到纯负载调度。
513
- sessionAffinity: config.reuseSessions !== false,
529
+ sessionAffinity: () => config.reuseSessions !== false,
514
530
  });
515
531
  ctx.inject(['attachments'], (scope) => {
516
532
  const store = scope.attachments;
@@ -559,10 +575,11 @@ export function apply(ctx, config) {
559
575
  warn: (message, error) => ctx.logger?.warn?.(redactSensitiveText(`${message}${error ? `: ${error instanceof Error ? error.message : String(error)}` : ''}`)),
560
576
  },
561
577
  });
562
- // 把 Config schema 投到设置表单(插件页)。弱依赖 settings,无 Web 的 profile 仍能加载。
578
+ // Config 的即时字段由 settings 投影;使用自定义卡片,不重复生成页面。
579
+ // 弱依赖 settings,无 Web 的 profile 仍能加载。
563
580
  ctx.inject(['settings'], (settingsCtx) => {
564
581
  const settings = settingsCtx.get('settings');
565
- settingsCtx.effect(() => settings.configure({ auto: true }, settingsCtx.fiber), 'antigravity-acp: schema settings form');
582
+ settingsCtx.effect(() => settings.configure({ auto: false }, ctx.fiber), 'antigravity-acp: schema settings form');
566
583
  });
567
584
  // cordis 4.x 的规范 dispose 挂点:generator effect,卸载时逆序执行。
568
585
  ctx.effect(function* () {