Merge pull request #13 from paperboygold/feature/inventory-bugfixes

feat: inventory bugfixes, status polish, editable inventory fields
This commit is contained in:
Spicy Marinara
2025-10-20 02:03:06 +02:00
committed by GitHub
10 changed files with 844 additions and 67 deletions
+130 -7
View File
@@ -3,11 +3,12 @@
* Handles all user interactions with the inventory v2 system
*/
import { extensionSettings, lastGeneratedData } from '../../core/state.js';
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js';
import { saveSettings, saveChatData, updateMessageSwipeData } from '../../core/persistence.js';
import { buildInventorySummary } from '../generation/promptBuilder.js';
import { renderInventory, getLocationId } from '../rendering/inventory.js';
import { parseItems, serializeItems } from '../../utils/itemParser.js';
import { sanitizeLocationName, sanitizeItemName } from '../../utils/security.js';
// Type imports
/** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */
@@ -25,15 +26,27 @@ let currentActiveSubTab = 'onPerson';
let collapsedLocations = [];
/**
* Updates lastGeneratedData.userStats to include current inventory in text format.
* This ensures the AI context stays synced with manual edits.
* Tracks which inline forms are currently open
* @type {Object}
*/
let openForms = {
addLocation: false,
addItemOnPerson: false,
addItemStored: {}, // { [locationName]: true/false }
addItemAssets: false
};
/**
* Updates lastGeneratedData.userStats AND committedTrackerData.userStats to include
* current inventory in text format.
* This ensures manual edits are immediately visible to AI in next generation.
*/
function updateLastGeneratedDataInventory() {
const stats = extensionSettings.userStats;
const inventorySummary = buildInventorySummary(stats.inventory);
// Rebuild the lastGeneratedData.userStats text format
lastGeneratedData.userStats =
// Rebuild the userStats text format
const statsText =
`Health: ${stats.health}%\n` +
`Satiety: ${stats.satiety}%\n` +
`Energy: ${stats.energy}%\n` +
@@ -41,6 +54,11 @@ function updateLastGeneratedDataInventory() {
`Arousal: ${stats.arousal}%\n` +
`${stats.mood}: ${stats.conditions}\n` +
`${inventorySummary}`;
// Update BOTH lastGeneratedData AND committedTrackerData
// This makes manual edits immediately visible to AI
lastGeneratedData.userStats = statsText;
committedTrackerData.userStats = statsText;
}
/**
@@ -56,9 +74,18 @@ export function showAddItemForm(field, location) {
const locationId = getLocationId(location);
formId = `rpg-add-item-form-stored-${locationId}`;
inputId = `.rpg-location-item-input[data-location="${location}"]`;
// Track in state
if (!openForms.addItemStored) openForms.addItemStored = {};
openForms.addItemStored[location] = true;
} else {
formId = `rpg-add-item-form-${field}`;
inputId = `#rpg-new-item-${field}`;
// Track in state
if (field === 'onPerson') {
openForms.addItemOnPerson = true;
} else if (field === 'assets') {
openForms.addItemAssets = true;
}
}
const form = $(`#${formId}`);
@@ -81,9 +108,19 @@ export function hideAddItemForm(field, location) {
const locationId = getLocationId(location);
formId = `rpg-add-item-form-stored-${locationId}`;
inputId = `.rpg-location-item-input[data-location="${location}"]`;
// Clear from state
if (openForms.addItemStored && openForms.addItemStored[location]) {
delete openForms.addItemStored[location];
}
} else {
formId = `rpg-add-item-form-${field}`;
inputId = `#rpg-new-item-${field}`;
// Clear from state
if (field === 'onPerson') {
openForms.addItemOnPerson = false;
} else if (field === 'assets') {
openForms.addItemAssets = false;
}
}
const form = $(`#${formId}`);
@@ -109,9 +146,17 @@ export function saveAddItem(field, location) {
}
const input = $(inputId);
const itemName = input.val().trim();
const rawItemName = input.val().trim();
if (!rawItemName) {
hideAddItemForm(field, location);
return;
}
// Security: Validate and sanitize item name
const itemName = sanitizeItemName(rawItemName);
if (!itemName) {
alert('Invalid item name.');
hideAddItemForm(field, location);
return;
}
@@ -198,6 +243,9 @@ export function showAddLocationForm() {
const form = $('#rpg-add-location-form');
const input = $('#rpg-new-location-name');
// Track in state
openForms.addLocation = true;
form.show();
input.val('').focus();
}
@@ -209,6 +257,9 @@ export function hideAddLocationForm() {
const form = $('#rpg-add-location-form');
const input = $('#rpg-new-location-name');
// Clear from state
openForms.addLocation = false;
form.hide();
input.val('');
}
@@ -219,9 +270,17 @@ export function hideAddLocationForm() {
export function saveAddLocation() {
const inventory = extensionSettings.userStats.inventory;
const input = $('#rpg-new-location-name');
const locationName = input.val().trim();
const rawLocationName = input.val().trim();
if (!rawLocationName) {
hideAddLocationForm();
return;
}
// Security: Validate and sanitize location name
const locationName = sanitizeLocationName(rawLocationName);
if (!locationName) {
alert('Invalid location name. Avoid special names like "__proto__" or "constructor".');
hideAddLocationForm();
return;
}
@@ -500,3 +559,67 @@ export function getInventoryRenderOptions() {
collapsedLocations
};
}
/**
* Restores the state of inline forms after re-rendering.
* This ensures forms that were open before re-render are shown again.
* Also cleans up orphaned form states for deleted locations (Bug #3 fix).
*/
export function restoreFormStates() {
// Restore add location form
if (openForms.addLocation) {
const form = $('#rpg-add-location-form');
const input = $('#rpg-new-location-name');
if (form.length > 0) {
form.show();
// Don't refocus to avoid disrupting user interaction
}
}
// Restore add item on person form
if (openForms.addItemOnPerson) {
const form = $('#rpg-add-item-form-onPerson');
const input = $('#rpg-new-item-onPerson');
if (form.length > 0) {
form.show();
}
}
// Restore add item assets form
if (openForms.addItemAssets) {
const form = $('#rpg-add-item-form-assets');
const input = $('#rpg-new-item-assets');
if (form.length > 0) {
form.show();
}
}
// Restore add item stored forms (for each location)
// Clean up orphaned states for deleted locations (Bug #3 fix)
if (openForms.addItemStored && typeof openForms.addItemStored === 'object') {
const inventory = extensionSettings.userStats.inventory;
const locationsToDelete = [];
for (const location in openForms.addItemStored) {
if (openForms.addItemStored[location]) {
// Check if location still exists in inventory
if (inventory?.stored && inventory.stored.hasOwnProperty(location)) {
// Location exists, restore form
const locationId = location.replace(/\s+/g, '-');
const form = $(`#rpg-add-item-form-stored-${locationId}`);
if (form.length > 0) {
form.show();
}
} else {
// Location was deleted, mark for cleanup
locationsToDelete.push(location);
}
}
}
// Clean up orphaned form states
for (const location of locationsToDelete) {
delete openForms.addItemStored[location];
}
}
}
+104
View File
@@ -0,0 +1,104 @@
/**
* Inventory Item Editing Module
* Handles inline editing of inventory item names
*/
import { extensionSettings, lastGeneratedData, committedTrackerData } from '../../core/state.js';
import { saveSettings, saveChatData, updateMessageSwipeData } from '../../core/persistence.js';
import { buildInventorySummary } from '../generation/promptBuilder.js';
import { renderInventory } from '../rendering/inventory.js';
import { parseItems, serializeItems } from '../../utils/itemParser.js';
import { sanitizeItemName } from '../../utils/security.js';
/**
* Updates an existing inventory item's name.
* Validates, sanitizes, and persists the change.
*
* @param {string} field - Field name ('onPerson', 'stored', 'assets')
* @param {number} index - Index of item in the array
* @param {string} newName - New name for the item
* @param {string} [location] - Location name (required for 'stored' field)
*/
export function updateInventoryItem(field, index, newName, location) {
const inventory = extensionSettings.userStats.inventory;
// Validate and sanitize the new item name
const sanitizedName = sanitizeItemName(newName);
if (!sanitizedName) {
console.warn('[RPG Companion] Invalid item name, reverting change');
// Re-render to revert the change in UI
renderInventory();
return;
}
// Get current items for the field
let currentString;
if (field === 'stored') {
if (!location) {
console.error('[RPG Companion] Location required for stored items');
return;
}
currentString = inventory.stored[location] || 'None';
} else {
currentString = inventory[field] || 'None';
}
// Parse current items
const items = parseItems(currentString);
// Validate index
if (index < 0 || index >= items.length) {
console.error(`[RPG Companion] Invalid item index: ${index}`);
return;
}
// Update the item at this index
items[index] = sanitizedName;
// Serialize back to string
const newItemString = serializeItems(items);
// Update the inventory
if (field === 'stored') {
inventory.stored[location] = newItemString;
} else {
inventory[field] = newItemString;
}
// Update lastGeneratedData and committedTrackerData with new inventory
updateLastGeneratedDataInventory();
// Save changes
saveSettings();
saveChatData();
updateMessageSwipeData();
// Re-render inventory
renderInventory();
}
/**
* Updates lastGeneratedData.userStats AND committedTrackerData.userStats to include
* current inventory in text format.
* This ensures manual edits are immediately visible to AI in next generation.
* @private
*/
function updateLastGeneratedDataInventory() {
const stats = extensionSettings.userStats;
const inventorySummary = buildInventorySummary(stats.inventory);
// Rebuild the userStats text format
const statsText =
`Health: ${stats.health}%\n` +
`Satiety: ${stats.satiety}%\n` +
`Energy: ${stats.energy}%\n` +
`Hygiene: ${stats.hygiene}%\n` +
`Arousal: ${stats.arousal}%\n` +
`${stats.mood}: ${stats.conditions}\n` +
`${inventorySummary}`;
// Update BOTH lastGeneratedData AND committedTrackerData
// This makes manual edits immediately visible to AI
lastGeneratedData.userStats = statsText;
committedTrackerData.userStats = statsText;
}
+5
View File
@@ -7,6 +7,7 @@ import { getContext } from '../../../../../../extensions.js';
import {
extensionSettings,
lastGeneratedData,
committedTrackerData,
$infoBoxContainer
} from '../../core/state.js';
import { saveChatData } from '../../core/persistence.js';
@@ -429,6 +430,10 @@ export function updateInfoBoxField(field, value) {
lastGeneratedData.infoBox = updatedLines.join('\n');
// Update BOTH lastGeneratedData AND committedTrackerData
// This makes manual edits immediately visible to AI
committedTrackerData.infoBox = updatedLines.join('\n');
// Update the message's swipe data
const chat = getContext().chat;
if (chat && chat.length > 0) {
+28 -10
View File
@@ -4,7 +4,8 @@
*/
import { extensionSettings, $inventoryContainer } from '../../core/state.js';
import { getInventoryRenderOptions } from '../interaction/inventoryActions.js';
import { getInventoryRenderOptions, restoreFormStates } from '../interaction/inventoryActions.js';
import { updateInventoryItem } from '../interaction/inventoryEdit.js';
import { parseItems } from '../../utils/itemParser.js';
// Type imports
@@ -62,14 +63,14 @@ export function renderOnPersonView(onPersonItems, viewMode = 'list') {
<button class="rpg-item-remove" data-action="remove-item" data-field="onPerson" data-index="${index}" title="Remove item">
<i class="fa-solid fa-times"></i>
</button>
<span class="rpg-item-name">${escapeHtml(item)}</span>
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="onPerson" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
</div>
`).join('');
} else {
// List view: full-width rows
itemsHtml = items.map((item, index) => `
<div class="rpg-item-row" data-field="onPerson" data-index="${index}">
<span class="rpg-item-name">${escapeHtml(item)}</span>
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="onPerson" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
<button class="rpg-item-remove" data-action="remove-item" data-field="onPerson" data-index="${index}" title="Remove item">
<i class="fa-solid fa-times"></i>
</button>
@@ -184,14 +185,14 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li
<button class="rpg-item-remove" data-action="remove-item" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="Remove item">
<i class="fa-solid fa-times"></i>
</button>
<span class="rpg-item-name">${escapeHtml(item)}</span>
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
</div>
`).join('');
} else {
// List view: full-width rows
itemsHtml = items.map((item, index) => `
<div class="rpg-item-row" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}">
<span class="rpg-item-name">${escapeHtml(item)}</span>
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
<button class="rpg-item-remove" data-action="remove-item" data-field="stored" data-location="${escapeHtml(location)}" data-index="${index}" title="Remove item">
<i class="fa-solid fa-times"></i>
</button>
@@ -210,9 +211,6 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li
</button>
<h5 class="rpg-storage-name">${escapeHtml(location)}</h5>
<div class="rpg-storage-actions">
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="stored" data-location="${escapeHtml(location)}" title="Add item to this location">
<i class="fa-solid fa-plus"></i>
</button>
<button class="rpg-inventory-remove-btn" data-action="remove-location" data-location="${escapeHtml(location)}" title="Remove this storage location">
<i class="fa-solid fa-trash"></i>
</button>
@@ -233,6 +231,11 @@ export function renderStoredView(stored, collapsedLocations = [], viewMode = 'li
<div class="rpg-item-list ${listViewClass}">
${itemsHtml}
</div>
<div class="rpg-storage-add-item-container">
<button class="rpg-inventory-add-btn" data-action="add-item" data-field="stored" data-location="${escapeHtml(location)}" title="Add item to this location">
<i class="fa-solid fa-plus"></i> Add Item
</button>
</div>
</div>
<div class="rpg-inline-confirmation" id="rpg-remove-confirm-${locationId}" style="display: none;">
<p>Remove "${escapeHtml(location)}"? This will delete all items stored there.</p>
@@ -278,14 +281,14 @@ export function renderAssetsView(assets, viewMode = 'list') {
<button class="rpg-item-remove" data-action="remove-item" data-field="assets" data-index="${index}" title="Remove asset">
<i class="fa-solid fa-times"></i>
</button>
<span class="rpg-item-name">${escapeHtml(item)}</span>
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="assets" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
</div>
`).join('');
} else {
// List view: full-width rows
itemsHtml = items.map((item, index) => `
<div class="rpg-item-row" data-field="assets" data-index="${index}">
<span class="rpg-item-name">${escapeHtml(item)}</span>
<span class="rpg-item-name rpg-editable" contenteditable="true" data-field="assets" data-index="${index}" title="Click to edit">${escapeHtml(item)}</span>
<button class="rpg-item-remove" data-action="remove-item" data-field="assets" data-index="${index}" title="Remove asset">
<i class="fa-solid fa-times"></i>
</button>
@@ -436,6 +439,9 @@ export function updateInventoryDisplay(containerId, options = {}) {
const inventory = extensionSettings.userStats.inventory;
const html = generateInventoryHTML(inventory, options);
container.innerHTML = html;
// Restore form states after re-rendering
restoreFormStates();
}
/**
@@ -458,6 +464,18 @@ export function renderInventory() {
// Generate HTML and update DOM
const html = generateInventoryHTML(inventory, options);
$inventoryContainer.html(html);
// Restore form states after re-rendering (fixes Bug #1)
restoreFormStates();
// Event listener for editing item names (mobile-friendly contenteditable)
$inventoryContainer.find('.rpg-item-name.rpg-editable').on('blur', function() {
const field = $(this).data('field');
const index = parseInt($(this).data('index'));
const location = $(this).data('location');
const newName = $(this).text().trim();
updateInventoryItem(field, index, newName, location);
});
}
/**
+36 -3
View File
@@ -9,12 +9,41 @@ import { selected_group, getGroupMembers } from '../../../../../../group-chats.j
import {
extensionSettings,
lastGeneratedData,
committedTrackerData,
$thoughtsContainer,
FALLBACK_AVATAR_DATA_URI
} from '../../core/state.js';
import { saveChatData } from '../../core/persistence.js';
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
/**
* Fuzzy name matching that handles:
* - Exact matches: "Sabrina" === "Sabrina"
* - Parenthetical additions: "Sabrina" matches "Sabrina (Margrokha's Avatar)"
* - Title additions: "Sabrina" matches "Princess Sabrina"
* - Word boundaries: "Sabrina" won't match "Sabrina's Mother"
*
* @param {string} cardName - Name from the character card
* @param {string} aiName - Name generated by the AI
* @returns {boolean} True if names match
*/
function namesMatch(cardName, aiName) {
if (!cardName || !aiName) return false;
// 1. Exact match (fast path)
if (cardName.toLowerCase() === aiName.toLowerCase()) return true;
// 2. Strip parentheses and match
const stripParens = (s) => s.replace(/\s*\([^)]*\)/g, '').trim();
const cardCore = stripParens(cardName).toLowerCase();
const aiCore = stripParens(aiName).toLowerCase();
if (cardCore === aiCore) return true;
// 3. Check if card name appears as complete word in AI name
const wordBoundary = new RegExp(`\\b${cardCore}\\b`);
return wordBoundary.test(aiCore);
}
/**
* Renders character thoughts (Present Characters) panel.
* Displays character cards with avatars, relationship badges, and traits.
@@ -138,7 +167,7 @@ export function renderThoughts() {
if (selected_group) {
const groupMembers = getGroupMembers(selected_group);
const matchingMember = groupMembers.find(member =>
member && member.name && member.name.toLowerCase() === char.name.toLowerCase()
member && member.name && namesMatch(member.name, char.name)
);
if (matchingMember && matchingMember.avatar && matchingMember.avatar !== 'none') {
@@ -152,7 +181,7 @@ export function renderThoughts() {
// For regular chats or if not found in group, search all characters
if (characterPortrait === FALLBACK_AVATAR_DATA_URI && characters && characters.length > 0) {
const matchingCharacter = characters.find(c =>
c && c.name && c.name.toLowerCase() === char.name.toLowerCase()
c && c.name && namesMatch(c.name, char.name)
);
if (matchingCharacter && matchingCharacter.avatar && matchingCharacter.avatar !== 'none') {
@@ -165,7 +194,7 @@ export function renderThoughts() {
// If this is the current character in a 1-on-1 chat, use their portrait
if (this_chid !== undefined && characters[this_chid] &&
characters[this_chid].name && characters[this_chid].name.toLowerCase() === char.name.toLowerCase()) {
characters[this_chid].name && namesMatch(characters[this_chid].name, char.name)) {
const thumbnailUrl = getSafeThumbnailUrl('avatar', characters[this_chid].avatar);
if (thumbnailUrl) {
characterPortrait = thumbnailUrl;
@@ -320,6 +349,10 @@ export function updateCharacterField(characterName, field, value) {
lastGeneratedData.characterThoughts = updatedLines.join('\n');
// console.log('[RPG Companion] 💾 Updated lastGeneratedData.characterThoughts:', lastGeneratedData.characterThoughts);
// Update BOTH lastGeneratedData AND committedTrackerData
// This makes manual edits immediately visible to AI
committedTrackerData.characterThoughts = updatedLines.join('\n');
// Also update the last assistant message's swipe data
const chat = getContext().chat;
if (chat && chat.length > 0) {
+27 -11
View File
@@ -8,6 +8,7 @@ import { user_avatar } from '../../../../../../../script.js';
import {
extensionSettings,
lastGeneratedData,
committedTrackerData,
$userStatsContainer,
FALLBACK_AVATAR_DATA_URI
} from '../../core/state.js';
@@ -17,6 +18,7 @@ import {
updateMessageSwipeData
} from '../../core/persistence.js';
import { getSafeThumbnailUrl } from '../../utils/avatars.js';
import { buildInventorySummary } from '../generation/promptBuilder.js';
/**
* Renders the user stats panel with health bars, mood, inventory, and classic stats.
@@ -53,7 +55,7 @@ export function renderUserStats() {
const html = `
<div class="rpg-stats-content">
<div class="rpg-stats-left">
<div style="display: flex; gap: clamp(4px, 0.8vh, 8px); align-items: center; flex-shrink: 0;">
<div style="display: flex; gap: clamp(4px, 0.8vh, 8px); align-items: center; justify-content: center; flex-shrink: 0;">
<img src="${userPortrait}" alt="${userName}" class="rpg-user-portrait" onerror="this.style.opacity='0.5';this.onerror=null;" />
</div>
<div class="rpg-stats-grid">
@@ -178,13 +180,15 @@ export function renderUserStats() {
// Update the setting
extensionSettings.userStats[field] = value;
// Also update lastGeneratedData to keep it in sync
if (!lastGeneratedData.userStats) {
lastGeneratedData.userStats = '';
}
// Regenerate the userStats text with updated value
const statsText = `Health: ${extensionSettings.userStats.health}%\nSatiety: ${extensionSettings.userStats.satiety}%\nEnergy: ${extensionSettings.userStats.energy}%\nHygiene: ${extensionSettings.userStats.hygiene}%\nArousal: ${extensionSettings.userStats.arousal}%\n${extensionSettings.userStats.mood}: ${extensionSettings.userStats.conditions}\nInventory: ${extensionSettings.userStats.inventory}`;
// Rebuild userStats text with proper inventory format
const stats = extensionSettings.userStats;
const inventorySummary = buildInventorySummary(stats.inventory);
const statsText = `Health: ${stats.health}%\nSatiety: ${stats.satiety}%\nEnergy: ${stats.energy}%\nHygiene: ${stats.hygiene}%\nArousal: ${stats.arousal}%\n${stats.mood}: ${stats.conditions}\n${inventorySummary}`;
// Update BOTH lastGeneratedData AND committedTrackerData
// This makes manual edits immediately visible to AI
lastGeneratedData.userStats = statsText;
committedTrackerData.userStats = statsText;
saveSettings();
saveChatData();
@@ -199,9 +203,15 @@ export function renderUserStats() {
const value = $(this).text().trim();
extensionSettings.userStats.mood = value || '😐';
// Update lastGeneratedData
const statsText = `Health: ${extensionSettings.userStats.health}%\nSatiety: ${extensionSettings.userStats.satiety}%\nEnergy: ${extensionSettings.userStats.energy}%\nHygiene: ${extensionSettings.userStats.hygiene}%\nArousal: ${extensionSettings.userStats.arousal}%\n${extensionSettings.userStats.mood}: ${extensionSettings.userStats.conditions}\nInventory: ${extensionSettings.userStats.inventory}`;
// Rebuild userStats text with proper inventory format
const stats = extensionSettings.userStats;
const inventorySummary = buildInventorySummary(stats.inventory);
const statsText = `Health: ${stats.health}%\nSatiety: ${stats.satiety}%\nEnergy: ${stats.energy}%\nHygiene: ${stats.hygiene}%\nArousal: ${stats.arousal}%\n${stats.mood}: ${stats.conditions}\n${inventorySummary}`;
// Update BOTH lastGeneratedData AND committedTrackerData
// This makes manual edits immediately visible to AI
lastGeneratedData.userStats = statsText;
committedTrackerData.userStats = statsText;
saveSettings();
saveChatData();
@@ -212,9 +222,15 @@ export function renderUserStats() {
const value = $(this).text().trim();
extensionSettings.userStats.conditions = value || 'None';
// Update lastGeneratedData
const statsText = `Health: ${extensionSettings.userStats.health}%\nSatiety: ${extensionSettings.userStats.satiety}%\nEnergy: ${extensionSettings.userStats.energy}%\nHygiene: ${extensionSettings.userStats.hygiene}%\nArousal: ${extensionSettings.userStats.arousal}%\n${extensionSettings.userStats.mood}: ${extensionSettings.userStats.conditions}\nInventory: ${extensionSettings.userStats.inventory}`;
// Rebuild userStats text with proper inventory format
const stats = extensionSettings.userStats;
const inventorySummary = buildInventorySummary(stats.inventory);
const statsText = `Health: ${stats.health}%\nSatiety: ${stats.satiety}%\nEnergy: ${stats.energy}%\nHygiene: ${stats.hygiene}%\nArousal: ${stats.arousal}%\n${stats.mood}: ${stats.conditions}\n${inventorySummary}`;
// Update BOTH lastGeneratedData AND committedTrackerData
// This makes manual edits immediately visible to AI
lastGeneratedData.userStats = statsText;
committedTrackerData.userStats = statsText;
saveSettings();
saveChatData();