The Neconyan handbook
Your guides Miso, Taro & Nori
Choose each guide's gender to change their artwork and pronouns. Your choices stay in this browser, separately from the app.
Your first extension¶
This starter adds Scene Note to the extension settings. It saves one short note for the current account. The note isn't inserted into prompts or tied to an individual chat.
The archive contains the exact source shown below, a short README and the licence. You can also download the manifest, JavaScript and stylesheet separately.
1. Install it in a test account¶
Extract the ZIP. Put the scene-note folder inside the test account's extensions directory. In a default single-user installation, the result is:
Neconyan/
data/
default-user/
extensions/
scene-note/
manifest.json
index.js
style.css
README.txt
LICENSE
If your installation uses a custom data root or another account handle, use that account's actual extensions directory. The manifest must be directly inside the extension folder, not hidden inside another extracted wrapper folder.
Reload Neconyan. Check Extensions → Manage extensions for Scene Note, enable it if necessary and open its settings through Extensions. Type a note, select Save note, wait for normal settings saving, then reload and confirm the text returns.
2. Describe the extension¶
The manifest tells the loader which files to load and which exported functions handle activation and removal.
{
"display_name": "Scene Note",
"loading_order": 100,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "Neconyan",
"version": "1.0.0",
"homePage": "https://platberlitz.github.io/neconyan-docs/extensions/first-extension/",
"hooks": {
"activate": "activate",
"disable": "deactivate",
"delete": "deactivate"
}
}
The JavaScript entry is loaded as a module. The named hooks refer to exported functions in that module. The starter performs its setup through activate, rather than starting work merely because another lifecycle action imported the file.
Before publishing a derivative, give it your own name, author, project URL and unique settings key. Keep the applicable original licence notices.
3. Build and save the panel¶
const ID = 'neconyan_scene_note';
const MAX_LENGTH = 2000;
let active = false;
let readyHandler = null;
let events = null;
let readyEvent = null;
function getContext() {
const context = globalThis.SillyTavern?.getContext?.();
if (!context?.extensionSettings || !context.eventSource) {
throw new Error('Scene Note needs the Neconyan extension context.');
}
return context;
}
function mount() {
if (!active || document.getElementById(ID)) return;
const host = document.getElementById('extensions_settings2');
if (!host) {
console.warn('[Scene Note] Extension settings host is unavailable.');
return;
}
const panel = document.createElement('section');
panel.id = ID;
panel.className = 'scene-note-settings';
const title = document.createElement('h3');
title.textContent = 'Scene Note';
const help = document.createElement('p');
help.textContent = 'A note for this account. It is not sent to the model.';
const label = document.createElement('label');
label.htmlFor = `${ID}_text`;
label.textContent = 'Your note';
const input = document.createElement('textarea');
input.id = label.htmlFor;
input.className = 'text_pole';
input.rows = 5;
input.maxLength = MAX_LENGTH;
const saved = getContext().extensionSettings[ID];
input.value = typeof saved?.text === 'string' ? saved.text : '';
const save = document.createElement('button');
save.type = 'button';
save.className = 'menu_button';
save.textContent = 'Save note';
const status = document.createElement('p');
status.setAttribute('role', 'status');
status.setAttribute('aria-live', 'polite');
input.addEventListener('input', () => {
status.textContent = 'Unsaved changes.';
});
save.addEventListener('click', () => {
const context = getContext();
context.extensionSettings[ID] = {
text: input.value.slice(0, MAX_LENGTH),
};
context.saveSettingsDebounced();
status.textContent = 'Save requested. Check app save errors before leaving.';
});
panel.append(title, help, label, input, save, status);
host.append(panel);
}
export function activate() {
if (active) return;
const context = getContext();
active = true;
events = context.eventSource;
readyEvent = context.eventTypes.APP_READY;
readyHandler = mount;
events.on(readyEvent, readyHandler);
}
export function deactivate() {
active = false;
if (events && readyHandler) {
events.removeListener(readyEvent, readyHandler);
}
readyHandler = null;
events = null;
readyEvent = null;
document.getElementById(ID)?.remove();
}
The compatibility object is still named SillyTavern. That is the actual API name, even in Neconyan. The code asks it for the current context instead of guessing a new global name.
The panel uses normal DOM elements and assigns user text through a textarea's value. It doesn't interpret the note as HTML. Its settings live under one unique key in extensionSettings, and the app's debounced settings writer schedules the save.
Save requested is intentional wording. This helper schedules saving; it isn't an acknowledgement from the server that writing has finished. A production feature that promises confirmed persistence needs a suitable confirmed-save path and failure handling.
4. Style only your panel¶
#neconyan_scene_note {
display: grid;
gap: 0.6rem;
padding: 0.75rem;
min-width: 0;
}
#neconyan_scene_note h3,
#neconyan_scene_note p {
margin: 0;
}
#neconyan_scene_note textarea {
width: 100%;
min-height: 7rem;
resize: vertical;
box-sizing: border-box;
}
#neconyan_scene_note .menu_button {
min-height: 44px;
padding: 0.5rem 0.8rem;
background: var(--neco-ginger);
color: var(--neco-on-accent);
border: 1px solid var(--neco-ginger);
justify-self: start;
}
#neconyan_scene_note .menu_button:hover {
border-color: var(--neco-ginger-hover);
box-shadow: inset 0 0 0 1px var(--neco-ginger-hover);
}
#neconyan_scene_note :focus-visible {
outline: 2px solid var(--neco-ginger);
outline-offset: 3px;
}
Every rule belongs to the extension's own root. The button uses Neconyan's accent and on-accent text variables, has a touch-friendly height and keeps a visible keyboard focus indicator.
5. Understand startup and cleanup¶
activate subscribes to APP_READY once. Neconyan's event source remembers that readiness event, so a listener added after readiness also runs. deactivate removes the exact listener and the panel; it leaves the user's saved note intact.
The activation guard prevents duplicate panels and subscriptions. Disabling or deleting should stop runtime behaviour, not silently erase settings. If you later add a separate data-cleaning action, explain that destructive action explicitly.
Taro
After testing the happy path, disable it, reload, enable it and reload again. One successful click is an encouraging start. It is not a lifecycle test.
Next: APIs and lifecycle, then Test and share.