tavern-harness 0.1.27 → 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 CHANGED
@@ -3,6 +3,46 @@
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
+
32
+ ## [0.1.28] - 2026-10-28
33
+
34
+ ### 中文
35
+
36
+ #### 修复
37
+
38
+ - 修复沙盒里打开的 HTML 预览加载不了图片、样式和字体等文件的问题:预览页面引用的工作区资源现在可以正常读取,生成报告或小工具时不用再把资源全部塞进单个 HTML
39
+
40
+ ### English
41
+
42
+ #### Fixed
43
+
44
+ - Fixed HTML previews in the sandboxed viewer failing to load images, stylesheets, and fonts from the workspace; generated reports and small tools can now reference separate asset files instead of inlining everything into one HTML file
45
+
6
46
  ## [0.1.27] - 2026-10-28
7
47
 
8
48
  ### 中文
@@ -786,6 +826,8 @@
786
826
  - Vite + React + Zustand + Dexie stack with browser-side local persistence
787
827
 
788
828
  <!-- 链接定义 -->
829
+ [0.1.29]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.28...v0.1.29
830
+ [0.1.28]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.27...v0.1.28
789
831
  [0.1.27]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.26...v0.1.27
790
832
  [0.1.26]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.25...v0.1.26
791
833
  [0.1.25]: https://github.com/Rannichan/Tavern-Harness/compare/v0.1.24...v0.1.25
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tavern-harness",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "Tavern Harness — 酒馆模式下的本地 AI Agent 助手(MyAgent-Android 的 Web 复刻)",
5
5
  "private": false,
6
6
  "type": "module",
@@ -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 publicLink = join(base, 'public');
499
- if (existsSync(publicLink)) {
500
- if (!lstatSync(publicLink).isSymbolicLink() || realpathSync(publicLink) !== realpathSync(PUBLIC_WORKSPACE)) {
501
- throw new Error('The session public entry is not a valid link to the public workspace');
502
- }
503
- } else {
504
- symlinkSync(IS_WIN ? PUBLIC_WORKSPACE : '../public', publicLink, IS_WIN ? 'junction' : 'dir');
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
- const readingPublicLink = base !== PUBLIC_WORKSPACE && (rel === 'public' || rel.startsWith('public/'));
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(readingPublicLink ? PUBLIC_WORKSPACE : base, realpathSync(file));
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
- const readingPublicLink = base !== PUBLIC_WORKSPACE && (rel === 'public' || rel.startsWith('public/'));
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
- const deletingPublicLink = base !== PUBLIC_WORKSPACE && (rel === 'public' || rel.startsWith('public/'));
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
- if (!deletingPublicLink) assertInside(base, realpathSync(fp));
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) || isPublicLinkReadPath(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; public files may only be read through the public link';
1121
- if (hasExternalWrite) return 'Script write paths must stay within the current session workspace and cannot use the public symlink';
1122
- if (hasExternalPath) return 'Script paths must stay within the current session workspace; public files may only be read through the public link';
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
- // - 可在确认后删除文件或目录(public 软链接除外)
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
- const isPublicLink = n.isDir && n.path === 'public/';
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
- {!isPublicLink && (
262
- <button
263
- className="icon-btn ws-fm-delete"
264
- title={t('common.delete')}
265
- aria-label={t('common.delete')}
266
- onClick={(event) => {
267
- event.stopPropagation();
268
- setPendingDelete(n);
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
- </div>
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 & regenerate',
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. public link is read-only in normal sessions.',
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. The public link is read-only in normal sessions.',
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,71 @@
1
1
  {
2
- "latest": "0.1.27",
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
+ },
45
+ {
46
+ "version": "0.1.28",
47
+ "date": "2026-10-28",
48
+ "breaking": false,
49
+ "compare": "v0.1.27...v0.1.28",
50
+ "groups": {
51
+ "zh-CN": [
52
+ {
53
+ "kind": "fixed",
54
+ "items": [
55
+ "修复沙盒里打开的 HTML 预览加载不了图片、样式和字体等文件的问题:预览页面引用的工作区资源现在可以正常读取,生成报告或小工具时不用再把资源全部塞进单个 HTML"
56
+ ]
57
+ }
58
+ ],
59
+ "en": [
60
+ {
61
+ "kind": "fixed",
62
+ "items": [
63
+ "Fixed HTML previews in the sandboxed viewer failing to load images, stylesheets, and fonts from the workspace; generated reports and small tools can now reference separate asset files instead of inlining everything into one HTML file"
64
+ ]
65
+ }
66
+ ]
67
+ }
68
+ },
5
69
  {
6
70
  "version": "0.1.27",
7
71
  "date": "2026-10-28",
@@ -155,6 +155,9 @@
155
155
  .message-editor-actions {
156
156
  align-items: center;
157
157
  }
158
+ .message-editor-actions .btn {
159
+ line-height: var(--typography-body-line-height);
160
+ }
158
161
  .message-editor-attachment {
159
162
  margin-right: auto;
160
163
  }
package/vite.config.ts CHANGED
@@ -324,6 +324,7 @@ function workspacePreviewPlugin(): Plugin {
324
324
  res.writeHead(200, {
325
325
  'Content-Type': previewMimeType(path),
326
326
  'Cache-Control': 'no-store',
327
+ 'Access-Control-Allow-Origin': '*',
327
328
  'X-Content-Type-Options': 'nosniff',
328
329
  });
329
330
  res.end(content);