feat(automation): expose group title and allow setting secondary text

Surface {{groupNow.*}} template variables (title, note, colour, times,
custom fields) in the automation template autocomplete so events inside a
group can reference their group. The runtime store already carries
groupNow, so substitution and filters worked already; this makes it
discoverable.

Extend the message-secondary action with an optional text field so an
automation can set the secondary message content, not just its source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZYPZVdZLWowU7DyzGkWyy
This commit is contained in:
Claude
2026-07-08 15:39:48 +00:00
committed by Carlos Valente
parent 25f0dba83a
commit 86b2132cdc
6 changed files with 102 additions and 26 deletions
@@ -163,5 +163,51 @@ describe('parseOutput', () => {
secondarySource: 'secondary',
});
});
it('parses message-secondary with a text value', () => {
expect(
parseOutput({
type: 'ontime',
action: 'message-secondary',
secondarySource: 'secondary',
text: 'hello',
}),
).toMatchObject({
secondarySource: 'secondary',
text: 'hello',
});
// an empty text is treated as no change
expect(
parseOutput({
type: 'ontime',
action: 'message-secondary',
secondarySource: 'secondary',
text: '',
}),
).toMatchObject({
secondarySource: 'secondary',
text: undefined,
});
// text can be set while clearing the secondary source
expect(
parseOutput({
type: 'ontime',
action: 'message-secondary',
secondarySource: null,
text: 'hello',
}),
).toMatchObject({
secondarySource: null,
text: 'hello',
});
expect(() =>
parseOutput({
type: 'ontime',
action: 'message-secondary',
secondarySource: 'secondary',
text: 123,
}),
).toThrow('Unexpected payload type:');
});
});
});
@@ -226,12 +226,21 @@ function parseOntimeAction(maybeOntimeAction: object): OntimeAction {
if (maybeOntimeAction.action === 'message-secondary') {
assert.hasKeys(maybeOntimeAction, ['secondarySource']);
// the secondary text is optional, an empty string is treated as no change
let text: string | undefined = undefined;
if ('text' in maybeOntimeAction) {
assert.isString(maybeOntimeAction.text);
text = indeterminateText(maybeOntimeAction.text);
}
// null is used to clear the secondary source
if (maybeOntimeAction.secondarySource === null) {
return {
type: 'ontime',
action: 'message-secondary',
secondarySource: null,
text,
};
}
@@ -240,6 +249,7 @@ function parseOntimeAction(maybeOntimeAction: object): OntimeAction {
type: 'ontime',
action: 'message-secondary',
secondarySource: chooseSecondarySource(maybeOntimeAction.secondarySource),
text,
};
}
@@ -66,6 +66,7 @@ export function toOntimeAction(action: OntimeAction) {
timer: {
secondarySource: action.secondarySource,
},
secondary: action.text,
});
break;
}