Implement {{charFirstMessage}} / {{greeting}} macro with alternate greeting indexing and substitution (#5220) * Initial plan * Implement {{firstMessage}} / {{greeting}} macro for character's first message Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * Simplify firstMessage resolver to use || instead of ?? for consistency Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * Remove {{firstMessage}} alias, add 0-based greeting index for alternate greetings Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * Add alternateGreetings to env, apply substitution to greetings Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * Remove legacy greeting macro, keep only new engine implementation Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com>
Signed| @@ -3230,6 +3230,8 @@ export function baseChatReplace(value, name1Override = null, name2Override = nul | ||
| 3230 | 3230 | * @property {string} version Character version |
| 3231 | 3231 | * @property {string} charDepthPrompt Character depth note |
| 3232 | 3232 | * @property {string} creatorNotes Character creator notes |
| 3233 | + * @property {string} firstMessage Character first message / greeting | |
| 3234 | + * @property {string[]} alternateGreetings Character alternate greetings | |
| 3233 | 3235 | */ |
| 3234 | 3236 | |
| 3235 | 3237 | /** |
| @@ -3316,6 +3318,17 @@ export function getCharacterCardFieldsLazy({ chid = undefined } = {}) { | ||
| 3316 | 3318 | const exampleDialog = chat_metadata.mes_example || character.mes_example || ''; |
| 3317 | 3319 | return baseChatReplace(exampleDialog.trim()); |
| 3318 | 3320 | }, |
| 3321 | + firstMessage: () => { | |
| 3322 | + if (!character) return ''; | |
| 3323 | + const firstMes = character.first_mes?.trim() || ''; | |
| 3324 | + return baseChatReplace(firstMes); | |
| 3325 | + }, | |
| 3326 | + alternateGreetings: () => { | |
| 3327 | + if (!character) return []; | |
| 3328 | + const altGreetings = character.data?.alternate_greetings; | |
| 3329 | + if (!Array.isArray(altGreetings)) return []; | |
| 3330 | + return altGreetings.map(greeting => baseChatReplace(greeting?.trim())); | |
| 3331 | + }, | |
| 3319 | 3332 | }; |
| 3320 | 3333 | |
| 3321 | 3334 | return createLazyFields(resolvers); |
| @@ -3342,6 +3355,8 @@ export function getCharacterCardFields({ chid = undefined } = {}) { | ||
| 3342 | 3355 | version: lazy.version, |
| 3343 | 3356 | charDepthPrompt: lazy.charDepthPrompt, |
| 3344 | 3357 | creatorNotes: lazy.creatorNotes, |
| 3358 | + firstMessage: lazy.firstMessage, | |
| 3359 | + alternateGreetings: lazy.alternateGreetings, | |
| 3345 | 3360 | }; |
| 3346 | 3361 | } |
| 3347 | 3362 | |
| @@ -140,6 +140,30 @@ export function registerEnvMacros() { | ||
| 140 | 140 | handler: ({ env }) => env.character.creatorNotes ?? '', |
| 141 | 141 | }); |
| 142 | 142 | |
| 143 | + MacroRegistry.registerMacro('charFirstMessage', { | |
| 144 | + aliases: [{ alias: 'greeting' }], | |
| 145 | + category: MacroCategory.CHARACTER, | |
| 146 | + unnamedArgs: [ | |
| 147 | + { | |
| 148 | + name: 'index', | |
| 149 | + optional: true, | |
| 150 | + defaultValue: '0', | |
| 151 | + type: MacroValueType.INTEGER, | |
| 152 | + description: '0-based index. 0 (default) returns the main greeting, 1 and up return alternate greetings.', | |
| 153 | + }, | |
| 154 | + ], | |
| 155 | + description: 'The character\'s first message / greeting. Optionally specify an index to access alternate greetings.', | |
| 156 | + returns: 'Character greeting at the given index, or empty string if out of bounds.', | |
| 157 | + exampleUsage: ['{{greeting}}', '{{greeting::0}}', '{{greeting::1}}'], | |
| 158 | + handler: ({ env, unnamedArgs: [index] }) => { | |
| 159 | + const i = Number(index ?? 0); | |
| 160 | + if (i === 0) return env.character.firstMessage ?? ''; | |
| 161 | + const altGreetings = env.character.alternateGreetings; | |
| 162 | + if (!Array.isArray(altGreetings)) return ''; | |
| 163 | + return altGreetings[i - 1] ?? ''; | |
| 164 | + }, | |
| 165 | + }); | |
| 166 | + | |
| 143 | 167 | // Character version macros (legacy variants and documented {{charVersion}}) |
| 144 | 168 | MacroRegistry.registerMacro('charVersion', { |
| 145 | 169 | aliases: [ |
| @@ -38,6 +38,8 @@ | ||
| 38 | 38 | * @property {string} [charDepthPrompt] |
| 39 | 39 | * @property {string} [creatorNotes] |
| 40 | 40 | * @property {string} [version] |
| 41 | + * @property {string} [firstMessage] | |
| 42 | + * @property {string[]} [alternateGreetings] | |
| 41 | 43 | */ |
| 42 | 44 | |
| 43 | 45 | /** |
| @@ -109,10 +109,19 @@ class MacroEnvBuilder { | ||
| 109 | 109 | ['version', 'version'], |
| 110 | 110 | ['charDepthPrompt', 'charDepthPrompt'], |
| 111 | 111 | ['creatorNotes', 'creatorNotes'], |
| 112 | + ['firstMessage', 'firstMessage'], | |
| 113 | + ['alternateGreetings', 'alternateGreetings'], | |
| 112 | 114 | ]); |
| 113 | 115 | for (const [envKey, fieldKey] of fieldMappings) { |
| 114 | 116 | Object.defineProperty(env.character, envKey, { |
| 115 | - get() { return fields[fieldKey] || ''; }, | |
| 117 | + get() { | |
| 118 | + const value = fields[fieldKey]; | |
| 119 | + // alternateGreetings should default to [] instead of '' | |
| 120 | + if (envKey === 'alternateGreetings') { | |
| 121 | + return Array.isArray(value) ? value : []; | |
| 122 | + } | |
| 123 | + return value || ''; | |
| 124 | + }, | |
| 116 | 125 | enumerable: true, |
| 117 | 126 | configurable: true, |
| 118 | 127 | }); |
| @@ -98,6 +98,8 @@ test.describe('MacroEnvBuilder', () => { | ||
| 98 | 98 | 'version', |
| 99 | 99 | 'charDepthPrompt', |
| 100 | 100 | 'creatorNotes', |
| 101 | + 'firstMessage', | |
| 102 | + 'alternateGreetings', | |
| 101 | 103 | ])); |
| 102 | 104 | }); |
| 103 | 105 | |