Expert skill for developing SillyTavern frontend interfaces and scripts using Tavern Helper framework...
Expert guidance for developing frontend interfaces and background scripts for SillyTavern using the Tavern Helper framework.
Use this skill when users request:
Projects live in src/项目名/ folders:
Frontend Interface (index.ts + index.html):
Script (index.ts only):
window.$ = window.parent.$Use getVariables() / replaceVariables() for persistent storage:
global - Shared across entire taverncharacter - Bound to specific character cardchat - Bound to specific chat filemessage - Bound to specific message floorscript - Bound to specific scriptextension - Bound to specific extensionSeparate Tavern Helper script enhancing message floor variables:
display_data and delta_data for visualizationawait waitGlobalInitialized('Mvu')See references/mvu_framework.md for detailed API.
Determine frontend interface or script based on whether UI is needed.
For frontend interfaces:
For scripts:
See references/tech_stack.md for full dependency list and usage patterns.
# Install dependencies (pnpm only)
pnpm install
# Development build
pnpm run build:dev
# Watch mode (auto-rebuild)
pnpm run watch
# Production build
pnpm run build
Option 1: Soft link (recommended)
ln -s /path/to/dist/项目名/index.html /root/web-dev/preview.html
Option 2: Copy
cp /path/to/dist/项目名/index.html /root/web-dev/
Access: https://dev.piepia.space/preview.html
Use browsermcp to test in real browser environment.
// ✅ Correct: TypeScript with Zod validation
import { z } from 'zod';
const Settings = z.object({
volume: z.number().min(0).max(1).default(0.5),
autoPlay: z.boolean().default(true),
}).prefault({});
type Settings = z.infer<typeof Settings>;
// ✅ jQuery for DOM
$('#button').on('click', handler);
// ✅ GSAP for animations (including typewriter)
gsap.to('#text', {
duration: 2,
text: { value: "New text", delimiter: "" },
ease: "none"
});
// ✅ Zod for validation
const result = Schema.safeParse(data);
if (!result.success) {
console.error(z.prettifyError(result.error));
}
// ✅ Correct: jQuery ready handler
$(() => {
toastr.success('Loaded');
// Initialization logic...
});
// ✅ Correct: Cleanup on unload
$(window).on('pagehide', () => {
toastr.info('Unloaded');
// Cleanup: remove listeners, stop timers...
});
// ❌ Wrong: DOMContentLoaded doesn't fire on network load
document.addEventListener('DOMContentLoaded', () => {
// Won't execute when loaded via $('body').load(url)!
});
let chat_id = SillyTavern.getCurrentChatId();
eventOn(tavern_events.CHAT_CHANGED, new_chat_id => {
if (chat_id !== new_chat_id) {
chat_id = new_chat_id;
window.location.reload();
}
});
<head>
<!-- Keep empty - webpack auto-injects styles/scripts -->
</head>
<body>
<!-- Static content only -->
<div id="app"></div>
<!-- ❌ NO <link rel="stylesheet" href="./index.css"> -->
<!-- ❌ NO <script src="./index.ts"> -->
<!-- ❌ NO <img src=""> empty placeholders -->
</body>
// Method 1: Tailwind CSS (simple styles)
import './tailwind.css'; // Content: @import 'tailwindcss';
// Method 2: Vue component styles (recommended)
// Component.vue
<template>...</template>
<style lang="scss" scoped>
.my-class { color: red; }
</style>
// Method 3: Global SCSS
import './index.scss';
// ✅ Use width + aspect-ratio (NOT vh)
.container {
width: 100%;
aspect-ratio: 16 / 9;
}
// ❌ Avoid forcing parent height
.bad {
min-height: 500px; // Not recommended
overflow: auto; // May cause scrollbars
}
// ✅ Fit container width, no horizontal scroll
body {
max-width: 100%;
overflow-x: hidden;
}
// ✅ Card style: transparent background (unless explicitly requested)
.card {
background: transparent;
}
See references/vue_template.md for complete Vue 3 + Pinia + Vue Router setup.
See references/pixijs_usage.md for game-like interfaces with heavy multimedia assets.
// ⚠️ $ operates on SillyTavern page, not iframe
$('body'); // Selects SillyTavern <body>
// Select iframe elements
$('body', document); // document = script iframe
// 1. Create mount point with jQuery
const $app = $('<div>').attr('id', 'my-app').appendTo('body');
// 2. Create Vue app
const app = createApp(App);
app.use(createPinia());
// 3. Mount to jQuery element
app.mount($app[0]); // Note [0] to get DOM element
// 4. Cleanup
$(window).on('pagehide', () => {
app.unmount();
$app.remove();
});
// Script styles only apply to iframe - manually teleport to parent
export function teleport_style() {
$(`<div>`)
.attr('script_id', getScriptId())
.append($(`head > style`, document).clone())
.appendTo('head'); // Appends to SillyTavern <head>
}
export function deteleport_style() {
$(`head > div[script_id="${getScriptId()}"]`).remove();
}
$(() => teleport_style());
$(window).on('pagehide', () => deteleport_style());
See references/script_patterns.md for Pinia + Zod settings management pattern.
// Triggered when user clicks button in Tavern Helper script library
eventOn(getButtonEvent('Reload'), () => {
window.location.reload();
});
eventOn(getButtonEvent('Clear Data'), () => {
if (confirm('Clear all data?')) {
replaceVariables({}, { type: 'script', script_id: getScriptId() });
toastr.success('Data cleared');
}
});
Priority order: Tavern Helper API > STScript Commands > SillyTavern Native API
// ❌ Don't: STScript
await triggerSlash('/setvar key=score 100');
const score = await triggerSlash('/getvar score');
// ✅ Do: Tavern Helper API
replaceVariables({ score: 100 }, { type: 'chat' });
const score = getVariables({ type: 'chat' }).score;
// ✅ Do: toastr instead of /echo
toastr.success('Hello');
// ✅ Do: Tavern Helper functions
const messages = getChatMessages(); // Instead of SillyTavern.chat
replaceWorldbook(data); // Instead of /setentryfield
Reason: Tavern Helper API provides:
Detailed documentation loaded as needed:
tech_stack.md - Complete dependency list and usage patternsapi_reference.md - Tavern Helper API documentationstscript_commands.md - All 264 STScript commands with examplestype_definitions.md - TypeScript type system overviewvue_template.md - Vue 3 + Pinia + Router setupscript_patterns.md - Common script development patternspixijs_usage.md - PixiJS multimedia interface guidemvu_framework.md - MVU variable framework APIbest_practices.md - Development do's and don'tsTemplate files for quick project initialization:
frontend-template/ - Vue 3 frontend interface boilerplatescript-template/ - Background script boilerplatetailwind.css - Tailwind CSS import fileconst data = ref({ score: 0, hp: 100 });
// Load initial values
data.value = getVariables({ type: 'chat' });
// Auto-save (remove Vue Proxy with klona)
watchEffect(() => {
replaceVariables(klona(data.value), { type: 'chat' });
});
import { tavern_events, eventOn } from '@types/iframe/event';
// AI message received
eventOn(tavern_events.MESSAGE_RECEIVED, (message_id) => {
console.log('New message', message_id);
});
// Message updated
eventOn(tavern_events.MESSAGE_UPDATED, (message_id) => {
console.log('Message updated', message_id);
});
// Simple generation (with chat history + character card)
const result = await generate({
prompt: 'Summarize the conversation',
quietPrompt: 'Summarize in 50 words or less',
});
// Raw generation (no chat history)
const result = await triggerSlash(`
/genraw instruct=off Why is the sky blue?
`);
const UserInput = z.object({
name: z.string().min(1),
age: z.number().int().positive(),
});
const result = UserInput.safeParse(data);
if (!result.success) {
toastr.error(z.prettifyError(result.error));
return;
}
const { name, age } = result.data; // Type-safe
❌ Node.js libraries (fs, path, http, etc.)
❌ <link> / <script> tags in index.html for local files
❌ DOMContentLoaded for load timing
❌ vh units in frontend interface styles (use aspect-ratio)
❌ Direct code execution in global scope (use $(() => {}))
❌ Saving Vue reactive data directly (use klona() first)
When developing features for users:
Current directory: /root/tavern_helper_template