tavern-harness 0.1.22 → 0.1.23

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/CHANGELOG.md CHANGED
@@ -3,6 +3,34 @@
3
3
  > 本文件由 `scripts/changelog.mjs` 从 `changelog/v*.md` 生成,请勿手动编辑。
4
4
  > 版本对比链接见文末;源文件位于仓库 `changelog/` 目录。
5
5
 
6
+ ## [0.1.23] - 2026-10-26
7
+
8
+ ### 中文
9
+
10
+ #### 改进
11
+
12
+ - 生成参数滑杆不再需要先点「解锁」:直接拖动即可调整,只有明显的横向滑动才会改变数值,上下滚动设置页时不会再误改温度、上下文压缩等参数
13
+ - 「重置」按钮不再受锁定状态限制,只要参数不是默认值就能一键恢复
14
+ - 酒馆老板的内置文案本地化明显加快:只读取与它相关的消息和参与者,并一次性批量写入,历史记录较多时也不会再逐条卡顿
15
+
16
+ #### 修复
17
+
18
+ - 移动端中文衬线字体在缺少对应字重的系统上也能正常显示加粗,不再一律退化成常规字重
19
+ - 移动端点按按钮、滑杆、开关时不再出现系统默认的蓝色高亮块
20
+
21
+ ### English
22
+
23
+ #### Changed
24
+
25
+ - Generation parameter sliders no longer need to be unlocked first: drag them directly, and only a clearly horizontal drag changes the value, so scrolling the settings page can no longer alter temperature, context compression, and other values by accident
26
+ - The "Reset" button is no longer tied to a lock state and is available whenever the parameters differ from their defaults
27
+ - Localizing the built-in Tavern Keeper text is much faster: only its own messages and participants are read, and they are written in a single batch, so large chat histories no longer stall
28
+
29
+ #### Fixed
30
+
31
+ - Chinese serif text on mobile now renders bold correctly on systems whose fallback fonts lack the requested weight, instead of silently falling back to regular
32
+ - Tapping buttons, sliders, and switches on mobile no longer flashes the system's default blue highlight
33
+
6
34
  ## [0.1.22] - 2026-10-26
7
35
 
8
36
  ### 中文
@@ -628,6 +656,7 @@
628
656
  - Vite + React + Zustand + Dexie stack with browser-side local persistence
629
657
 
630
658
  <!-- 链接定义 -->
659
+ [0.1.23]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.22...v0.1.23
631
660
  [0.1.22]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.21...v0.1.22
632
661
  [0.1.21]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.20...v0.1.21
633
662
  [0.1.20]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.19...v0.1.20
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tavern-harness",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "description": "Tavern Harness — 酒馆模式下的本地 AI Agent 助手(MyAgent-Android 的 Web 复刻)",
5
5
  "private": false,
6
6
  "type": "module",
@@ -141,6 +141,17 @@ function validKey(value) {
141
141
  return typeof value === 'string' && value.length > 0 && value.length <= 255;
142
142
  }
143
143
 
