tavern-harness 0.1.18 → 0.1.19

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,30 @@
3
3
  > 本文件由 `scripts/changelog.mjs` 从 `changelog/v*.md` 生成,请勿手动编辑。
4
4
  > 版本对比链接见文末;源文件位于仓库 `changelog/` 目录。
5
5
 
6
+ ## [0.1.19] - 2026-10-25
7
+
8
+ ### 中文
9
+
10
+ #### 新增
11
+
12
+ - 新增四项「首次行动」成就:首次导出游戏(独乐乐不如众乐乐)、首次导入游戏(发现者)、首次创建角色卡或世界书(创造者)、首次创建自定义技能(探索者)
13
+
14
+ #### 改进
15
+
16
+ - 聊天标题栏更简洁:副标题只保留上下文用量,不再重复显示会话模式
17
+ - 手机端标题栏按钮区支持横向滑动,按钮较多时不再被挤出屏幕或误触页面
18
+
19
+ ### English
20
+
21
+ #### Added
22
+
23
+ - Four new first-action achievements: export a game (Sharing Is Caring), import a game (Discoverer), create a character card or Lorebook (Creator), and create a custom skill (Explorer)
24
+
25
+ #### Changed
26
+
27
+ - The chat header is cleaner: the subtitle now shows only context usage instead of repeating the session mode
28
+ - On mobile, the header action row scrolls horizontally, so buttons are no longer pushed off screen or trigger page gestures
29
+
6
30
  ## [0.1.18] - 2026-10-25
7
31
 
8
32
  ### 中文
@@ -528,6 +552,7 @@
528
552
  - Vite + React + Zustand + Dexie stack with browser-side local persistence
529
553
 
530
554
  <!-- 链接定义 -->
555
+ [0.1.19]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.18...v0.1.19
531
556
  [0.1.18]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.17...v0.1.18
532
557
  [0.1.17]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.16...v0.1.17
533
558
  [0.1.16]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.15...v0.1.16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tavern-harness",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "description": "Tavern Harness — 酒馆模式下的本地 AI Agent 助手(MyAgent-Android 的 Web 复刻)",
5
5
  "private": false,
6
6
  "type": "module",
