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 | * @property {string} version Character version | 3230 | * @property {string} version Character version |
| 3231 | * @property {string} charDepthPrompt Character depth note | 3231 | * @property {string} charDepthPrompt Character depth note |
| 3232 | * @property {string} creatorNotes Character creator notes | 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 | const exampleDialog = chat_metadata.mes_example || character.mes_example || ''; | 3318 | const exampleDialog = chat_metadata.mes_example || character.mes_example || ''; |
| 3317 | return baseChatReplace(exampleDialog.trim()); | 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 | return createLazyFields(resolvers); | 3334 | return createLazyFields(resolvers); |
| @@ -3342,6 +3355,8 @@ export function getCharacterCardFields({ chid = undefined } = {}) { | |||
| 3342 | version: lazy.version, | 3355 | version: lazy.version, |
| 3343 | charDepthPrompt: lazy.charDepthPrompt, | 3356 | charDepthPrompt: lazy.charDepthPrompt, |
| 3344 | creatorNotes: lazy.creatorNotes, | 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 | handler: ({ env }) => env.character.creatorNotes ?? '', | 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 | // Character version macros (legacy variants and documented {{charVersion}}) | 167 | // Character version macros (legacy variants and documented {{charVersion}}) |
| 144 | MacroRegistry.registerMacro('charVersion', { | 168 | MacroRegistry.registerMacro('charVersion', { |
| 145 | aliases: [ | 169 | aliases: [ |
| @@ -38,6 +38,8 @@ | |||
| 38 | * @property {string} [charDepthPrompt] | 38 | * @property {string} [charDepthPrompt] |
| 39 | * @property {string} [creatorNotes] | 39 | * @property {string} [creatorNotes] |
| 40 | * @property {string} [version] | 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 | ['version', 'version'], | 109 | ['version', 'version'], |
| 110 | ['charDepthPrompt', 'charDepthPrompt'], | 110 | ['charDepthPrompt', 'charDepthPrompt'], |
| 111 | ['creatorNotes', 'creatorNotes'], | 111 | ['creatorNotes', 'creatorNotes'], |
| 112 | ['firstMessage', 'firstMessage'], | ||
| 113 | ['alternateGreetings', 'alternateGreetings'], | ||
| 112 | ]); | 114 | ]); |
| 113 | for (const [envKey, fieldKey] of fieldMappings) { | 115 | for (const [envKey, fieldKey] of fieldMappings) { |
| 114 | Object.defineProperty(env.character, envKey, { | 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 | enumerable: true, | 125 | enumerable: true, |
| 117 | configurable: true, | 126 | configurable: true, |
| 118 | }); | 127 | }); |
| @@ -98,6 +98,8 @@ test.describe('MacroEnvBuilder', () => { | |||
| 98 | 'version', | 98 | 'version', |
| 99 | 'charDepthPrompt', | 99 | 'charDepthPrompt', |
| 100 | 'creatorNotes', | 100 | 'creatorNotes', |
| 101 | 'firstMessage', | ||
| 102 | 'alternateGreetings', | ||
| 101 | ])); | 103 | ])); |
| 102 | }); | 104 | }); |
| 103 | 105 | ||