tavern-harness 0.1.25 → 0.1.26

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,32 @@
3
3
  > 本文件由 `scripts/changelog.mjs` 从 `changelog/v*.md` 生成,请勿手动编辑。
4
4
  > 版本对比链接见文末;源文件位于仓库 `changelog/` 目录。
5
5
 
6
+ ## [0.1.26] - 2026-10-27
7
+
8
+ ### 中文
9
+
10
+ #### 改进
11
+
12
+ - 角色卡与世界书的知识库编辑器现在支持 JSON 语法高亮,键名、字符串、数字等用不同颜色区分,长内容更容易扫读
13
+ - 知识库 JSON 格式有误时,提示会指出具体出错的第几行,方便直接定位修改
14
+
15
+ #### 修复
16
+
17
+ - 修复手机上编辑角色时,切换「角色 / 知识库」标签会收起键盘、底部按钮随之闪烁跳动的问题
18
+ - 修复技能卡片里过长的工具名在手机上撑出屏幕、把操作按钮挤到看不见的问题
19
+
20
+ ### English
21
+
22
+ #### Changed
23
+
24
+ - The character-card and world-book knowledge editors now highlight JSON syntax, coloring keys, strings, and numbers so long entries are easier to scan
25
+ - When knowledge-base JSON is invalid, the error now names the exact line, so the problem can be found and fixed directly
26
+
27
+ #### Fixed
28
+
29
+ - Fixed an issue on mobile where switching between the Character and Knowledge tabs while editing a character dismissed the keyboard and made the bottom buttons flicker
30
+ - Fixed overly long tool names in skill cards overflowing the screen on mobile and pushing the action buttons out of view
31
+
6
32
  ## [0.1.25] - 2026-10-27
7
33
 
8
34
  ### 中文
@@ -716,6 +742,7 @@
716
742
  - Vite + React + Zustand + Dexie stack with browser-side local persistence
717
743
 
718
744
  <!-- 链接定义 -->
745
+ [0.1.26]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.25...v0.1.26
719
746
  [0.1.25]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.24...v0.1.25
720
747
  [0.1.24]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.23...v0.1.24
721
748
  [0.1.23]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.22...v0.1.23
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tavern-harness",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "description": "Tavern Harness — 酒馆模式下的本地 AI Agent 助手(MyAgent-Android 的 Web 复刻)",
5
5
  "private": false,
6
6
  "type": "module",
@@ -1,7 +1,17 @@
1
1
  import { memo, useEffect, useRef } from 'react';
2
2
  import { Compartment, EditorState } from '@codemirror/state';
3
3
  import { EditorView, lineNumbers } from '@codemirror/view';
4
+ import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
4
5
  import { json } from '@codemirror/lang-json';
6
+ import { tags } from '@lezer/highlight';
7
+
8
+ const characterBookJsonHighlight = syntaxHighlighting(HighlightStyle.define([
9
+ { tag: tags.propertyName, color: 'var(--primary-dim)', fontWeight: '600' },
10
+ { tag: tags.string, color: 'var(--success)' },
11
+ { tag: [tags.number, tags.bool, tags.null], color: 'var(--think)' },
12
+ { tag: tags.keyword, color: 'var(--primary)' },
13
+ { tag: tags.punctuation, color: 'var(--text-dim)' },
14
+ ]));
5
15
 
