Skip to content

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.

Download the starter ZIP

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.

manifest.json
{
  "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

index.js
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

style.css
#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.