tavern-harness 0.1.28 → 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/CHANGELOG.md +27 -0
- package/package.json +1 -1
- package/sandbox-server.mjs +20 -45
- package/src/assets/tavern-handbook.md +3 -0
- package/src/components/WorkspaceFileManagerModal.tsx +14 -19
- package/src/core/i18n.ts +3 -3
- package/src/core/toolDefinitions.ts +22 -22
- package/src/generated/changelog.json +41 -1
- package/src/theme/chat.css +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,32 @@
|
|
|
3
3
|
> 本文件由 `scripts/changelog.mjs` 从 `changelog/v*.md` 生成,请勿手动编辑。
|
|
4
4
|
> 版本对比链接见文末;源文件位于仓库 `changelog/` 目录。
|
|
5
5
|
|
|
6
|
+
## [0.1.29] - 2026-10-28
|
|
7
|
+
|
|
8
|
+
### 中文
|
|
9
|
+
|
|
10
|
+
#### 改进
|
|
11
|
+
|
|
12
|
+
- 消息编辑器中编辑用户消息后,主按钮由「保存并重新生成」改为「保存并发送」,文案与实际行为一致,操作按钮的文字对齐也更整齐
|
|
13
|
+
- 普通会话的工作区不再包含只读的 `public` 公共目录:文件管理器和工具只能看到当前会话自己的文件,列表中所有内容都可以删除;旧会话残留的 `public` 链接会在下次使用该会话时自动清理
|
|
14
|
+
|
|
15
|
+
#### 内部
|
|
16
|
+
|
|
17
|
+
- 将内置 `file_display` 工具移到文件读写工具之后,让模型更早看到它
|
|
18
|
+
- 酒馆手册补充技能编写建议:HTML 技能应把数据与页面分离,更新数据时只写数据文件
|
|
19
|
+
|
|
20
|
+
### English
|
|
21
|
+
|
|
22
|
+
#### Changed
|
|
23
|
+
|
|
24
|
+
- In the message editor, the user-message action is now labeled "Save & send" instead of "Save & regenerate", matching what it actually does; action buttons are also aligned more neatly
|
|
25
|
+
- Regular sessions no longer expose a read-only `public` directory in their workspace: the file manager and tools see only the session's own files, and everything listed can be deleted. Leftover `public` links are removed the next time the session is used
|
|
26
|
+
|
|
27
|
+
#### Internal
|
|
28
|
+
|
|
29
|
+
- Moved the built-in `file_display` tool right after the file read/write tools so models notice it earlier
|
|
30
|
+
- Added skill authoring guidance to the tavern handbook: HTML skills should separate data from presentation and only rewrite the data file on updates
|
|
31
|
+
|
|
6
32
|
## [0.1.28] - 2026-10-28
|
|
7
33
|
|
|
8
34
|
### 中文
|
|
@@ -800,6 +826,7 @@
|
|
|
800
826
|
- Vite + React + Zustand + Dexie stack with browser-side local persistence
|
|
801
827
|
|
|
802
828
|
<!-- 链接定义 -->
|
|
829
|
+
[0.1.29]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.28...v0.1.29
|
|
803
830
|
[0.1.28]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.27...v0.1.28
|
|
804
831
|
[0.1.27]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.26...v0.1.27
|
|
805
832
|
[0.1.26]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.25...v0.1.26
|
package/package.json
CHANGED
package/sandbox-server.mjs
CHANGED
|
@@ -490,18 +490,18 @@ function resolveSessionBase(session) {
|
|
|
490
490
|
return join(RESOLVED_WORKSPACE_ROOT, raw);
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
-
/**
|
|
493
|
+
/** 创建会话目录;普通会话不暴露公共工作区。 */
|
|
494
494
|
function ensureSessionBase(base) {
|
|
495
495
|
mkdirSync(PUBLIC_WORKSPACE, { recursive: true });
|
|
496
496
|
mkdirSync(base, { recursive: true });
|
|
497
497
|
if (base === PUBLIC_WORKSPACE) return;
|
|
498
|
-
const
|
|
499
|
-
if (
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
498
|
+
const legacyPublicLink = join(base, 'public');
|
|
499
|
+
if (
|
|
500
|
+
existsSync(legacyPublicLink)
|
|
501
|
+
&& lstatSync(legacyPublicLink).isSymbolicLink()
|
|
502
|
+
&& realpathSync(legacyPublicLink) === realpathSync(PUBLIC_WORKSPACE)
|
|
503
|
+
) {
|
|
504
|
+
rmSync(legacyPublicLink);
|
|
505
505
|
}
|
|
506
506
|
}
|
|
507
507
|
|
|
@@ -596,11 +596,10 @@ const server = createServer(async (req, res) => {
|
|
|
596
596
|
const payload = JSON.parse(body);
|
|
597
597
|
const base = resolveSessionBase(payload?.session);
|
|
598
598
|
const rel = sanitizeWorkspaceRelativePath(payload?.path);
|
|
599
|
-
|
|
600
|
-
if (!readingPublicLink && resolvesOutsideSession(rel, base)) throw new Error('Path is outside the workspace');
|
|
599
|
+
if (resolvesOutsideSession(rel, base)) throw new Error('Path is outside the workspace');
|
|
601
600
|
const file = resolve(base, rel);
|
|
602
601
|
if (!existsSync(file) || !lstatSync(file).isFile()) throw new Error('File not found');
|
|
603
|
-
assertInside(
|
|
602
|
+
assertInside(base, realpathSync(file));
|
|
604
603
|
const content = await readFilePromise(file);
|
|
605
604
|
if (content.length > 8 * 1024 * 1024) throw new Error('Preview file exceeds the 8MB limit');
|
|
606
605
|
res.end(JSON.stringify({ ok: true, content: content.toString('base64') }));
|
|
@@ -635,8 +634,7 @@ const server = createServer(async (req, res) => {
|
|
|
635
634
|
const rawPath = String(payload?.path ?? '').replace(/\\/g, '/').trim();
|
|
636
635
|
if (!rawPath) throw new Error('Invalid path');
|
|
637
636
|
const rel = sanitizeWorkspaceRelativePath(rawPath);
|
|
638
|
-
|
|
639
|
-
if (!readingPublicLink && resolvesOutsideSession(rel, base)) {
|
|
637
|
+
if (resolvesOutsideSession(rel, base)) {
|
|
640
638
|
throw new Error('Path is outside the workspace');
|
|
641
639
|
}
|
|
642
640
|
const fp = resolve(base, rel);
|
|
@@ -649,9 +647,6 @@ const server = createServer(async (req, res) => {
|
|
|
649
647
|
const content = String(payload?.content ?? '');
|
|
650
648
|
ensureSessionBase(base);
|
|
651
649
|
const rel = sanitizeWorkspaceRelativePath(rawPath);
|
|
652
|
-
if (base !== PUBLIC_WORKSPACE && (rel === 'public' || rel.startsWith('public/'))) {
|
|
653
|
-
throw new Error('The public directory is read-only');
|
|
654
|
-
}
|
|
655
650
|
if (resolvesOutsideSession(rel, base)) {
|
|
656
651
|
throw new Error('Path is outside the workspace');
|
|
657
652
|
}
|
|
@@ -674,18 +669,14 @@ const server = createServer(async (req, res) => {
|
|
|
674
669
|
const rawPath = String(payload?.path ?? '').replace(/\\/g, '/').trim();
|
|
675
670
|
if (!rawPath) throw new Error('Invalid path');
|
|
676
671
|
const rel = sanitizeWorkspaceRelativePath(rawPath);
|
|
677
|
-
|
|
678
|
-
if (!deletingPublicLink && resolvesOutsideSession(rel, base)) {
|
|
672
|
+
if (resolvesOutsideSession(rel, base)) {
|
|
679
673
|
throw new Error('Path is outside the workspace');
|
|
680
674
|
}
|
|
681
675
|
const fp = resolve(base, rel);
|
|
682
676
|
if (!existsSync(fp)) throw new Error('File or directory not found');
|
|
683
677
|
const entry = lstatSync(fp);
|
|
684
|
-
if (entry.isSymbolicLink() && rel === 'public') {
|
|
685
|
-
throw new Error('The public workspace link cannot be deleted');
|
|
686
|
-
}
|
|
687
678
|
if (!entry.isFile() && !entry.isDirectory()) throw new Error('Path is not a file or directory');
|
|
688
|
-
|
|
679
|
+
assertInside(base, realpathSync(fp));
|
|
689
680
|
if (entry.isDirectory()) {
|
|
690
681
|
rmSync(fp, { recursive: true, force: false });
|
|
691
682
|
} else {
|
|
@@ -869,14 +860,6 @@ function extractPathCandidate(value) {
|
|
|
869
860
|
return candidate || null;
|
|
870
861
|
}
|
|
871
862
|
|
|
872
|
-
function isPublicLinkReadPath(value, sessionBase) {
|
|
873
|
-
if (sessionBase === PUBLIC_WORKSPACE) return false;
|
|
874
|
-
const candidate = extractPathCandidate(value);
|
|
875
|
-
if (!candidate) return false;
|
|
876
|
-
const normalized = candidate.replace(/\\/g, '/').trim();
|
|
877
|
-
return normalized === 'public' || normalized.startsWith('public/');
|
|
878
|
-
}
|
|
879
|
-
|
|
880
863
|
/** Only the platform null device may be used outside the workspace, and only as a redirect sink. */
|
|
881
864
|
function isNullDeviceSink(value) {
|
|
882
865
|
const candidate = extractPathCandidate(value);
|
|
@@ -1097,29 +1080,23 @@ function validateScript(script, sessionBase) {
|
|
|
1097
1080
|
const commandWriteTargets = shellWriteTargets(cmd, args);
|
|
1098
1081
|
const redirectWriteTargets = [...(output ? [output.path] : []), ...(errorOutput ? [errorOutput.path] : [])];
|
|
1099
1082
|
if (
|
|
1100
|
-
commandWriteTargets.some((arg) => resolvesOutsideSession(arg, sessionBase)
|
|
1101
|
-
|| redirectWriteTargets.some((arg) => (
|
|
1102
|
-
!isNullDeviceSink(arg) && (resolvesOutsideSession(arg, sessionBase) || isPublicLinkReadPath(arg, sessionBase))
|
|
1103
|
-
))
|
|
1083
|
+
commandWriteTargets.some((arg) => resolvesOutsideSession(arg, sessionBase))
|
|
1084
|
+
|| redirectWriteTargets.some((arg) => !isNullDeviceSink(arg) && resolvesOutsideSession(arg, sessionBase))
|
|
1104
1085
|
) {
|
|
1105
1086
|
hasExternalWrite = true;
|
|
1106
1087
|
}
|
|
1107
1088
|
const readTargets = [...shellReadTargets(cmd, args), ...(input ? [input] : [])];
|
|
1108
|
-
if (readTargets.some((arg) => (
|
|
1109
|
-
!isPublicLinkReadPath(arg, sessionBase) && resolvesOutsideSession(arg, sessionBase)
|
|
1110
|
-
))) {
|
|
1089
|
+
if (readTargets.some((arg) => resolvesOutsideSession(arg, sessionBase))) {
|
|
1111
1090
|
hasExternalRead = true;
|
|
1112
1091
|
}
|
|
1113
|
-
if ([cmd, ...args].some((arg) => (
|
|
1114
|
-
!isPublicLinkReadPath(arg, sessionBase) && resolvesOutsideSession(arg, sessionBase)
|
|
1115
|
-
))) {
|
|
1092
|
+
if ([cmd, ...args].some((arg) => resolvesOutsideSession(arg, sessionBase))) {
|
|
1116
1093
|
hasExternalPath = true;
|
|
1117
1094
|
}
|
|
1118
1095
|
}
|
|
1119
1096
|
}
|
|
1120
|
-
if (hasExternalRead) return 'Script read paths must stay within the current session workspace
|
|
1121
|
-
if (hasExternalWrite) return 'Script write paths must stay within the current session workspace
|
|
1122
|
-
if (hasExternalPath) return 'Script paths must stay within the current session workspace
|
|
1097
|
+
if (hasExternalRead) return 'Script read paths must stay within the current session workspace';
|
|
1098
|
+
if (hasExternalWrite) return 'Script write paths must stay within the current session workspace';
|
|
1099
|
+
if (hasExternalPath) return 'Script paths must stay within the current session workspace';
|
|
1123
1100
|
if (hasNonAllowlistedCommand) return { needConfirmReason: 'non_allowlisted' };
|
|
1124
1101
|
return null;
|
|
1125
1102
|
}
|
|
@@ -1294,8 +1271,6 @@ async function walkWorkspace(dir, prefix, out) {
|
|
|
1294
1271
|
try {
|
|
1295
1272
|
if (ent.isDirectory()) {
|
|
1296
1273
|
await walkWorkspace(full, rel, out);
|
|
1297
|
-
} else if (ent.isSymbolicLink() && rel === 'public' && realpathSync(full) === realpathSync(PUBLIC_WORKSPACE)) {
|
|
1298
|
-
await walkWorkspace(full, rel, out);
|
|
1299
1274
|
} else if (ent.isFile()) {
|
|
1300
1275
|
out.push(rel);
|
|
1301
1276
|
}
|
|
@@ -37,3 +37,6 @@
|
|
|
37
37
|
1. **Create the minimum number of skills**: Assess responsibilities against the user's needs. If one skill can accomplish the goal, do not split it into or add multiple redundant skills.
|
|
38
38
|
2. **Document skill usage and workflows clearly**: In both World Info and the character persona, clearly record each skill's trigger conditions and usage steps so the character can invoke it as required.
|
|
39
39
|
3. **Verify character skill equipment**: After creating or configuring a game, confirm that each relevant character has every skill required by the workflow. In particular, verify that relevant built-in skills are enabled.
|
|
40
|
+
|
|
41
|
+
### Skill Creation
|
|
42
|
+
1. **Separate data from presentation in HTML-based skills**: HTML is generally generated once as a static page. When data needs to be updated, the skill should write only the data file, while the page reads that data at runtime.
|
|
@@ -10,7 +10,7 @@ import { useStore } from '../store/store';
|
|
|
10
10
|
// - 会话头部的工作目录标签点击打开
|
|
11
11
|
// - 只展示当前会话专属磁盘工作区(sandbox_workspace/)
|
|
12
12
|
// - 目录点击进入、支持返回上级与面包屑;文件点击复用 file_display 弹窗预览
|
|
13
|
-
// -
|
|
13
|
+
// - 可在确认后删除文件或目录
|
|
14
14
|
// ============================================================
|
|
15
15
|
|
|
16
16
|
function extOf(path: string): string {
|
|
@@ -215,10 +215,8 @@ export function WorkspaceFileManagerModal({ sessionId, onClose }: { sessionId: n
|
|
|
215
215
|
<span className="ws-fm-name">..</span>
|
|
216
216
|
</div>
|
|
217
217
|
)}
|
|
218
|
-
{visible.map((n) =>
|
|
219
|
-
|
|
220
|
-
return (
|
|
221
|
-
<div
|
|
218
|
+
{visible.map((n) => (
|
|
219
|
+
<div
|
|
222
220
|
key={n.path}
|
|
223
221
|
className="ws-fm-row"
|
|
224
222
|
role="button"
|
|
@@ -258,22 +256,19 @@ export function WorkspaceFileManagerModal({ sessionId, onClose }: { sessionId: n
|
|
|
258
256
|
aria-label={t('common.download')}
|
|
259
257
|
disabled
|
|
260
258
|
><span aria-hidden="true">↓</span></button>
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
><Icon name="trash" size={13} /></button>
|
|
271
|
-
)}
|
|
259
|
+
<button
|
|
260
|
+
className="icon-btn ws-fm-delete"
|
|
261
|
+
title={t('common.delete')}
|
|
262
|
+
aria-label={t('common.delete')}
|
|
263
|
+
onClick={(event) => {
|
|
264
|
+
event.stopPropagation();
|
|
265
|
+
setPendingDelete(n);
|
|
266
|
+
}}
|
|
267
|
+
><Icon name="trash" size={13} /></button>
|
|
272
268
|
</span>
|
|
273
269
|
)}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
})}
|
|
270
|
+
</div>
|
|
271
|
+
))}
|
|
277
272
|
{visible.length === 0 && <div className="ws-fm-empty">{t('header.fileManagerEmpty')}</div>}
|
|
278
273
|
</div>
|
|
279
274
|
)}
|
package/src/core/i18n.ts
CHANGED
|
@@ -131,7 +131,7 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
|
|
|
131
131
|
'chat.copyMsg': '复制消息',
|
|
132
132
|
'chat.addAttachment': '添加附件',
|
|
133
133
|
'chat.saveHint': '⌘/Ctrl+Enter 保存 · Esc 取消',
|
|
134
|
-
'chat.saveRegenerate': '
|
|
134
|
+
'chat.saveRegenerate': '保存并发送',
|
|
135
135
|
'chat.saveOnly': '仅保存',
|
|
136
136
|
'chat.toolCall': '调用工具: {name}',
|
|
137
137
|
'chat.contextCompressed': '上下文已压缩',
|
|
@@ -660,7 +660,7 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
|
|
|
660
660
|
'chat.copyMsg': '複製訊息',
|
|
661
661
|
'chat.addAttachment': '新增附件',
|
|
662
662
|
'chat.saveHint': '⌘/Ctrl+Enter 儲存 · Esc 取消',
|
|
663
|
-
'chat.saveRegenerate': '
|
|
663
|
+
'chat.saveRegenerate': '儲存並傳送',
|
|
664
664
|
'chat.saveOnly': '僅儲存',
|
|
665
665
|
'chat.toolCall': '呼叫工具: {name}',
|
|
666
666
|
'chat.contextCompressed': '上下文已壓縮',
|
|
@@ -1178,7 +1178,7 @@ const MESSAGES: Record<LangKey, Record<string, string>> = {
|
|
|
1178
1178
|
'chat.copyMsg': 'Copy message',
|
|
1179
1179
|
'chat.addAttachment': 'Add attachment',
|
|
1180
1180
|
'chat.saveHint': '⌘/Ctrl+Enter to save · Esc to cancel',
|
|
1181
|
-
'chat.saveRegenerate': 'Save &
|
|
1181
|
+
'chat.saveRegenerate': 'Save & send',
|
|
1182
1182
|
'chat.saveOnly': 'Save only',
|
|
1183
1183
|
'chat.toolCall': 'Tool: {name}',
|
|
1184
1184
|
'chat.contextCompressed': 'Context compressed',
|
|
@@ -12,6 +12,7 @@ export const BUILTIN_TOOL_NAMES = [
|
|
|
12
12
|
'file_read',
|
|
13
13
|
'file_write',
|
|
14
14
|
'file_edit',
|
|
15
|
+
'file_display',
|
|
15
16
|
'create_skill',
|
|
16
17
|
'update_skill',
|
|
17
18
|
'delete_skill',
|
|
@@ -19,7 +20,6 @@ export const BUILTIN_TOOL_NAMES = [
|
|
|
19
20
|
'get_character_info',
|
|
20
21
|
'get_lorebook_info',
|
|
21
22
|
'get_skill_info',
|
|
22
|
-
'file_display',
|
|
23
23
|
'create_character',
|
|
24
24
|
'update_character',
|
|
25
25
|
'delete_character',
|
|
@@ -89,7 +89,7 @@ export const BUILTIN_TOOLS: ChatCompletionTool[] = [
|
|
|
89
89
|
properties: {
|
|
90
90
|
path: {
|
|
91
91
|
type: 'string',
|
|
92
|
-
description: 'Relative path inside the current conversation workspace. Absolute paths and parent traversal are rejected.
|
|
92
|
+
description: 'Relative path inside the current conversation workspace. Absolute paths and parent traversal are rejected.',
|
|
93
93
|
},
|
|
94
94
|
content: {
|
|
95
95
|
type: 'string',
|
|
@@ -112,7 +112,7 @@ export const BUILTIN_TOOLS: ChatCompletionTool[] = [
|
|
|
112
112
|
properties: {
|
|
113
113
|
path: {
|
|
114
114
|
type: 'string',
|
|
115
|
-
description: 'Relative path inside the current conversation workspace. Absolute paths and parent traversal are rejected. The file must exist.
|
|
115
|
+
description: 'Relative path inside the current conversation workspace. Absolute paths and parent traversal are rejected. The file must exist.',
|
|
116
116
|
},
|
|
117
117
|
old_text: {
|
|
118
118
|
type: 'string',
|
|
@@ -135,6 +135,25 @@ export const BUILTIN_TOOLS: ChatCompletionTool[] = [
|
|
|
135
135
|
additionalProperties: false,
|
|
136
136
|
}
|
|
137
137
|
),
|
|
138
|
+
fn(
|
|
139
|
+
'file_display',
|
|
140
|
+
'Display any file from the current workspace to the user in a popup. Useful for inspecting existing files, reviewing generated output (reports, dashboards, images, HTML pages), or browsing the workspace content. Only shows the file in the UI — it does NOT return the file content; use file_read to actually read the content.',
|
|
141
|
+
{
|
|
142
|
+
type: 'object',
|
|
143
|
+
properties: {
|
|
144
|
+
path: {
|
|
145
|
+
type: 'string',
|
|
146
|
+
description: 'Relative path inside the current workspace. Must exist.',
|
|
147
|
+
},
|
|
148
|
+
title: {
|
|
149
|
+
type: 'string',
|
|
150
|
+
description: 'Optional popup title. Defaults to the file name.',
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
required: ['path'],
|
|
154
|
+
additionalProperties: false,
|
|
155
|
+
}
|
|
156
|
+
),
|
|
138
157
|
fn(
|
|
139
158
|
'create_skill',
|
|
140
159
|
'Create a new generated skill (tool) usable by characters. The skill is defined by a declarative execution schema. New skill is NOT enabled for any character until you enable it via update_character.',
|
|
@@ -244,25 +263,6 @@ export const BUILTIN_TOOLS: ChatCompletionTool[] = [
|
|
|
244
263
|
additionalProperties: false,
|
|
245
264
|
}
|
|
246
265
|
),
|
|
247
|
-
fn(
|
|
248
|
-
'file_display',
|
|
249
|
-
'Display any file from the current workspace to the user in a popup. Useful for inspecting existing files, reviewing generated output (reports, dashboards, images, HTML pages), or browsing the workspace content. Only shows the file in the UI — it does NOT return the file content; use file_read to actually read the content.',
|
|
250
|
-
{
|
|
251
|
-
type: 'object',
|
|
252
|
-
properties: {
|
|
253
|
-
path: {
|
|
254
|
-
type: 'string',
|
|
255
|
-
description: 'Relative path inside the current workspace. Must exist.',
|
|
256
|
-
},
|
|
257
|
-
title: {
|
|
258
|
-
type: 'string',
|
|
259
|
-
description: 'Optional popup title. Defaults to the file name.',
|
|
260
|
-
},
|
|
261
|
-
},
|
|
262
|
-
required: ['path'],
|
|
263
|
-
additionalProperties: false,
|
|
264
|
-
}
|
|
265
|
-
),
|
|
266
266
|
fn(
|
|
267
267
|
'create_character',
|
|
268
268
|
'Create a new character (NPC) in the tavern. The character becomes available for NPC or group sessions.',
|
|
@@ -1,7 +1,47 @@
|
|
|
1
1
|
{
|
|
2
|
-
"latest": "0.1.
|
|
2
|
+
"latest": "0.1.29",
|
|
3
3
|
"repo": "https://github.com/Rannichan/Tavern-Harness",
|
|
4
4
|
"entries": [
|
|
5
|
+
{
|
|
6
|
+
"version": "0.1.29",
|
|
7
|
+
"date": "2026-10-28",
|
|
8
|
+
"breaking": false,
|
|
9
|
+
"compare": "v0.1.28...v0.1.29",
|
|
10
|
+
"groups": {
|
|
11
|
+
"zh-CN": [
|
|
12
|
+
{
|
|
13
|
+
"kind": "changed",
|
|
14
|
+
"items": [
|
|
15
|
+
"消息编辑器中编辑用户消息后,主按钮由「保存并重新生成」改为「保存并发送」,文案与实际行为一致,操作按钮的文字对齐也更整齐",
|
|
16
|
+
"普通会话的工作区不再包含只读的 `public` 公共目录:文件管理器和工具只能看到当前会话自己的文件,列表中所有内容都可以删除;旧会话残留的 `public` 链接会在下次使用该会话时自动清理"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"kind": "internal",
|
|
21
|
+
"items": [
|
|
22
|
+
"将内置 `file_display` 工具移到文件读写工具之后,让模型更早看到它",
|
|
23
|
+
"酒馆手册补充技能编写建议:HTML 技能应把数据与页面分离,更新数据时只写数据文件"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"en": [
|
|
28
|
+
{
|
|
29
|
+
"kind": "changed",
|
|
30
|
+
"items": [
|
|
31
|
+
"In the message editor, the user-message action is now labeled \"Save & send\" instead of \"Save & regenerate\", matching what it actually does; action buttons are also aligned more neatly",
|
|
32
|
+
"Regular sessions no longer expose a read-only `public` directory in their workspace: the file manager and tools see only the session's own files, and everything listed can be deleted. Leftover `public` links are removed the next time the session is used"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"kind": "internal",
|
|
37
|
+
"items": [
|
|
38
|
+
"Moved the built-in `file_display` tool right after the file read/write tools so models notice it earlier",
|
|
39
|
+
"Added skill authoring guidance to the tavern handbook: HTML skills should separate data from presentation and only rewrite the data file on updates"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
},
|
|
5
45
|
{
|
|
6
46
|
"version": "0.1.28",
|
|
7
47
|
"date": "2026-10-28",
|
package/src/theme/chat.css
CHANGED