package/src/App.tsx CHANGED
@@ -293,7 +293,6 @@ function SessionHeader({
293
293
  const [exportOpen, setExportOpen] = useState(false);
294
294
  const [showWorkspace, setShowWorkspace] = useState(false);
295
295
 
296
- const modeLabel = session.mode === 'STANDARD' ? t('header.modeStandard') : session.mode === 'NPC' ? t('header.modeNpc') : t('header.modeGroup');
297
296
  const messages = useStore((s) => s.messages[session.id!] ?? []);
298
297
  const contextTokens = useMemo(() => {
299
298
  const assistantMessages = messages.filter((message) => message.role === 'assistant' && message.totalTokens > 0);
@@ -321,15 +320,11 @@ function SessionHeader({
321
320
  <SessionVisual mode={session.mode} npcName={npcRef?.name ?? undefined} hue={npcRef?.avatarColorOrdinal ?? 0} imageUrl={npcRef?.avatarDataUrl} members={session.mode === 'GROUP' ? groupMemberAvatars : undefined} size="lg" />
322
321
  <div className="tinfo">
323
322
  <div className="ttitle">{session.title}</div>
324
- <div className="tsub">
325
- <span className="tsub-mode">{modeLabel}</span>
326
- {contextTokens != null && (
327
- <span className="tsub-context-group">
328
- <span>·</span>
329
- <span className="tsub-mode">{t('nav.contextUsage', { tokens: formatContextTokens(contextTokens) })}</span>
330
- </span>
331
- )}
332
- </div>
323
+ {contextTokens != null && (
324
+ <div className="tsub">
325
+ <span className="tsub-mode">{t('nav.contextUsage', { tokens: formatContextTokens(contextTokens) })}</span>
326
+ </div>
327
+ )}
333
328
  </div>
334
329
  <div className="chat-actions">
335
330
  <button className="btn-ghost icon-tooltip" title={t(usesPublicWorkspace ? 'header.workspaceRootHint' : 'header.workspaceHint')} onClick={() => setShowWorkspace(true)}>
@@ -29,6 +29,7 @@ import { textPreview } from '../core/textPreview';
29
29
  import { AVATAR_STORAGE_SIZE, shrinkAvatarDataUrl } from '../core/avatarThumbnail';
30
30
  import { ALL_BUILTIN_TOOL_NAMES } from '../core/toolDefinitions';
31
31
  import { useT } from '../core/i18n';
32
+ import { unlockAchievement } from '../core/achievements';
32
33
 
33
34
  const CharacterBookEditor = lazy(() => import('./CharacterBookEditor'));
34
35
 
@@ -297,6 +298,7 @@ function CharacterEditorModal({ npc, isNew, onClose, onSaved }: { npc: NpcCharac
297
298
  ...npc,
298
299
  ...npcData,
299
300
  });
301
+ await unlockAchievement('creator');
300
302
  addToast(t('toast.charCreated', { name }));
301
303
  } else {
302
304
  await db.npcs.update(npc.id!, npcData);
@@ -517,6 +519,7 @@ function WorldBookList({ books, onChanged }: {
517
519
  addToast(t('toast.wbUpdated'));
518
520
  } else {
519
521
  await db.worldBooks.add({ ...data, imageUri: null, createdAt: Date.now() });
522
+ await unlockAchievement('creator');
520
523
  addToast(t('toast.wbCreated'));
521
524
  }
522
525
  setEditing(null);
@@ -3,6 +3,7 @@ import { useStore } from '../store/store';
3
3
  import { Icon, Modal } from './shared';
4
4
  import { useT } from '../core/i18n';
5
5
  import { exportGameplay, importGameplay } from '../core/gameplay';
6
+ import { unlockAchievement } from '../core/achievements';
6
7
 
7
8
  // ============================================================
8
9
  // 游戏导出 / 导入
@@ -35,6 +36,7 @@ export function GameplayExportModal({
35
36
  if (result === 'canceled') {
36
37
  addToast(t('toast.exportCanceled'));
37
38
  } else {
39
+ await unlockAchievement('game-exporter');
38
40
  addToast(t('toast.gameplayExported'));
39
41
  }
40
42
  onClose();
@@ -125,6 +127,7 @@ export function GameplayImportModal({
125
127
  setBusy(true);
126
128
  try {
127
129
  const result = await importGameplay(pendingData.current);
130
+ await unlockAchievement('game-importer');
128
131
  pendingData.current = null;
129
132
  setSelectedName(null);
130
133
  addToast(
@@ -22,6 +22,10 @@ export const ACHIEVEMENTS: AchievementDef[] = [
22
22
  { id: 'night-owl', name: '午夜不归人', icon: '🌙', threshold: 1_000_000, description: '累计使用 1,000,000 Token' },
23
23
  { id: 'legend', name: '酒馆传奇', icon: '🏆', threshold: 100_000_000, description: '累计使用 100,000,000 Token' },
24
24
  { id: 'th-shareholder', name: 'TH股东', icon: '📈', iconUrl: '/tavern-harness-logo.png', description: '连续点击首页图标 5 次' },
25
+ { id: 'game-exporter', name: '独乐乐不如众乐乐', icon: '📤', description: '首次导出游戏' },
26
+ { id: 'game-importer', name: '发现者', icon: '📥', description: '首次导入游戏' },
27
+ { id: 'creator', name: '创造者', icon: '🎨', description: '首次创建角色卡或世界书' },
28
+ { id: 'explorer', name: '探索者', icon: '🧩', description: '首次创建自定义技能' },
25
29
  ];
26
30
 
27
31
  /** 生涯总 token(输入 + 输出) */
package/src/core/i18n.ts CHANGED
@@ -384,6 +384,14 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
384
384
  'ach.legend.desc': '累计使用 100,000,000 Token',
385
385
  'ach.thShareholder.name': 'TH股东',
386
386
  'ach.thShareholder.desc': '5 秒内连续点击首页图标 5 次',
387
+ 'ach.gameExporter.name': '独乐乐不如众乐乐',
388
+ 'ach.gameExporter.desc': '首次导出游戏',
389
+ 'ach.gameImporter.name': '发现者',
390
+ 'ach.gameImporter.desc': '首次导入游戏',
391
+ 'ach.creator.name': '创造者',
392
+ 'ach.creator.desc': '首次创建角色卡或世界书',
393
+ 'ach.explorer.name': '探索者',
394
+ 'ach.explorer.desc': '首次创建自定义技能',
387
395
 
388
396
  // ---- 成就解锁弹窗 ----
389
397
  'achModal.title': '🎉 成就解锁!',
@@ -891,6 +899,14 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
891
899
  'ach.legend.desc': '累計使用 100,000,000 Token',
892
900
  'ach.thShareholder.name': 'TH股東',
893
901
  'ach.thShareholder.desc': '5 秒內連續點擊首頁圖示 5 次',
902
+ 'ach.gameExporter.name': '獨樂樂不如眾樂樂',
903
+ 'ach.gameExporter.desc': '首次匯出遊戲',
904
+ 'ach.gameImporter.name': '發現者',
905
+ 'ach.gameImporter.desc': '首次匯入遊戲',
906
+ 'ach.creator.name': '創造者',
907
+ 'ach.creator.desc': '首次建立角色卡或世界書',
908
+ 'ach.explorer.name': '探索者',
909
+ 'ach.explorer.desc': '首次建立自訂技能',
894
910
 
895
911
  'achModal.title': '🎉 成就解鎖!',
896
912
  'achModal.total': '生涯已累計 {n} Tokens',
@@ -1392,6 +1408,14 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
1392
1408
  'ach.legend.desc': 'Cumulatively used 100,000,000 tokens',
1393
1409
  'ach.thShareholder.name': 'TH Shareholder',
1394
1410
  'ach.thShareholder.desc': 'Clicked the home icon 5 times within 5 seconds',
1411
+ 'ach.gameExporter.name': 'Sharing Is Caring',
1412
+ 'ach.gameExporter.desc': 'Exported a game for the first time',
1413
+ 'ach.gameImporter.name': 'Discoverer',
1414
+ 'ach.gameImporter.desc': 'Imported a game for the first time',
1415
+ 'ach.creator.name': 'Creator',
1416
+ 'ach.creator.desc': 'Created a character card or Lorebook for the first time',
1417
+ 'ach.explorer.name': 'Explorer',
1418
+ 'ach.explorer.desc': 'Created a custom skill for the first time',
1395
1419
 
1396
1420
  'achModal.title': '🎉 Achievement unlocked!',
1397
1421
  'achModal.total': 'Lifetime total {n} tokens',
@@ -9,6 +9,7 @@ import type {
9
9
  } from '../../types/models';
10
10
  import { BUILTIN_TOOL_NAMES } from '../toolDefinitions';
11
11
  import { translate } from '../i18n';
12
+ import { unlockAchievement } from '../achievements';
12
13
  import { rollDice } from './builtinTools';
13
14
  import {
14
15
  createSessionWorkspaceDir,
@@ -409,6 +410,7 @@ async function handleCreateSkill(args: Record<string, unknown>): Promise<string>
409
410
  if (exists) return `ERROR: Skill ${name} already exists`;
410
411
  const tool = await createSkillTool(name, String(args.description ?? ''), (args.parameters as Record<string, unknown>) ?? {}, args.execution);
411
412
  await db.tools.add(tool);
413
+ await unlockAchievement('explorer');
412
414
  return translate('tool.skillCreated', { name });
413
415
  }
414
416
 
@@ -493,6 +495,7 @@ async function handleCreateCharacter(args: Record<string, unknown>): Promise<str
493
495
  isBuiltIn: false,
494
496
  createdAt: Date.now(),
495
497
  });
498
+ await unlockAchievement('creator');
496
499
  return translate('tool.characterCreated', { name });
497
500
  }
498
501
 
@@ -638,6 +641,7 @@ async function handleCreateLorebook(args: Record<string, unknown>): Promise<stri
638
641
  if (!name || !content) return 'ERROR: name and content are required';
639
642
  if (await db.worldBooks.where('name').equals(name).first()) return `ERROR: Lorebook ${name} already exists`;
640
643
  await db.worldBooks.add({ name, content: content.slice(0, 10_000), imageUri: null, createdAt: Date.now() });
644
+ await unlockAchievement('creator');
641
645
  return translate('tool.lorebookCreated', { name });
642
646
  }
643
647
 
@@ -1,7 +1,45 @@
1
1
  {
2
- "latest": "0.1.18",
2
+ "latest": "0.1.19",
3
3
  "repo": "https://github.com/Rannichan/Tavern-Harness",
4
4
  "entries": [
5
+ {
6
+ "version": "0.1.19",
7
+ "date": "2026-10-25",
8
+ "breaking": false,
9
+ "compare": "v0.1.18...v0.1.19",
10
+ "groups": {
11
+ "zh-CN": [
12
+ {
13
+ "kind": "added",
14
+ "items": [
15
+ "新增四项「首次行动」成就:首次导出游戏(独乐乐不如众乐乐)、首次导入游戏(发现者)、首次创建角色卡或世界书(创造者)、首次创建自定义技能(探索者)"
16
+ ]
17
+ },
18
+ {
19
+ "kind": "changed",
20
+ "items": [
21
+ "聊天标题栏更简洁:副标题只保留上下文用量,不再重复显示会话模式",
22
+ "手机端标题栏按钮区支持横向滑动,按钮较多时不再被挤出屏幕或误触页面"
23
+ ]
24
+ }
25
+ ],
26
+ "en": [
27
+ {
28
+ "kind": "added",
29
+ "items": [
30
+ "Four new first-action achievements: export a game (Sharing Is Caring), import a game (Discoverer), create a character card or Lorebook (Creator), and create a custom skill (Explorer)"
31
+ ]
32
+ },
33
+ {
34
+ "kind": "changed",
35
+ "items": [
36
+ "The chat header is cleaner: the subtitle now shows only context usage instead of repeating the session mode",
37
+ "On mobile, the header action row scrolls horizontally, so buttons are no longer pushed off screen or trigger page gestures"
38
+ ]
39
+ }
40
+ ]
41
+ }
42
+ },
5
43
  {
6
44
  "version": "0.1.18",
7
45
  "date": "2026-10-25",
@@ -1804,11 +1804,14 @@ hr.sep {
1804
1804
  .chat-actions {
1805
1805
  display: flex;
1806
1806
  flex: 0 1 auto;
1807
+ min-width: 0;
1807
1808
  justify-content: flex-end;
1808
1809
  gap: 0;
1809
1810
  max-width: none;
1810
1811
  overflow-x: auto;
1812
+ overscroll-behavior-x: contain;
1811
1813
  scrollbar-width: none;
1814
+ touch-action: pan-x;
1812
1815
  }
1813
1816
 
1814
1817
  .chat-actions::-webkit-scrollbar {