6
16
  const CharacterBookEditor = memo(function CharacterBookEditor({
7
17
  value,
@@ -37,6 +47,7 @@ const CharacterBookEditor = memo(function CharacterBookEditor({
37
47
  extensions: [
38
48
  lineNumbers(),
39
49
  json(),
50
+ characterBookJsonHighlight,
40
51
  wrappingRef.current.of(lineWrapping ? EditorView.lineWrapping : []),
41
52
  readOnlyRef.current.of(readOnly ? [EditorState.readOnly.of(true), EditorView.editable.of(false)] : []),
42
53
  EditorView.updateListener.of((update) => {
@@ -35,6 +35,19 @@ const CharacterBookEditor = lazy(() => import('./CharacterBookEditor'));
35
35
 
36
36
  type WorkshopTab = 'characters' | 'worldbooks' | 'skills';
37
37
 
38
+ function invalidJsonLine(value: string): number | null {
39
+ const trimmed = value.trim();
40
+ if (!trimmed) return null;
41
+ try {
42
+ JSON.parse(trimmed);
43
+ return null;
44
+ } catch (error) {
45
+ const position = /position (\d+)/i.exec(String(error))?.[1];
46
+ if (!position) return 1;
47
+ return trimmed.slice(0, Number(position)).split('\n').length;
48
+ }
49
+ }
50
+
38
51
  // 内置角色「酒馆老板」默认启用所有技能
39
52
  const ALL_DEFAULT_SKILLS = [...ALL_BUILTIN_TOOL_NAMES];
40
53
 
@@ -274,13 +287,10 @@ function CharacterEditorModal({ npc, isNew, onClose, onSaved }: { npc: NpcCharac
274
287
  addToast(t('toast.nameRequired'), 'error');
275
288
  return;
276
289
  }
277
- if (characterBook.trim()) {
278
- try {
279
- JSON.parse(characterBook);
280
- } catch {
281
- addToast(t('toast.characterBookInvalidJson'), 'error');
282
- return;
283
- }
290
+ const characterBookInvalidLine = invalidJsonLine(characterBook);
291
+ if (characterBookInvalidLine != null) {
292
+ addToast(t('toast.characterBookInvalidJson', { line: characterBookInvalidLine }), 'error');
293
+ return;
284
294
  }
285
295
  // 清理空白/重复项:第一项作为主开场白,其余作为备用
286
296
  const cleaned = greetings.map((g) => g.trim()).filter((g, i, arr) => g && arr.indexOf(g) === i);
@@ -318,9 +328,10 @@ function CharacterEditorModal({ npc, isNew, onClose, onSaved }: { npc: NpcCharac
318
328
  <div className="modal-head">
319
329
  <span className="typo-dialog-title">{isNew ? t('workshop.newCharTitle') : t('workshop.editCharTitle')}</span>
320
330
  <div className="raw-tabs">
321
- <button className={`raw-tab ${editorTab === 'character' ? 'active' : ''}`} onClick={() => setEditorTab('character')}>{t('workshop.characterTab')}</button>
331
+ <button className={`raw-tab ${editorTab === 'character' ? 'active' : ''}`} onPointerDown={(event) => event.preventDefault()} onClick={() => setEditorTab('character')}>{t('workshop.characterTab')}</button>
322
332
  <button
323
333
  className={`raw-tab ${editorTab === 'knowledge' ? 'active' : ''}`}
334
+ onPointerDown={(event) => event.preventDefault()}
324
335
  onClick={() => setEditorTab('knowledge')}
325
336
  >
326
337
  {t('workshop.knowledgeTab')}
@@ -337,8 +348,8 @@ function CharacterEditorModal({ npc, isNew, onClose, onSaved }: { npc: NpcCharac
337
348
  <button className="icon-btn" onClick={onClose}><Icon name="x" /></button>
338
349
  </div>
339
350
  </div>
340
- <div className={`modal-body character-editor-body ${editorTab === 'knowledge' ? 'knowledge-tab' : ''}`}>
341
- {editorTab === 'character' && <>
351
+ <div className="modal-body character-editor-body">
352
+ <div className={`character-editor-tab-panel ${editorTab === 'character' ? 'active' : ''}`}>
342
353
  <div style={{ display: 'flex', gap: 14, alignItems: 'center' }}>
343
354
  <Avatar name={name || t('chat.speakerChar')} colorOrdinal={npc.avatarColorOrdinal} imageUrl={avatarDataUrl} size="sm" />
344
355
  <div style={{ display: 'flex', gap: 8 }}>
@@ -427,8 +438,8 @@ function CharacterEditorModal({ npc, isNew, onClose, onSaved }: { npc: NpcCharac
427
438
  </div>
428
439
  )}
429
440
  </div>
430
- </>}
431
- {editorTab === 'knowledge' && (
441
+ </div>
442
+ <div className={`character-editor-tab-panel knowledge-tab ${editorTab === 'knowledge' ? 'active' : ''}`}>
432
443
  <div className="field prompt-field character-book-field">
433
444
  <label>{t('workshop.characterBookLabel')}</label>
434
445
  <Suspense fallback={<div className="character-book-editor-loading" />}>
@@ -439,7 +450,7 @@ function CharacterEditorModal({ npc, isNew, onClose, onSaved }: { npc: NpcCharac
439
450
  />
440
451
  </Suspense>
441
452
  </div>
442
- )}
453
+ </div>
443
454
  </div>
444
455
  <div className="modal-foot">
445
456
  {npc.isBuiltIn && (
@@ -448,7 +459,7 @@ function CharacterEditorModal({ npc, isNew, onClose, onSaved }: { npc: NpcCharac
448
459
  </button>
449
460
  )}
450
461
  <button className="btn" onClick={onClose}>{t('common.cancel')}</button>
451
- <button className="btn btn-primary" onClick={save}>{t('common.save')}</button>
462
+ <button className="btn btn-primary" type="button" onPointerDown={(event) => event.preventDefault()} onClick={save}>{t('common.save')}</button>
452
463
  </div>
453
464
  </Modal>
454
465
  );
@@ -501,13 +512,10 @@ function WorldBookList({ books, onChanged }: {
501
512
  addToast(t('toast.wbNameContentRequired'), 'error');
502
513
  return;
503
514
  }
504
- if (knowledge) {
505
- try {
506
- JSON.parse(knowledge);
507
- } catch {
508
- addToast(t('toast.knowledgeInvalidJson'), 'error');
509
- return;
510
- }
515
+ const knowledgeInvalidLine = invalidJsonLine(knowledge);
516
+ if (knowledgeInvalidLine != null) {
517
+ addToast(t('toast.knowledgeInvalidJson', { line: knowledgeInvalidLine }), 'error');
518
+ return;
511
519
  }
512
520
  const data = {
513
521
  name: name.trim(),
@@ -791,7 +799,7 @@ function WorldBookForm({
791
799
  </button>
792
800
  )}
793
801
  <button className="btn" onClick={onClose}>{t('common.cancel')}</button>
794
- <button className="btn btn-primary" onClick={() => onSave(name, content, characterBook)}>{t('common.save')}</button>
802
+ <button className="btn btn-primary" type="button" onPointerDown={(event) => event.preventDefault()} onClick={() => onSave(name, content, characterBook)}>{t('common.save')}</button>
795
803
  </div>
796
804
  </>
797
805
  );
package/src/core/i18n.ts CHANGED
@@ -471,13 +471,13 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
471
471
  'toast.importFailed': '导入失败: {msg}',
472
472
  'toast.charDeleted': '已删除角色「{name}」',
473
473
  'toast.nameRequired': '名称不能为空',
474
- 'toast.characterBookInvalidJson': 'Character Book 必须是有效的 JSON',
474
+ 'toast.characterBookInvalidJson': '角色卡知识库 JSON 格式无效(第 {line} 行),请修改后再保存',
475
475
  'toast.charCreated': '已创建角色「{name}」',
476
476
  'toast.charUpdated': '已更新角色「{name}」',
477
477
  'toast.wbDraftImported': '已解析角色卡内的世界书「{name}」,请确认后保存',
478
478
  'toast.wbNameRequired': '世界书名不能为空',
479
479
  'toast.wbNameContentRequired': '世界书名不能为空,且内容与知识库至少填写一项',
480
- 'toast.knowledgeInvalidJson': '知识库必须是有效的 JSON',
480
+ 'toast.knowledgeInvalidJson': '世界书知识库 JSON 格式无效(第 {line} 行),请修改后再保存',
481
481
  'toast.wbImported': '已导入世界书「{name}」({total} 条知识,{enabled} 条启用),请确认后保存',
482
482
  'toast.wbImportFailed': '世界书 JSON 导入失败: {msg}',
483
483
  'toast.wbUpdated': '已更新世界书',
@@ -989,13 +989,13 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
989
989
  'toast.importFailed': '匯入失敗: {msg}',
990
990
  'toast.charDeleted': '已刪除角色「{name}」',
991
991
  'toast.nameRequired': '名稱不能為空',
992
- 'toast.characterBookInvalidJson': 'Character Book 必須是有效的 JSON',
992
+ 'toast.characterBookInvalidJson': 'Character Book JSON 格式無效(第 {line} 行),請修改後再儲存',
993
993
  'toast.charCreated': '已建立角色「{name}」',
994
994
  'toast.charUpdated': '已更新角色「{name}」',
995
995
  'toast.wbDraftImported': '已解析角色卡內的世界書「{name}」,請確認後儲存',
996
996
  'toast.wbNameRequired': '世界書名不能為空',
997
997
  'toast.wbNameContentRequired': '世界書名不能為空,且內容與知識庫至少填寫一項',
998
- 'toast.knowledgeInvalidJson': '知識庫必須是有效的 JSON',
998
+ 'toast.knowledgeInvalidJson': '世界書知識庫 JSON 格式無效(第 {line} 行),請修改後再儲存',
999
999
  'toast.wbImported': '已匯入世界書「{name}」({total} 條知識,{enabled} 條啟用),請確認後儲存',
1000
1000
  'toast.wbImportFailed': '世界書 JSON 匯入失敗: {msg}',
1001
1001
  'toast.wbUpdated': '已更新世界書',
@@ -1505,13 +1505,13 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
1505
1505
  'toast.importFailed': 'Import failed: {msg}',
1506
1506
  'toast.charDeleted': 'Deleted character "{name}"',
1507
1507
  'toast.nameRequired': 'Name cannot be empty',
1508
- 'toast.characterBookInvalidJson': 'Character Book must be valid JSON',
1508
+ 'toast.characterBookInvalidJson': 'Character Book JSON is invalid (line {line}). Correct it before saving.',
1509
1509
  'toast.charCreated': 'Created character "{name}"',
1510
1510
  'toast.charUpdated': 'Updated character "{name}"',
1511
1511
  'toast.wbDraftImported': 'Parsed the Lorebook "{name}" from the character card — review and save',
1512
1512
  'toast.wbNameRequired': 'Lorebook name cannot be empty',
1513
1513
  'toast.wbNameContentRequired': 'Lorebook name is required, and content or knowledge base must be filled in',
1514
- 'toast.knowledgeInvalidJson': 'Knowledge base must be valid JSON',
1514
+ 'toast.knowledgeInvalidJson': 'Lorebook knowledge-base JSON is invalid (line {line}). Correct it before saving.',
1515
1515
  'toast.wbImported': 'Imported world book "{name}" ({total} entries, {enabled} enabled) — review and save',
1516
1516
  'toast.wbImportFailed': 'World book JSON import failed: {msg}',
1517
1517
  'toast.wbUpdated': 'Lorebook updated',
@@ -1,7 +1,47 @@
1
1
  {
2
- "latest": "0.1.25",
2
+ "latest": "0.1.26",
3
3
  "repo": "https://github.com/Rannichan/Tavern-Harness",
4
4
  "entries": [
5
+ {
6
+ "version": "0.1.26",
7
+ "date": "2026-10-27",
8
+ "breaking": false,
9
+ "compare": "v0.1.25...v0.1.26",
10
+ "groups": {
11
+ "zh-CN": [
12
+ {
13
+ "kind": "changed",
14
+ "items": [
15
+ "角色卡与世界书的知识库编辑器现在支持 JSON 语法高亮,键名、字符串、数字等用不同颜色区分,长内容更容易扫读",
16
+ "知识库 JSON 格式有误时,提示会指出具体出错的第几行,方便直接定位修改"
17
+ ]
18
+ },
19
+ {
20
+ "kind": "fixed",
21
+ "items": [
22
+ "修复手机上编辑角色时,切换「角色 / 知识库」标签会收起键盘、底部按钮随之闪烁跳动的问题",
23
+ "修复技能卡片里过长的工具名在手机上撑出屏幕、把操作按钮挤到看不见的问题"
24
+ ]
25
+ }
26
+ ],
27
+ "en": [
28
+ {
29
+ "kind": "changed",
30
+ "items": [
31
+ "The character-card and world-book knowledge editors now highlight JSON syntax, coloring keys, strings, and numbers so long entries are easier to scan",
32
+ "When knowledge-base JSON is invalid, the error now names the exact line, so the problem can be found and fixed directly"
33
+ ]
34
+ },
35
+ {
36
+ "kind": "fixed",
37
+ "items": [
38
+ "Fixed an issue on mobile where switching between the Character and Knowledge tabs while editing a character dismissed the keyboard and made the bottom buttons flicker",
39
+ "Fixed overly long tool names in skill cards overflowing the screen on mobile and pushing the action buttons out of view"
40
+ ]
41
+ }
42
+ ]
43
+ }
44
+ },
5
45
  {
6
46
  "version": "0.1.25",
7
47
  "date": "2026-10-27",
@@ -24,6 +24,7 @@
24
24
  --font-size-2xs: 9.5px;
25
25
  --font-size-xs: 10.5px;
26
26
  --font-size-sm: 11px;
27
+ --font-size-caption: 11.5px;
27
28
  --font-size-md: 12px;
28
29
  --font-size-body-sm: 12.5px;
29
30
  --font-size-body: 14px;
@@ -105,7 +105,23 @@
105
105
  flex: 1;
106
106
  min-height: 0;
107
107
  }
108
- .modal-editor .modal-body.knowledge-tab {
108
+ .character-editor-body {
109
+ overflow: hidden;
110
+ }
111
+ .character-editor-tab-panel {
112
+ display: none;
113
+ min-height: 0;
114
+ }
115
+ .character-editor-tab-panel.active {
116
+ display: flex;
117
+ flex: 1;
118
+ min-height: 0;
119
+ flex-direction: column;
120
+ gap: 12px;
121
+ overflow-y: auto;
122
+ overscroll-behavior: contain;
123
+ }
124
+ .character-editor-tab-panel.knowledge-tab {
109
125
  overflow: hidden;
110
126
  }
111
127
  .modal-editor .grow-textarea {
@@ -161,6 +177,11 @@
161
177
  }
162
178
  .character-book-editor .cm-content {
163
179
  padding: 9px 12px;
180
+ caret-color: var(--text-primary);
181
+ }
182
+ /* CodeMirror 的默认光标颜色为黑色,不会随应用主题变量切换。 */
183
+ .character-book-editor .cm-cursor {
184
+ border-left-color: var(--text-primary) !important;
164
185
  }
165
186
  .character-book-editor .cm-gutters {
166
187
  border-right: 1px solid var(--border-soft);
@@ -514,11 +535,14 @@
514
535
  flex-shrink: 0;
515
536
  }
516
537
  .list-item .l-sub {
517
- font-size: var(--font-size-body-sm);
538
+ font-size: var(--font-size-caption);
518
539
  color: var(--text-muted);
519
540
  font-family: var(--font-family-mono);
520
541
  }
521
542
  .list-item.sortable-skill .l-sub {
543
+ min-width: 0;
544
+ overflow-wrap: anywhere;
545
+ word-break: break-word;
522
546
  color: var(--text-secondary);
523
547
  }
524
548
 
@@ -1388,10 +1412,41 @@
1388
1412
  gap: 4px;
1389
1413
  }
1390
1414
 
1415
+ .list-item.sortable-skill {
1416
+ min-width: 0;
1417
+ }
1418
+
1391
1419
  .skill-item-actions .btn {
1392
1420
  padding: 5px 7px;
1393
1421
  }
1394
1422
 
1423
+ /* 保持隐藏知识库编辑器在布局树中:若用 display:none 隐藏一个仍有
1424
+ 焦点的 CodeMirror,移动浏览器会收起键盘并重算动态视口,造成底部按钮闪烁。 */
1425
+ .character-editor-body {
1426
+ position: relative;
1427
+ }
1428
+
1429
+ .character-editor-tab-panel {
1430
+ position: absolute;
1431
+ inset: 16px 14px;
1432
+ display: flex;
1433
+ flex-direction: column;
1434
+ opacity: 0;
1435
+ pointer-events: none;
1436
+ overflow: hidden;
1437
+ }
1438
+
1439
+ .character-editor-tab-panel.active {
1440
+ position: absolute;
1441
+ display: flex;
1442
+ opacity: 1;
1443
+ pointer-events: auto;
1444
+ }
1445
+
1446
+ .character-editor-tab-panel:not(.active) {
1447
+ overflow-y: auto;
1448
+ }
1449
+
1395
1450
  .character-editor-modal .modal-head {
1396
1451
  grid-template-columns: minmax(0, 1fr) auto;
1397
1452
  }