144
+ function validBulkUpdates(value) {
145
+ return Array.isArray(value)
146
+ && value.length <= 10_000
147
+ && value.every((update) => update
148
+ && typeof update === 'object'
149
+ && validKey(update.key)
150
+ && update.changes
151
+ && typeof update.changes === 'object'
152
+ && !Array.isArray(update.changes));
153
+ }
154
+
144
155
  function lockState(clientId) {
145
156
  return {
146
157
  writable: Boolean(writeLock && writeLock.clientId === clientId),
@@ -188,6 +199,20 @@ const listStartupRecords = database.prepare(`
188
199
  `);
189
200
  const deleteRecord = database.prepare('DELETE FROM app_records WHERE collection = ? AND record_key = ?');
190
201
  const clearCollection = database.prepare('DELETE FROM app_records WHERE collection = ?');
202
+ function bulkUpdateRecords(collection, updates) {
203
+ database.exec('BEGIN IMMEDIATE');
204
+ try {
205
+ for (const { key, changes } of updates) {
206
+ const record = getRecord.get(collection, key);
207
+ if (!record) continue;
208
+ saveRecord(collection, key, { ...JSON.parse(record.value_json), ...changes });
209
+ }
210
+ database.exec('COMMIT');
211
+ } catch (error) {
212
+ database.exec('ROLLBACK');
213
+ throw error;
214
+ }
215
+ }
191
216
  const nextNumericRecordKey = database.prepare(`
192
217
  SELECT COALESCE(MAX(CAST(record_key AS INTEGER)), 0) + 1 AS next_key
193
218
  FROM app_records
@@ -351,6 +376,16 @@ const server = createServer(async (req, res) => {
351
376
  json(res, 200, { ok: true, id: allocateNumericRecordId(collection) });
352
377
  return;
353
378
  }
379
+ if (pathname === '/records/bulk-update') {
380
+ if (!validBulkUpdates(body.updates)) {
381
+ json(res, 400, { ok: false, message: 'Invalid record updates' });
382
+ return;
383
+ }
384
+ requireWriter(clientId);
385
+ bulkUpdateRecords(collection, body.updates);
386
+ json(res, 200, { ok: true });
387
+ return;
388
+ }
354
389
  if (pathname === '/records/put') {
355
390
  if (!validKey(body.key)) {
356
391
  json(res, 400, { ok: false, message: 'Invalid record key' });
@@ -1,4 +1,4 @@
1
- import { memo, useEffect, useState } from 'react';
1
+ import { memo, useRef, useState, type PointerEvent as ReactPointerEvent } from 'react';
2
2
  import { useStore } from '../store/store';
3
3
  import { db, DEFAULT_GENERATION_SETTINGS } from '../db/database';
4
4
  import { type ThemeMode } from '../theme/theme';
@@ -24,24 +24,72 @@ export const SettingsView = memo(function SettingsView() {
24
24
  const [showResetConfirm, setShowResetConfirm] = useState(false);
25
25
  const [resetting, setResetting] = useState(false);
26
26
  const [showVersionHistory, setShowVersionHistory] = useState(false);
27
- const [generationParamsLocked, setGenerationParamsLocked] = useState(true);
28
- const activeView = useStore((s) => s.activeView);
27
+ const generationSliderGesture = useRef<{
28
+ pointerId: number;
29
+ startX: number;
30
+ startY: number;
31
+ startValue: number;
32
+ direction: 'pending' | 'horizontal' | 'vertical';
33
+ } | null>(null);
29
34
  const t = useT();
30
35
  const hasUnseenVersion = useHasUnseenVersion();
31
36
 
32
37
  if (!settings) return null;
33
38
 
39
+ const startGenerationSliderGesture = (event: ReactPointerEvent<HTMLInputElement>) => {
40
+ if (event.pointerType !== 'touch') return;
41
+ const input = event.currentTarget;
42
+ generationSliderGesture.current = {
43
+ pointerId: event.pointerId,
44
+ startX: event.clientX,
45
+ startY: event.clientY,
46
+ startValue: Number(input.value),
47
+ direction: 'pending',
48
+ };
49
+ };
50
+
51
+ const classifyGenerationSliderGesture = (event: ReactPointerEvent<HTMLInputElement>) => {
52
+ const gesture = generationSliderGesture.current;
53
+ if (!gesture || gesture.pointerId !== event.pointerId || gesture.direction !== 'pending') return gesture?.direction;
54
+
55
+ const deltaX = Math.abs(event.clientX - gesture.startX);
56
+ const deltaY = Math.abs(event.clientY - gesture.startY);
57
+ if (Math.max(deltaX, deltaY) < 10) return 'pending';
58
+
59
+ gesture.direction = deltaX > deltaY ? 'horizontal' : 'vertical';
60
+ return gesture.direction;
61
+ };
62
+
63
+ const endGenerationSliderGesture = (event: ReactPointerEvent<HTMLInputElement>) => {
64
+ if (generationSliderGesture.current?.pointerId === event.pointerId) generationSliderGesture.current = null;
65
+ };
66
+
67
+ const updateGenerationSlider = (key: keyof typeof settings, value: number | 'off') => {
68
+ const gesture = generationSliderGesture.current;
69
+ if (!gesture) {
70
+ setSettings({ [key]: value });
71
+ } else if (gesture.direction === 'horizontal') {
72
+ setSettings({ [key]: value });
73
+ } else if (gesture.direction === 'pending') {
74
+ setSettings({ [key]: gesture.startValue });
75
+ }
76
+ };
77
+
34
78
  // 数字滑杆——数值显示固定在行内,不溢出卡片
35
79
  const slider = (key: keyof typeof settings, min: number, max: number, step: number, fmt?: (v: number) => string) => (
36
80
  <label className="range-row" style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 10, alignItems: 'center' }}>
37
81
  <input
38
82
  type="range"
83
+ className="generation-slider"
39
84
  min={min}
40
85
  max={max}
41
86
  step={step}
42
87
  value={settings[key] as number}
43
- disabled={generationParamsLocked}
44
- onChange={(e) => setSettings({ [key]: parseFloat(e.target.value) })}
88
+ onPointerDown={startGenerationSliderGesture}
89
+ onPointerMove={classifyGenerationSliderGesture}
90
+ onPointerUp={endGenerationSliderGesture}
91
+ onPointerCancel={endGenerationSliderGesture}
92
+ onChange={(e) => updateGenerationSlider(key, parseFloat(e.target.value))}
45
93
  />
46
94
  <span className="range-val">{fmt ? fmt(settings[key] as number) : (settings[key] as number)}</span>
47
95
  </label>
@@ -55,12 +103,6 @@ export const SettingsView = memo(function SettingsView() {
55
103
  await setSettings({ ...generationDefaults });
56
104
  addToast(t('toast.genParamsReset'));
57
105
  };
58
- const toggleGenerationParamsLock = () => setGenerationParamsLocked((locked) => !locked);
59
-
60
- useEffect(() => {
61
- if (activeView !== 'settings') setGenerationParamsLocked(true);
62
- }, [activeView]);
63
-
64
106
  return (
65
107
  <div className="view-page">
66
108
  <div className="view-col">
@@ -112,26 +154,15 @@ export const SettingsView = memo(function SettingsView() {
112
154
  <div className="set-section">
113
155
  <div className="settings-section-head">
114
156
  <h3>{t('settings.genParams')}</h3>
115
- <div className="settings-gen-actions">
116
- <button
117
- className="btn btn-sm settings-gen-lock"
118
- title={t(generationParamsLocked ? 'settings.genParamsUnlock' : 'settings.genParamsLock')}
119
- aria-label={t(generationParamsLocked ? 'settings.genParamsUnlock' : 'settings.genParamsLock')}
120
- aria-pressed={generationParamsLocked}
121
- onClick={toggleGenerationParamsLock}
122
- >
123
- <Icon name={generationParamsLocked ? 'lock' : 'unlock'} size={12} /> {t(generationParamsLocked ? 'settings.genParamsLocked' : 'settings.genParamsUnlocked')}
124
- </button>
125
- <button
126
- className="btn btn-sm settings-gen-reset"
127
- title={t('settings.genParamsResetTitle')}
128
- aria-label={t('settings.genParamsResetTitle')}
129
- disabled={generationParamsLocked || isGenerationDefault}
130
- onClick={() => void resetGenerationParams()}
131
- >
132
- <Icon name="refresh" size={12} /> {t('settings.genParamsReset')}
133
- </button>
134
- </div>
157
+ <button
158
+ className="btn btn-sm settings-gen-reset"
159
+ title={t('settings.genParamsResetTitle')}
160
+ aria-label={t('settings.genParamsResetTitle')}
161
+ disabled={isGenerationDefault}
162
+ onClick={() => void resetGenerationParams()}
163
+ >
164
+ <Icon name="refresh" size={12} /> {t('settings.genParamsReset')}
165
+ </button>
135
166
  </div>
136
167
  <table className="table param-table">
137
168
  <tbody>
@@ -193,12 +224,16 @@ export const SettingsView = memo(function SettingsView() {
193
224
  <label className="range-row" style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 10, alignItems: 'center' }}>
194
225
  <input
195
226
  type="range"
227
+ className="generation-slider"
196
228
  min="0"
197
229
  max="100"
198
230
  step="1"
199
231
  value={settings.contextCompressionThreshold === 'off' ? 0 : settings.contextCompressionThreshold}
200
- disabled={generationParamsLocked}
201
- onChange={(e) => setSettings({ contextCompressionThreshold: Number(e.target.value) || 'off' })}
232
+ onPointerDown={startGenerationSliderGesture}
233
+ onPointerMove={classifyGenerationSliderGesture}
234
+ onPointerUp={endGenerationSliderGesture}
235
+ onPointerCancel={endGenerationSliderGesture}
236
+ onChange={(e) => updateGenerationSlider('contextCompressionThreshold', Number(e.target.value) || 'off')}
202
237
  />
203
238
  <span className="range-val">
204
239
  {settings.contextCompressionThreshold === 'off' ? t('settings.off') : settings.contextCompressionThreshold}
@@ -215,7 +250,6 @@ export const SettingsView = memo(function SettingsView() {
215
250
  <select
216
251
  className="select"
217
252
  value={settings.reasoningEffort}
218
- disabled={generationParamsLocked}
219
253
  onChange={(e) => setSettings({ reasoningEffort: e.target.value as ReasoningEffort })}
220
254
  >
221
255
  {['auto', 'off', 'low', 'medium', 'xhigh'].map((r) => <option key={r} value={r}>{r}</option>)}
package/src/core/i18n.ts CHANGED
@@ -202,10 +202,6 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
202
202
  'settings.yoloMode': 'YOLO 模式',
203
203
  'settings.yoloModeDesc': '自动批准工具修改和 Shell 执行,无需逐次确认。仅在信任角色和技能时开启。',
204
204
  'settings.genParams': '🎛 生成参数',
205
- 'settings.genParamsLocked': '已锁定',
206
- 'settings.genParamsUnlocked': '已解锁',
207
- 'settings.genParamsLock': '锁定生成参数',
208
- 'settings.genParamsUnlock': '解锁生成参数',
209
205
  'settings.genParamsReset': '重置',
210
206
  'settings.genParamsResetTitle': '将生成参数恢复为默认值',
211
207
  'settings.tempDesc': '创造性与随机性',
@@ -253,7 +249,7 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
253
249
  'builtinNpc.prompt': '神秘的酒馆老板,可以响应客人的任何需求',
254
250
  'builtinNpc.greeting': '你来啦!快坐下~',
255
251
  'builtinWorldbook.name': '酒馆经营手册(精华版)',
256
- 'settings.providerTitle': '🔌 模型服务 Provider(多端点)',
252
+ 'settings.providerTitle': '🔌 模型服务 Provider',
257
253
  'settings.noProvider': '还没有配置 Provider,点击下方按钮添加',
258
254
  'settings.enable': '启用',
259
255
  'settings.disable': '停用',
@@ -729,10 +725,6 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
729
725
  'settings.yoloMode': 'YOLO 模式',
730
726
  'settings.yoloModeDesc': '自動批准工具修改和 Shell 執行,無需逐次確認。僅在信任角色和技能時開啟。',
731
727
  'settings.genParams': '🎛 產生參數',
732
- 'settings.genParamsLocked': '已鎖定',
733
- 'settings.genParamsUnlocked': '已解鎖',
734
- 'settings.genParamsLock': '鎖定產生參數',
735
- 'settings.genParamsUnlock': '解鎖產生參數',
736
728
  'settings.genParamsReset': '重設',
737
729
  'settings.genParamsResetTitle': '將產生參數恢復為預設值',
738
730
  'settings.tempDesc': '創造性與隨機性',
@@ -780,7 +772,7 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
780
772
  'builtinNpc.prompt': '神秘的酒館老闆,可以回應客人的任何需求',
781
773
  'builtinNpc.greeting': '你來啦!快坐下~',
782
774
  'builtinWorldbook.name': '酒館經營手冊(精華版)',
783
- 'settings.providerTitle': '🔌 模型服務 Provider(多端點)',
775
+ 'settings.providerTitle': '🔌 模型服務 Provider',
784
776
  'settings.noProvider': '還沒有設定 Provider,點擊下方按鈕新增',
785
777
  'settings.enable': '啟用',
786
778
  'settings.disable': '停用',
@@ -1247,10 +1239,6 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
1247
1239
  'settings.yoloMode': 'YOLO mode',
1248
1240
  'settings.yoloModeDesc': 'Automatically approve tool changes and Shell commands without asking each time. Only enable for trusted characters and skills.',
1249
1241
  'settings.genParams': '🎛 Generation parameters',
1250
- 'settings.genParamsLocked': 'Locked',
1251
- 'settings.genParamsUnlocked': 'Unlocked',
1252
- 'settings.genParamsLock': 'Lock generation parameters',
1253
- 'settings.genParamsUnlock': 'Unlock generation parameters',
1254
1242
  'settings.genParamsReset': 'Reset',
1255
1243
  'settings.genParamsResetTitle': 'Restore generation parameters to their default values',
1256
1244
  'settings.tempDesc': 'Creativity & randomness',
@@ -1298,7 +1286,7 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
1298
1286
  'builtinNpc.prompt': 'A mysterious tavern keeper who can fulfill any request from guests',
1299
1287
  'builtinNpc.greeting': 'Welcome! Have a seat~',
1300
1288
  'builtinWorldbook.name': 'Tavern Management Handbook (Essentials)',
1301
- 'settings.providerTitle': '🔌 Model Providers (multiple endpoints)',
1289
+ 'settings.providerTitle': '🔌 Model Providers',
1302
1290
  'settings.noProvider': 'No providers configured yet — add one below',
1303
1291
  'settings.enable': 'Enabled',
1304
1292
  'settings.disable': 'Disabled',
@@ -530,6 +530,17 @@ class RemoteTable<T extends object> {
530
530
  }
531
531
  async bulkAdd(values: T[]): Promise<void> { for (const value of values) await this.add(value); }
532
532
  async bulkPut(values: T[]): Promise<void> { for (const value of values) await this.put(value); }
533
+ async bulkUpdate(updates: Array<{ key: unknown; changes: Partial<T> }>): Promise<void> {
534
+ if (updates.length === 0) return;
535
+ await requireWriteLock();
536
+ await persistenceRequest('/records/bulk-update', {
537
+ collection: this.collection,
538
+ updates: updates.map(({ key, changes }) => ({
539
+ key: Array.isArray(key) ? key.join(':') : String(key),
540
+ changes,
541
+ })),
542
+ });
543
+ }
533
544
  async put(value: T): Promise<number> { return this.add(value); }
534
545
  async update(key: unknown, changes: Partial<T>): Promise<number> {
535
546
  await requireWriteLock();
@@ -797,40 +808,33 @@ export async function localizeBuiltinNpc(): Promise<void> {
797
808
  if (isPristine(boss.greeting || '')) updates.greeting = tGreeting;
798
809
  if (Object.keys(updates).length > 0) {
799
810
  await db.npcs.update(boss.id!, updates);
800
- // 会话标题 / 最后一条消息 / 参与者 / 消息里的旧内置文案一并更新(仅限仍是内置原文的记录)
801
- const sessions = await db.sessions.toArray();
802
- for (const s of sessions) {
803
- const changes: Partial<ChatSession> = {};
804
- if (s.title === '酒馆老板' || s.title === '酒館老闆' || s.title === 'Tavern Keeper') {
805
- changes.title = tName;
806
- }
807
- if (s.lastMessage && isPristine(s.lastMessage)) {
808
- changes.lastMessage = tGreeting;
809
- }
810
- if (Object.keys(changes).length > 0) {
811
- await db.sessions.update(s.id!, changes);
812
- }
813
- }
814
- // 参与者 / 消息里的旧内置文案一并更新(仅限仍是内置原文的记录)
815
- const participants = await db.participants.toArray();
816
- for (const p of participants) {
817
- if (
818
- p.kind === 'NPC' &&
819
- p.npcId === boss.id &&
820
- (p.displayName === '酒馆老板' || p.displayName === '酒館老闆' || p.displayName === 'Tavern Keeper')
821
- ) {
822
- await db.participants.update(p, { displayName: tName });
823
- }
824
- }
825
- const messages = await db.messages.toArray();
826
- for (const m of messages) {
827
- if (
828
- m.speakerParticipantId === boss.id &&
829
- (m.speakerName === '酒馆老板' || m.speakerName === '酒館老闆' || m.speakerName === 'Tavern Keeper')
830
- ) {
831
- await db.messages.update(m.id!, { speakerName: tName });
832
- }
833
- }
811
+ // 仅请求与酒馆老板关联的记录;批量写入可避免大量历史消息产生逐条网络往返。
812
+ const [sessions, participants, messages] = await Promise.all([
813
+ db.sessions.toArray(),
814
+ db.participants.where('npcId').equals(boss.id!).toArray(),
815
+ db.messages.where('speakerParticipantId').equals(boss.id!).toArray(),
816
+ ]);
817
+ await Promise.all([
818
+ db.sessions.bulkUpdate(sessions.flatMap((session) => {
819
+ const changes: Partial<ChatSession> = {};
820
+ if (session.title === '酒馆老板' || session.title === '酒館老闆' || session.title === 'Tavern Keeper') {
821
+ changes.title = tName;
822
+ }
823
+ if (session.lastMessage && isPristine(session.lastMessage)) changes.lastMessage = tGreeting;
824
+ return Object.keys(changes).length > 0 ? [{ key: session.id!, changes }] : [];
825
+ })),
826
+ db.participants.bulkUpdate(participants.flatMap((participant) => (
827
+ participant.kind === 'NPC' &&
828
+ (participant.displayName === '酒馆老板' || participant.displayName === '酒館老闆' || participant.displayName === 'Tavern Keeper')
829
+ ? [{ key: [participant.sessionId, participant.participantId], changes: { displayName: tName } }]
830
+ : []
831
+ ))),
832
+ db.messages.bulkUpdate(messages.flatMap((message) => (
833
+ message.speakerName === '酒馆老板' || message.speakerName === '酒館老闆' || message.speakerName === 'Tavern Keeper'
834
+ ? [{ key: message.id!, changes: { speakerName: tName } }]
835
+ : []
836
+ ))),
837
+ ]);
834
838
  }
835
839
 
836
840
  if (prevLang !== lang) {
@@ -1,7 +1,49 @@
1
1
  {
2
- "latest": "0.1.22",
2
+ "latest": "0.1.23",
3
3
  "repo": "https://github.com/Rannichan/Tavern-Harness",
4
4
  "entries": [
5
+ {
6
+ "version": "0.1.23",
7
+ "date": "2026-10-26",
8
+ "breaking": false,
9
+ "compare": "v0.1.22...v0.1.23",
10
+ "groups": {
11
+ "zh-CN": [
12
+ {
13
+ "kind": "changed",
14
+ "items": [
15
+ "生成参数滑杆不再需要先点「解锁」:直接拖动即可调整,只有明显的横向滑动才会改变数值,上下滚动设置页时不会再误改温度、上下文压缩等参数",
16
+ "「重置」按钮不再受锁定状态限制,只要参数不是默认值就能一键恢复",
17
+ "酒馆老板的内置文案本地化明显加快:只读取与它相关的消息和参与者,并一次性批量写入,历史记录较多时也不会再逐条卡顿"
18
+ ]
19
+ },
20
+ {
21
+ "kind": "fixed",
22
+ "items": [
23
+ "移动端中文衬线字体在缺少对应字重的系统上也能正常显示加粗,不再一律退化成常规字重",
24
+ "移动端点按按钮、滑杆、开关时不再出现系统默认的蓝色高亮块"
25
+ ]
26
+ }
27
+ ],
28
+ "en": [
29
+ {
30
+ "kind": "changed",
31
+ "items": [
32
+ "Generation parameter sliders no longer need to be unlocked first: drag them directly, and only a clearly horizontal drag changes the value, so scrolling the settings page can no longer alter temperature, context compression, and other values by accident",
33
+ "The \"Reset\" button is no longer tied to a lock state and is available whenever the parameters differ from their defaults",
34
+ "Localizing the built-in Tavern Keeper text is much faster: only its own messages and participants are read, and they are written in a single batch, so large chat histories no longer stall"
35
+ ]
36
+ },
37
+ {
38
+ "kind": "fixed",
39
+ "items": [
40
+ "Chinese serif text on mobile now renders bold correctly on systems whose fallback fonts lack the requested weight, instead of silently falling back to regular",
41
+ "Tapping buttons, sliders, and switches on mobile no longer flashes the system's default blue highlight"
42
+ ]
43
+ }
44
+ ]
45
+ }
46
+ },
5
47
  {
6
48
  "version": "0.1.22",
7
49
  "date": "2026-10-26",
@@ -23,6 +23,7 @@
23
23
 
24
24
  * {
25
25
  box-sizing: border-box;
26
+ -webkit-tap-highlight-color: transparent;
26
27
  }
27
28
 
28
29
  html,
@@ -85,14 +86,6 @@ button {
85
86
  white-space: nowrap;
86
87
  }
87
88
 
88
- /* Android WebView adds a blue overlay to non-button controls such as switch labels. */
89
- button,
90
- a,
91
- label,
92
- [role='button'] {
93
- -webkit-tap-highlight-color: transparent;
94
- }
95
-
96
89
  input,
97
90
  textarea,
98
91
  select {
@@ -1807,6 +1800,11 @@ hr.sep {
1807
1800
  height: 36px;
1808
1801
  }
1809
1802
 
1803
+ :root {
1804
+ /* Android 中文衬线回退字体常缺少所需字重;移动端统一允许合成字重。 */
1805
+ font-synthesis: weight;
1806
+ }
1807
+
1810
1808
  .chat-header .ttitle {
1811
1809
  max-width: min(52vw, 260px);
1812
1810
  font-size: 15px;
@@ -2,6 +2,10 @@
2
2
  弹窗 / 覆盖层 / 视图页 / 卡片列表
3
3
  ============================================================ */
4
4
 
5
+ .generation-slider {
6
+ touch-action: pan-y;
7
+ }
8
+
5
9
  .overlay {
6
10
  position: fixed;
7
11
  inset: 0;
@@ -625,13 +629,7 @@
625
629
  justify-content: space-between;
626
630
  gap: 12px;
627
631
  }
628
- .settings-gen-actions {
629
- display: flex;
630
- flex-shrink: 0;
631
- gap: 6px;
632
- }
633
- .settings-gen-reset,
634
- .settings-gen-lock {
632
+ .settings-gen-reset {
635
633
  flex-shrink: 0;
636
634
  }
637
635
  .settings-reset-section {