tavern-harness 0.1.20 → 0.1.22

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,46 @@
3
3
  > 本文件由 `scripts/changelog.mjs` 从 `changelog/v*.md` 生成,请勿手动编辑。
4
4
  > 版本对比链接见文末;源文件位于仓库 `changelog/` 目录。
5
5
 
6
+ ## [0.1.22] - 2026-10-26
7
+
8
+ ### 中文
9
+
10
+ #### 改进
11
+
12
+ - 生成参数默认锁定:「设置 → 生成参数」里的滑杆和下拉框要先点「解锁」才能调整,离开设置页后会自动重新锁定,避免滑动页面时误改温度、上下文压缩等参数
13
+ - 「重置」按钮在锁定状态下不可点击,解锁后才能恢复默认值
14
+ - 会话列表的标题和副标题只在内容确实溢出时才做末尾渐隐,短标题不再被淡边影响
15
+ - 移动端顶部导航按钮之间的间距略微加大,点击时更不容易误触相邻按钮
16
+
17
+ ### English
18
+
19
+ #### Changed
20
+
21
+ - Generation parameters are now locked by default: the sliders and dropdowns under Settings → Generation parameters stay read-only until you tap "Unlock", and they lock again when you leave the settings page, so scrolling can no longer change temperature, context compression, and other values by accident
22
+ - The "Reset" button is disabled while parameters are locked and becomes available after unlocking
23
+ - Session list titles and subtitles only fade out at the end when the text actually overflows, so short titles are no longer dimmed by the mask
24
+ - Mobile top-bar navigation buttons have a little more spacing, reducing accidental taps on neighbours
25
+
26
+ ## [0.1.21] - 2026-10-26
27
+
28
+ ### 中文
29
+
30
+ #### 改进
31
+
32
+ - 移动端会话头部的操作按钮收进「更多操作」菜单(工作区文件、编辑对话、重置对话、导出游玩记录、删除对话),头部不再拥挤;主题切换按钮仍然直接显示
33
+ - 移动端会话标题可用的横向空间明显变大,长标题能显示更多内容
34
+ - 会话列表中的标题和副标题过长时改为末尾渐隐,不再用省略号生硬截断
35
+ - 会话头部的上下文用量标签字号略微收小,和标题搭配更协调
36
+
37
+ ### English
38
+
39
+ #### Changed
40
+
41
+ - On mobile, session header actions now collapse into a "More actions" menu (workspace files, edit, reset, export gameplay, delete), so the header is no longer cramped; the theme toggle stays one tap away
42
+ - Mobile session titles get noticeably more horizontal room, so long titles reveal more of their text
43
+ - Long session list titles and subtitles now fade out at the end instead of being cut off with an ellipsis
44
+ - The context usage badge in the session header uses a slightly smaller font so it sits better next to the title
45
+
6
46
  ## [0.1.20] - 2026-10-26
7
47
 
8
48
  ### 中文
@@ -588,6 +628,8 @@
588
628
  - Vite + React + Zustand + Dexie stack with browser-side local persistence
589
629
 
590
630
  <!-- 链接定义 -->
631
+ [0.1.22]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.21...v0.1.22
632
+ [0.1.21]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.20...v0.1.21
591
633
  [0.1.20]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.19...v0.1.20
592
634
  [0.1.19]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.18...v0.1.19
593
635
  [0.1.18]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.17...v0.1.18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tavern-harness",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Tavern Harness — 酒馆模式下的本地 AI Agent 助手(MyAgent-Android 的 Web 复刻)",
5
5
  "private": false,
6
6
  "type": "module",
package/src/App.tsx CHANGED
@@ -327,6 +327,8 @@ function SessionHeader({
327
327
  const [confirmDelete, setConfirmDelete] = useState(false);
328
328
  const [exportOpen, setExportOpen] = useState(false);
329
329
  const [showWorkspace, setShowWorkspace] = useState(false);
330
+ const [showMobileActions, setShowMobileActions] = useState(false);
331
+ const mobileActionsRef = useRef<HTMLDivElement>(null);
330
332
 
331
333
  const messages = useStore((s) => s.messages[session.id!] ?? []);
332
334
  const contextTokens = useMemo(() => {
@@ -350,6 +352,17 @@ function SessionHeader({
350
352
  || (themeMode === 'system' && document.documentElement.dataset.theme === 'dark');
351
353
  const toggleTheme = () => void setSettings({ themeMode: isDarkTheme ? 'light' : 'dark' });
352
354
 
355
+ useEffect(() => {
356
+ if (!showMobileActions) return;
357
+ const closeOnOutsidePointerDown = (event: PointerEvent) => {
358
+ if (!mobileActionsRef.current?.contains(event.target as Node)) {
359
+ setShowMobileActions(false);
360
+ }
361
+ };
362
+ document.addEventListener('pointerdown', closeOnOutsidePointerDown);
363
+ return () => document.removeEventListener('pointerdown', closeOnOutsidePointerDown);
364
+ }, [showMobileActions]);
365
+
353
366
  return (
354
367
  <div className="chat-header">
355
368
  <SessionVisual mode={session.mode} npcName={npcRef?.name ?? undefined} hue={npcRef?.avatarColorOrdinal ?? 0} imageUrl={npcRef?.avatarDataUrl} members={session.mode === 'GROUP' ? groupMemberAvatars : undefined} size="lg" />
@@ -361,7 +374,7 @@ function SessionHeader({
361
374
  </div>
362
375
  )}
363
376
  </div>
364
- <div className="chat-actions">
377
+ <div className="chat-actions" ref={mobileActionsRef}>
365
378
  <button className="btn-ghost icon-tooltip" title={t(usesPublicWorkspace ? 'header.workspaceRootHint' : 'header.workspaceHint')} onClick={() => setShowWorkspace(true)}>
366
379
  <Icon name="folder" size={17} />
367
380
  </button>
@@ -378,7 +391,17 @@ function SessionHeader({
378
391
  <Icon name="trash" size={17} />
379
392
  </button>
380
393
  <button
381
- className="btn-ghost icon-tooltip"
394
+ className="btn-ghost icon-tooltip chat-actions-more"
395
+ type="button"
396
+ title={t('header.moreActions')}
397
+ aria-label={t('header.moreActions')}
398
+ aria-expanded={showMobileActions}
399
+ onClick={() => setShowMobileActions(!showMobileActions)}
400
+ >
401
+ <Icon name="more" size={18} />
402
+ </button>
403
+ <button
404
+ className="btn-ghost icon-tooltip chat-theme-toggle"
382
405
  type="button"
383
406
  title={t(isDarkTheme ? 'header.switchToLightTip' : 'header.switchToDarkTip')}
384
407
  aria-label={t(isDarkTheme ? 'header.switchToLightTip' : 'header.switchToDarkTip')}
@@ -386,6 +409,25 @@ function SessionHeader({
386
409
  >
387
410
  <Icon name={isDarkTheme ? 'sun' : 'moon'} size={17} />
388
411
  </button>
412
+ {showMobileActions && (
413
+ <div className="chat-actions-menu card">
414
+ <button onClick={() => { setShowWorkspace(true); setShowMobileActions(false); }}>
415
+ <Icon name="folder" size={15} /> {t('header.workspaceAction')}
416
+ </button>
417
+ <button onClick={() => { setShowEdit(true); setShowMobileActions(false); }}>
418
+ <Icon name="pencil" size={15} /> {t('nav.editSession')}
419
+ </button>
420
+ <button onClick={() => { setConfirmReset(true); setShowMobileActions(false); }}>
421
+ <Icon name="refresh" size={15} /> {t('header.resetSessionTip')}
422
+ </button>
423
+ <button onClick={() => { setExportOpen(true); setShowMobileActions(false); }}>
424
+ <Icon name="share" size={15} /> {t('nav.exportGameplay')}
425
+ </button>
426
+ <button className="danger" onClick={() => { setConfirmDelete(true); setShowMobileActions(false); }}>
427
+ <Icon name="trash" size={15} /> {t('header.deleteTip')}
428
+ </button>
429
+ </div>
430
+ )}
389
431
  </div>
390
432
 
391
433
  {showEdit && <NewSessionMenu editingSession={session} onClose={() => setShowEdit(false)} />}
@@ -1,4 +1,4 @@
1
- import { memo, useState } from 'react';
1
+ import { memo, useEffect, useState } 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,6 +24,8 @@ 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
29
  const t = useT();
28
30
  const hasUnseenVersion = useHasUnseenVersion();
29
31
 
@@ -38,6 +40,7 @@ export const SettingsView = memo(function SettingsView() {
38
40
  max={max}
39
41
  step={step}
40
42
  value={settings[key] as number}
43
+ disabled={generationParamsLocked}
41
44
  onChange={(e) => setSettings({ [key]: parseFloat(e.target.value) })}
42
45
  />
43
46
  <span className="range-val">{fmt ? fmt(settings[key] as number) : (settings[key] as number)}</span>
@@ -52,6 +55,11 @@ export const SettingsView = memo(function SettingsView() {
52
55
  await setSettings({ ...generationDefaults });
53
56
  addToast(t('toast.genParamsReset'));
54
57
  };
58
+ const toggleGenerationParamsLock = () => setGenerationParamsLocked((locked) => !locked);
59
+
60
+ useEffect(() => {
61
+ if (activeView !== 'settings') setGenerationParamsLocked(true);
62
+ }, [activeView]);
55
63
 
56
64
  return (
57
65
  <div className="view-page">
@@ -104,15 +112,26 @@ export const SettingsView = memo(function SettingsView() {
104
112
  <div className="set-section">
105
113
  <div className="settings-section-head">
106
114
  <h3>{t('settings.genParams')}</h3>
107
- <button
108
- className="btn btn-sm settings-gen-reset"
109
- title={t('settings.genParamsResetTitle')}
110
- aria-label={t('settings.genParamsResetTitle')}
111
- disabled={isGenerationDefault}
112
- onClick={() => void resetGenerationParams()}
113
- >
114
- <Icon name="refresh" size={12} /> {t('settings.genParamsReset')}
115
- </button>
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>
116
135
  </div>
117
136
  <table className="table param-table">
118
137
  <tbody>
@@ -178,6 +197,7 @@ export const SettingsView = memo(function SettingsView() {
178
197
  max="100"
179
198
  step="1"
180
199
  value={settings.contextCompressionThreshold === 'off' ? 0 : settings.contextCompressionThreshold}
200
+ disabled={generationParamsLocked}
181
201
  onChange={(e) => setSettings({ contextCompressionThreshold: Number(e.target.value) || 'off' })}
182
202
  />
183
203
  <span className="range-val">
@@ -195,6 +215,7 @@ export const SettingsView = memo(function SettingsView() {
195
215
  <select
196
216
  className="select"
197
217
  value={settings.reasoningEffort}
218
+ disabled={generationParamsLocked}
198
219
  onChange={(e) => setSettings({ reasoningEffort: e.target.value as ReasoningEffort })}
199
220
  >
200
221
  {['auto', 'off', 'low', 'medium', 'xhigh'].map((r) => <option key={r} value={r}>{r}</option>)}
@@ -1,4 +1,4 @@
1
- import { useEffect, useMemo, useRef, useState } from 'react';
1
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState, type ComponentPropsWithoutRef, type ReactNode } from 'react';
2
2
  import { createPortal } from 'react-dom';
3
3
  import { QRCodeSVG } from 'qrcode.react';
4
4
  import { useStore } from '../store/store';
@@ -19,6 +19,23 @@ const SHAREHOLDER_CLICK_COUNT = 5;
19
19
  const SHAREHOLDER_CLICK_WINDOW_MS = 5_000;
20
20
  const SIDEBAR_MAX = 520;
21
21
 
22
+ function FadeOverflowText({ className, children, ...props }: ComponentPropsWithoutRef<'span'> & { children: ReactNode }) {
23
+ const ref = useRef<HTMLSpanElement>(null);
24
+ const [overflowing, setOverflowing] = useState(false);
25
+
26
+ useLayoutEffect(() => {
27
+ const element = ref.current;
28
+ if (!element) return;
29
+ const updateOverflow = () => setOverflowing(element.scrollWidth > element.clientWidth);
30
+ updateOverflow();
31
+ const observer = new ResizeObserver(updateOverflow);
32
+ observer.observe(element);
33
+ return () => observer.disconnect();
34
+ }, [children]);
35
+
36
+ return <span ref={ref} className={`${className ?? ''}${overflowing ? ' has-overflow' : ''}`} {...props}>{children}</span>;
37
+ }
38
+
22
39
  /** 触发 App 级全局游戏导出弹窗(会话右键菜单共用) */
23
40
  function openGlobalGameplayExport(sessionId: number, includeHistory: boolean) {
24
41
  window.dispatchEvent(
@@ -409,10 +426,10 @@ export function Sidebar() {
409
426
  {!!(s as { pinned?: unknown }).pinned && (
410
427
  <span className="spinned" title={t('nav.pinSession')}><Icon name="pin" size={11} /></span>
411
428
  )}
412
- <span className="stitle-text">{s.title}</span>
429
+ <FadeOverflowText className="stitle-text">{s.title}</FadeOverflowText>
413
430
  {hasActivity && <span className="session-activity-dot" aria-label={t('nav.sessionActive')} />}
414
431
  </div>
415
- <div className="sprev">{s.lastMessage || t('nav.newConversation')}</div>
432
+ <FadeOverflowText className="sprev">{s.lastMessage || t('nav.newConversation')}</FadeOverflowText>
416
433
  </div>
417
434
  </button>
418
435
  );
@@ -409,6 +409,8 @@ const ICONS: Record<string, React.ReactNode> = {
409
409
  upload: <><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><path d="m17 8-5-5-5 5M12 3v12" /></>,
410
410
  import: <><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><path d="M17 14l-5 5-5-5M12 19V7" /></>,
411
411
  pin: <><path d="M9 4h6M12 4v3l4 3v3h-8v-3l4-3" /><path d="M12 13v7" /></>,
412
+ lock: <><rect x="5" y="10" width="14" height="11" rx="2" /><path d="M8 10V7a4 4 0 0 1 8 0v3" /></>,
413
+ unlock: <><rect x="5" y="10" width="14" height="11" rx="2" /><path d="M8 10V7a4 4 0 0 1 7.4-1.9" /></>,
412
414
  'git-branch': <><path d="M6 3v12" /><circle cx="18" cy="6" r="3" /><circle cx="6" cy="18" r="3" /><path d="M18 9a9 9 0 0 1-9 9" /></>,
413
415
  'zoom-in': <><circle cx="11" cy="11" r="7" /><path d="m21 21-4.35-4.35M11 8v6M8 11h6" /></>,
414
416
  'zoom-out': <><circle cx="11" cy="11" r="7" /><path d="m21 21-4.35-4.35M8 11h6" /></>,
package/src/core/i18n.ts CHANGED
@@ -81,6 +81,8 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
81
81
  'header.modeStandard': '标准对话',
82
82
  'header.modeNpc': '角色对话',
83
83
  'header.modeGroup': '群聊',
84
+ 'header.moreActions': '更多操作',
85
+ 'header.workspaceAction': '工作区文件',
84
86
  'header.editSessionTip': '编辑对话设置',
85
87
  'header.resetSessionTip': '重置对话',
86
88
  'header.resetSessionTitle': '重置对话',
@@ -200,6 +202,10 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
200
202
  'settings.yoloMode': 'YOLO 模式',
201
203
  'settings.yoloModeDesc': '自动批准工具修改和 Shell 执行,无需逐次确认。仅在信任角色和技能时开启。',
202
204
  'settings.genParams': '🎛 生成参数',
205
+ 'settings.genParamsLocked': '已锁定',
206
+ 'settings.genParamsUnlocked': '已解锁',
207
+ 'settings.genParamsLock': '锁定生成参数',
208
+ 'settings.genParamsUnlock': '解锁生成参数',
203
209
  'settings.genParamsReset': '重置',
204
210
  'settings.genParamsResetTitle': '将生成参数恢复为默认值',
205
211
  'settings.tempDesc': '创造性与随机性',
@@ -605,6 +611,8 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
605
611
  'header.modeStandard': '標準對話',
606
612
  'header.modeNpc': '角色對話',
607
613
  'header.modeGroup': '群聊',
614
+ 'header.moreActions': '更多操作',
615
+ 'header.workspaceAction': '工作區檔案',
608
616
  'header.editSessionTip': '編輯對話設定',
609
617
  'header.resetSessionTip': '重置對話',
610
618
  'header.resetSessionTitle': '重置對話',
@@ -721,6 +729,10 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
721
729
  'settings.yoloMode': 'YOLO 模式',
722
730
  'settings.yoloModeDesc': '自動批准工具修改和 Shell 執行,無需逐次確認。僅在信任角色和技能時開啟。',
723
731
  'settings.genParams': '🎛 產生參數',
732
+ 'settings.genParamsLocked': '已鎖定',
733
+ 'settings.genParamsUnlocked': '已解鎖',
734
+ 'settings.genParamsLock': '鎖定產生參數',
735
+ 'settings.genParamsUnlock': '解鎖產生參數',
724
736
  'settings.genParamsReset': '重設',
725
737
  'settings.genParamsResetTitle': '將產生參數恢復為預設值',
726
738
  'settings.tempDesc': '創造性與隨機性',
@@ -1117,6 +1129,8 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
1117
1129
  'header.modeStandard': 'Standard chat',
1118
1130
  'header.modeNpc': 'Character chat',
1119
1131
  'header.modeGroup': 'Group chat',
1132
+ 'header.moreActions': 'More actions',
1133
+ 'header.workspaceAction': 'Workspace files',
1120
1134
  'header.editSessionTip': 'Edit conversation settings',
1121
1135
  'header.resetSessionTip': 'Reset conversation',
1122
1136
  'header.resetSessionTitle': 'Reset conversation',
@@ -1233,6 +1247,10 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
1233
1247
  'settings.yoloMode': 'YOLO mode',
1234
1248
  'settings.yoloModeDesc': 'Automatically approve tool changes and Shell commands without asking each time. Only enable for trusted characters and skills.',
1235
1249
  'settings.genParams': '🎛 Generation parameters',
1250
+ 'settings.genParamsLocked': 'Locked',
1251
+ 'settings.genParamsUnlocked': 'Unlocked',
1252
+ 'settings.genParamsLock': 'Lock generation parameters',
1253
+ 'settings.genParamsUnlock': 'Unlock generation parameters',
1236
1254
  'settings.genParamsReset': 'Reset',
1237
1255
  'settings.genParamsResetTitle': 'Restore generation parameters to their default values',
1238
1256
  'settings.tempDesc': 'Creativity & randomness',
@@ -1,7 +1,67 @@
1
1
  {
2
- "latest": "0.1.20",
2
+ "latest": "0.1.22",
3
3
  "repo": "https://github.com/Rannichan/Tavern-Harness",
4
4
  "entries": [
5
+ {
6
+ "version": "0.1.22",
7
+ "date": "2026-10-26",
8
+ "breaking": false,
9
+ "compare": "v0.1.21...v0.1.22",
10
+ "groups": {
11
+ "zh-CN": [
12
+ {
13
+ "kind": "changed",
14
+ "items": [
15
+ "生成参数默认锁定:「设置 → 生成参数」里的滑杆和下拉框要先点「解锁」才能调整,离开设置页后会自动重新锁定,避免滑动页面时误改温度、上下文压缩等参数",
16
+ "「重置」按钮在锁定状态下不可点击,解锁后才能恢复默认值",
17
+ "会话列表的标题和副标题只在内容确实溢出时才做末尾渐隐,短标题不再被淡边影响",
18
+ "移动端顶部导航按钮之间的间距略微加大,点击时更不容易误触相邻按钮"
19
+ ]
20
+ }
21
+ ],
22
+ "en": [
23
+ {
24
+ "kind": "changed",
25
+ "items": [
26
+ "Generation parameters are now locked by default: the sliders and dropdowns under Settings → Generation parameters stay read-only until you tap \"Unlock\", and they lock again when you leave the settings page, so scrolling can no longer change temperature, context compression, and other values by accident",
27
+ "The \"Reset\" button is disabled while parameters are locked and becomes available after unlocking",
28
+ "Session list titles and subtitles only fade out at the end when the text actually overflows, so short titles are no longer dimmed by the mask",
29
+ "Mobile top-bar navigation buttons have a little more spacing, reducing accidental taps on neighbours"
30
+ ]
31
+ }
32
+ ]
33
+ }
34
+ },
35
+ {
36
+ "version": "0.1.21",
37
+ "date": "2026-10-26",
38
+ "breaking": false,
39
+ "compare": "v0.1.20...v0.1.21",
40
+ "groups": {
41
+ "zh-CN": [
42
+ {
43
+ "kind": "changed",
44
+ "items": [
45
+ "移动端会话头部的操作按钮收进「更多操作」菜单(工作区文件、编辑对话、重置对话、导出游玩记录、删除对话),头部不再拥挤;主题切换按钮仍然直接显示",
46
+ "移动端会话标题可用的横向空间明显变大,长标题能显示更多内容",
47
+ "会话列表中的标题和副标题过长时改为末尾渐隐,不再用省略号生硬截断",
48
+ "会话头部的上下文用量标签字号略微收小,和标题搭配更协调"
49
+ ]
50
+ }
51
+ ],
52
+ "en": [
53
+ {
54
+ "kind": "changed",
55
+ "items": [
56
+ "On mobile, session header actions now collapse into a \"More actions\" menu (workspace files, edit, reset, export gameplay, delete), so the header is no longer cramped; the theme toggle stays one tap away",
57
+ "Mobile session titles get noticeably more horizontal room, so long titles reveal more of their text",
58
+ "Long session list titles and subtitles now fade out at the end instead of being cut off with an ellipsis",
59
+ "The context usage badge in the session header uses a slightly smaller font so it sits better next to the title"
60
+ ]
61
+ }
62
+ ]
63
+ }
64
+ },
5
65
  {
6
66
  "version": "0.1.20",
7
67
  "date": "2026-10-26",
@@ -573,10 +573,14 @@ body.resizing .sidebar-resizer {
573
573
  }
574
574
  .session-item .stitle-text {
575
575
  overflow: hidden;
576
- text-overflow: ellipsis;
577
576
  white-space: nowrap;
578
577
  min-width: 0;
579
578
  }
579
+ .session-item .stitle-text.has-overflow,
580
+ .session-item .sprev.has-overflow {
581
+ -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 18px), transparent);
582
+ mask-image: linear-gradient(to right, #000 calc(100% - 18px), transparent);
583
+ }
580
584
  .session-item .spinned {
581
585
  display: inline-flex;
582
586
  align-items: center;
@@ -611,11 +615,11 @@ body.resizing .sidebar-resizer {
611
615
  }
612
616
  }
613
617
  .session-item .sprev {
618
+ display: block;
614
619
  font-size: 11.5px;
615
620
  color: var(--text-faint);
616
621
  white-space: nowrap;
617
622
  overflow: hidden;
618
- text-overflow: ellipsis;
619
623
  }
620
624
  /* ---------- 会话区域(头部 + 滚动列表) ---------- */
621
625
  .session-section {
@@ -1072,6 +1076,7 @@ body.resizing .sidebar-resizer {
1072
1076
  }
1073
1077
  .chat-header .tsub-mode {
1074
1078
  flex-shrink: 0;
1079
+ font-size: 12px;
1075
1080
  font-weight: 700;
1076
1081
  color: var(--primary);
1077
1082
  }
@@ -1111,6 +1116,10 @@ body.resizing .sidebar-resizer {
1111
1116
  color: var(--danger);
1112
1117
  background: var(--danger-soft);
1113
1118
  }
1119
+ .chat-actions .chat-actions-more,
1120
+ .chat-actions-menu {
1121
+ display: none;
1122
+ }
1114
1123
 
1115
1124
  /* ---------- 卡片/仪表盘 ---------- */
1116
1125
  .dash-scroll {
@@ -1673,7 +1682,7 @@ hr.sep {
1673
1682
  display: flex;
1674
1683
  flex: 1;
1675
1684
  min-width: 0;
1676
- gap: 4px;
1685
+ gap: 8px;
1677
1686
  margin: -12px -10px;
1678
1687
  padding: 12px 10px;
1679
1688
  overflow-x: auto;
@@ -1799,7 +1808,7 @@ hr.sep {
1799
1808
  }
1800
1809
 
1801
1810
  .chat-header .ttitle {
1802
- max-width: min(28vw, 180px);
1811
+ max-width: min(52vw, 260px);
1803
1812
  font-size: 15px;
1804
1813
  }
1805
1814
 
@@ -1808,16 +1817,67 @@ hr.sep {
1808
1817
  }
1809
1818
 
1810
1819
  .chat-actions {
1820
+ position: relative;
1811
1821
  flex: 0 0 auto;
1812
1822
  gap: 0;
1813
1823
  }
1814
1824
 
1825
+ .chat-actions .icon-tooltip:not(.chat-actions-more, .chat-theme-toggle) {
1826
+ display: none;
1827
+ }
1828
+
1829
+ .chat-actions .chat-actions-more {
1830
+ display: inline-flex;
1831
+ }
1832
+
1815
1833
  .chat-actions .icon-tooltip {
1816
1834
  width: 34px;
1817
1835
  height: 34px;
1818
1836
  flex-shrink: 0;
1819
1837
  }
1820
1838
 
1839
+ .chat-actions-menu {
1840
+ position: absolute;
1841
+ top: calc(100% + 6px);
1842
+ right: 0;
1843
+ z-index: 60;
1844
+ display: flex;
1845
+ flex-direction: column;
1846
+ min-width: 164px;
1847
+ padding: 6px;
1848
+ background: var(--surface);
1849
+ border: 1px solid var(--border);
1850
+ border-radius: 12px;
1851
+ box-shadow: var(--shadow-lg);
1852
+ }
1853
+
1854
+ .chat-actions-menu button {
1855
+ display: flex;
1856
+ align-items: center;
1857
+ gap: 9px;
1858
+ width: 100%;
1859
+ padding: 8px 10px;
1860
+ border: none;
1861
+ border-radius: 8px;
1862
+ background: none;
1863
+ color: var(--text);
1864
+ font-size: 13px;
1865
+ font-weight: 600;
1866
+ text-align: left;
1867
+ }
1868
+
1869
+ .chat-actions-menu button:hover {
1870
+ background: var(--surface-2);
1871
+ }
1872
+
1873
+ .chat-actions-menu button.danger {
1874
+ color: var(--danger);
1875
+ }
1876
+
1877
+ .chat-actions-menu button.danger:hover {
1878
+ background: var(--danger-soft);
1879
+ }
1880
+
1821
1881
  .chat-scroll {
1822
1882
  padding-top: 14px;
1823
1883
  }
@@ -625,7 +625,13 @@
625
625
  justify-content: space-between;
626
626
  gap: 12px;
627
627
  }
628
- .settings-gen-reset {
628
+ .settings-gen-actions {
629
+ display: flex;
630
+ flex-shrink: 0;
631
+ gap: 6px;
632
+ }
633
+ .settings-gen-reset,
634
+ .settings-gen-lock {
629
635
  flex-shrink: 0;
630
636
  }
631
637
  .settings-reset-section {