@sherif-fanous/pi-presets-plus 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/package.json +74 -0
  5. package/src/activation/active-state.ts +25 -0
  6. package/src/activation/apply.ts +236 -0
  7. package/src/activation/baseline.ts +32 -0
  8. package/src/activation/clear.ts +434 -0
  9. package/src/activation/dirty.ts +69 -0
  10. package/src/activation/drift-handlers.ts +71 -0
  11. package/src/activation/drift.ts +77 -0
  12. package/src/activation/same-set.ts +32 -0
  13. package/src/activation/state-matches.ts +29 -0
  14. package/src/activation/thinking.ts +54 -0
  15. package/src/commands/presets/clear.ts +18 -0
  16. package/src/commands/presets/index.ts +9 -0
  17. package/src/commands/presets/notify.ts +22 -0
  18. package/src/commands/presets/reload.ts +28 -0
  19. package/src/commands/presets/router.ts +139 -0
  20. package/src/commands/presets/status.ts +262 -0
  21. package/src/flag.ts +88 -0
  22. package/src/hotkey-conflicts.ts +136 -0
  23. package/src/hotkey-reload-baseline.ts +112 -0
  24. package/src/hotkeys.ts +104 -0
  25. package/src/index.ts +171 -0
  26. package/src/messages.ts +34 -0
  27. package/src/store/api.ts +262 -0
  28. package/src/store/load.ts +175 -0
  29. package/src/store/merge.ts +69 -0
  30. package/src/store/paths.ts +38 -0
  31. package/src/store/save.ts +75 -0
  32. package/src/store/validate.ts +195 -0
  33. package/src/types.ts +169 -0
  34. package/src/ui/confirm.ts +126 -0
  35. package/src/ui/editor.ts +1617 -0
  36. package/src/ui/filter.ts +79 -0
  37. package/src/ui/frame.ts +109 -0
  38. package/src/ui/hotkey-input.ts +242 -0
  39. package/src/ui/info-dialog.ts +118 -0
  40. package/src/ui/labels.ts +51 -0
  41. package/src/ui/picker-state.ts +151 -0
  42. package/src/ui/picker.ts +982 -0
  43. package/src/ui/reload-prompt.ts +59 -0
  44. package/src/ui/status.ts +55 -0
  45. package/src/ui/widgets.ts +274 -0
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Hotkey conflict annotation for loaded presets.
3
+ *
4
+ * Owns preset-vs-preset hotkey parsing and conflict marking. It does NOT own
5
+ * shortcut registration, notifications, or built-in keybinding checks.
6
+ */
7
+ import type { LoadedPreset, PresetScope } from "./types.js";
8
+ import {
9
+ isPiBuiltin,
10
+ parseHotkey,
11
+ type ParsedHotkey,
12
+ } from "./ui/hotkey-input.js";
13
+
14
+ export interface HotkeyAnalysis {
15
+ readonly conflicts: HotkeyConflict[];
16
+ /**
17
+ * Invalid declarations found during annotation.
18
+ *
19
+ * This is per-load diagnostic data. User-facing callers should emit these
20
+ * warnings only at deliberate notification boundaries (for example, once at
21
+ * session-start) rather than every time storage is re-read.
22
+ */
23
+ readonly invalid: HotkeyDiagnostic[];
24
+ readonly parsed: ReadonlyMap<LoadedPreset, ParsedHotkey>;
25
+ }
26
+
27
+ export interface HotkeyConflict {
28
+ readonly loser: LoadedPreset & { hotkey: string };
29
+ readonly winner: PresetIdentity;
30
+ }
31
+
32
+ export interface HotkeyDiagnostic {
33
+ readonly preset: LoadedPreset & { hotkey: string };
34
+ /** Short, human-readable parse failure reason safe to show in UI copy. */
35
+ readonly reason: string;
36
+ }
37
+
38
+ export interface PresetIdentity {
39
+ readonly name: string;
40
+ readonly scope: PresetScope;
41
+ }
42
+
43
+ /**
44
+ * Annotate presets with hotkey conflict markers and return parsed hotkey data.
45
+ *
46
+ * Mutates freshly-loaded presets so every UI path can read one canonical
47
+ * annotation without maintaining a parallel conflict map. Before recomputing,
48
+ * it clears all existing hotkey markers so stale annotations from a previous
49
+ * load cannot persist. Invalid hotkeys are reported as ignored and do not
50
+ * participate in conflict detection because they do not identify a valid chord.
51
+ */
52
+ export function annotateAndAnalyzeHotkeys(
53
+ presets: LoadedPreset[],
54
+ ): HotkeyAnalysis {
55
+ const claimed = new Map<string, PresetIdentity>();
56
+ const conflicts: HotkeyConflict[] = [];
57
+ const invalid: HotkeyDiagnostic[] = [];
58
+ const parsedHotkeys = new Map<LoadedPreset, ParsedHotkey>();
59
+
60
+ for (const preset of presets) {
61
+ preset.hotkeyConflict = undefined;
62
+ preset.hotkeyShadowsBuiltin = undefined;
63
+
64
+ const { hotkey } = preset;
65
+
66
+ if (!hotkey) continue;
67
+
68
+ const presetWithHotkey: LoadedPreset & { hotkey: string } = {
69
+ ...preset,
70
+ hotkey,
71
+ };
72
+ const parsed = parseHotkey(hotkey);
73
+
74
+ if (!parsed.ok) {
75
+ invalid.push({ preset: presetWithHotkey, reason: parsed.reason });
76
+
77
+ continue;
78
+ }
79
+
80
+ parsedHotkeys.set(preset, parsed.parsed);
81
+
82
+ if (isPiBuiltin(parsed.parsed)) {
83
+ preset.hotkeyShadowsBuiltin = true;
84
+ }
85
+
86
+ // Annotate before the shadowed-skip so muted picker entries still explain the shadowing.
87
+ if (preset.shadowed) continue;
88
+
89
+ const winner = claimed.get(parsed.parsed.normalized);
90
+
91
+ if (winner) {
92
+ preset.hotkeyConflict = true;
93
+ conflicts.push({ loser: presetWithHotkey, winner });
94
+
95
+ continue;
96
+ }
97
+
98
+ claimed.set(parsed.parsed.normalized, {
99
+ name: preset.name,
100
+ scope: preset.scope,
101
+ });
102
+ }
103
+
104
+ return { conflicts, invalid, parsed: parsedHotkeys };
105
+ }
106
+
107
+ /** Returns `"<name>" (<scope>)`, including the quotes around the name. */
108
+ export function formatPresetIdentity(identity: PresetIdentity): string {
109
+ return `"${identity.name}" (${identity.scope})`;
110
+ }
111
+
112
+ /**
113
+ * Return whether two hotkey declarations differ after commit-time cleanup.
114
+ *
115
+ * Both values are trimmed before comparison, and `undefined` is treated the
116
+ * same as an empty or whitespace-only string. Parseable hotkeys compare by
117
+ * their normalized chord, so casing and modifier order do not matter; strings
118
+ * that fail parsing fall back to trimmed literal comparison. This matches
119
+ * persistence forms where absent and cleared hotkeys both mean "no binding".
120
+ */
121
+ export function hotkeyChanged(
122
+ prev: string | undefined,
123
+ next: string | undefined,
124
+ ): boolean {
125
+ return normalizeHotkeyForChange(prev) !== normalizeHotkeyForChange(next);
126
+ }
127
+
128
+ function normalizeHotkeyForChange(hotkey: string | undefined): string {
129
+ const trimmed = hotkey?.trim() ?? "";
130
+
131
+ if (trimmed.length === 0) return "";
132
+
133
+ const parsed = parseHotkey(trimmed);
134
+
135
+ return parsed.ok ? parsed.parsed.normalized : trimmed;
136
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Runtime hotkey baseline used to decide whether `/reload` is still needed.
3
+ *
4
+ * Owns the in-memory snapshot of hotkeys registered for this extension runtime;
5
+ * it does NOT own storage reads, conflict analysis, or shortcut registration.
6
+ */
7
+ import { hotkeyChanged, type PresetIdentity } from "./hotkey-conflicts.js";
8
+ import type { LoadedPreset } from "./types.js";
9
+
10
+ const acknowledgedPendingHotkeys = new Map<string, string | undefined>();
11
+ const runtimeHotkeys = new Map<string, string | undefined>();
12
+
13
+ /** Clear runtime hotkey state for tests that exercise prompt decisions. */
14
+ export function clearRuntimeHotkeyBaseline(): void {
15
+ acknowledgedPendingHotkeys.clear();
16
+ runtimeHotkeys.clear();
17
+ }
18
+
19
+ /** Return whether deleting `identity` leaves runtime bindings out of date. */
20
+ export function deleteNeedsHotkeyReload(identity: PresetIdentity): boolean {
21
+ return commitNeedsHotkeyReload(identity, undefined);
22
+ }
23
+
24
+ /** Remember a declined prompt so the same pending state is not re-prompted. */
25
+ export function recordReloadPromptDeclined(
26
+ identity: PresetIdentity & { readonly hotkey?: string | undefined },
27
+ hotkey = identity.hotkey,
28
+ ): void {
29
+ acknowledgedPendingHotkeys.set(presetKey(identity), hotkey);
30
+ }
31
+
32
+ /** Return whether saving `saved` leaves runtime bindings out of date. */
33
+ export function saveNeedsHotkeyReload(
34
+ initial: PresetIdentity | undefined,
35
+ saved: PresetIdentity & { readonly hotkey?: string | undefined },
36
+ ): boolean {
37
+ if (!commitNeedsHotkeyReload(saved, saved.hotkey)) return false;
38
+
39
+ const initialRuntimeHotkey = runtimeHotkeyFor(initial);
40
+
41
+ if (!hotkeyChanged(initialRuntimeHotkey, saved.hotkey)) {
42
+ return (
43
+ Boolean(initialRuntimeHotkey?.trim()) && identityChanged(initial, saved)
44
+ );
45
+ }
46
+
47
+ return true;
48
+ }
49
+
50
+ /** Capture the hotkeys that are actually represented by this runtime. */
51
+ export function setRuntimeHotkeyBaseline(
52
+ presets: readonly LoadedPreset[],
53
+ ): void {
54
+ acknowledgedPendingHotkeys.clear();
55
+ runtimeHotkeys.clear();
56
+
57
+ for (const preset of presets) {
58
+ runtimeHotkeys.set(presetKey(preset), preset.hotkey);
59
+ }
60
+ }
61
+
62
+ function acknowledgedPendingHotkeyMatches(
63
+ identity: PresetIdentity & { readonly hotkey?: string | undefined },
64
+ ): boolean {
65
+ if (!acknowledgedPendingHotkeys.has(presetKey(identity))) return false;
66
+
67
+ return !hotkeyChanged(
68
+ acknowledgedPendingHotkeys.get(presetKey(identity)),
69
+ identity.hotkey,
70
+ );
71
+ }
72
+
73
+ function commitNeedsHotkeyReload(
74
+ identity: PresetIdentity,
75
+ hotkey: string | undefined,
76
+ ): boolean {
77
+ if (runtimeMatches(identity, hotkey)) {
78
+ acknowledgedPendingHotkeys.delete(presetKey(identity));
79
+
80
+ return false;
81
+ }
82
+
83
+ return !acknowledgedPendingHotkeyMatches({ ...identity, hotkey });
84
+ }
85
+
86
+ function identityChanged(
87
+ prev: PresetIdentity | undefined,
88
+ next: PresetIdentity,
89
+ ): boolean {
90
+ if (!prev) return false;
91
+
92
+ return prev.name !== next.name || prev.scope !== next.scope;
93
+ }
94
+
95
+ function presetKey(identity: PresetIdentity): string {
96
+ return `${identity.scope}:${identity.name}`;
97
+ }
98
+
99
+ function runtimeHotkeyFor(
100
+ identity: PresetIdentity | undefined,
101
+ ): string | undefined {
102
+ if (!identity) return undefined;
103
+
104
+ return runtimeHotkeys.get(presetKey(identity));
105
+ }
106
+
107
+ function runtimeMatches(
108
+ identity: PresetIdentity,
109
+ hotkey: string | undefined,
110
+ ): boolean {
111
+ return !hotkeyChanged(runtimeHotkeyFor(identity), hotkey);
112
+ }
package/src/hotkeys.ts ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Per-preset shortcut registration for pi-presets-plus.
3
+ *
4
+ * Owns session-start shortcut registration, conflict/invalid notifications,
5
+ * and shortcut activation guardrails. It does NOT own hotkey parsing,
6
+ * conflict marking, persistent storage, or the core preset apply implementation.
7
+ */
8
+ import { apply } from "./activation/apply.js";
9
+ import {
10
+ formatPresetIdentity,
11
+ type HotkeyAnalysis,
12
+ } from "./hotkey-conflicts.js";
13
+ import type { LoadedPreset } from "./types.js";
14
+ import { isPiBuiltin } from "./ui/hotkey-input.js";
15
+ import type {
16
+ ExtensionAPI,
17
+ ExtensionContext,
18
+ } from "@mariozechner/pi-coding-agent";
19
+ import type { KeyId } from "@mariozechner/pi-tui";
20
+
21
+ export type CurrentPresetsLoader = (
22
+ ctx: ExtensionContext,
23
+ ) => Promise<LoadedPreset[]>;
24
+
25
+ export function registerHotkeys(
26
+ pi: ExtensionAPI,
27
+ ctx: Pick<ExtensionContext, "ui">,
28
+ presets: LoadedPreset[],
29
+ hotkeyAnalysis: HotkeyAnalysis,
30
+ loadCurrentPresets: CurrentPresetsLoader,
31
+ ): void {
32
+ for (const conflict of hotkeyAnalysis.conflicts) {
33
+ ctx.ui.notify(
34
+ `${formatPresetSubject(conflict.loser)} hotkey "${conflict.loser.hotkey}" conflicts with preset ${formatPresetIdentity(conflict.winner)}. The first registered wins.`,
35
+ "warning",
36
+ );
37
+ }
38
+
39
+ for (const invalid of hotkeyAnalysis.invalid) {
40
+ ctx.ui.notify(
41
+ `${formatPresetSubject(invalid.preset)}: invalid hotkey "${invalid.preset.hotkey}" — ignored (${invalid.reason}). It will not be registered or considered for conflicts until fixed.`,
42
+ "warning",
43
+ );
44
+ }
45
+
46
+ // Registration is intentionally one-shot because pi exposes no
47
+ // unregisterShortcut API. loadAll() marks conflicts on every read for the
48
+ // picker; this notification pass only explains the session-start bindings.
49
+ for (const preset of presets) {
50
+ const parsed = hotkeyAnalysis.parsed.get(preset);
51
+
52
+ if (!parsed) continue;
53
+ if (preset.shadowed || preset.hotkeyConflict === true) continue;
54
+
55
+ if (isPiBuiltin(parsed)) {
56
+ ctx.ui.notify(
57
+ `${formatPresetSubject(preset)} hotkey "${preset.hotkey}" shadows a Pi built-in. The preset binding will take precedence.`,
58
+ "info",
59
+ );
60
+ }
61
+
62
+ const registeredName = preset.name;
63
+ const registeredScope = preset.scope;
64
+
65
+ pi.registerShortcut(parsed.normalized as KeyId, {
66
+ description: `Activate preset "${registeredName}"`,
67
+ handler: async (handlerCtx) => {
68
+ try {
69
+ // Re-read on every press so edits/removals made after session-start
70
+ // are honored without re-registering shortcuts. This trades a small
71
+ // amount of I/O for correctness until pi exposes unregister support.
72
+ const currentPresets = await loadCurrentPresets(handlerCtx);
73
+ const current = currentPresets.find(
74
+ (candidate) =>
75
+ candidate.name === registeredName &&
76
+ candidate.scope === registeredScope,
77
+ );
78
+
79
+ if (!current) {
80
+ handlerCtx.ui.notify(
81
+ `Preset "${registeredName}" no longer exists.`,
82
+ "warning",
83
+ );
84
+
85
+ return;
86
+ }
87
+
88
+ const result = await apply(current, handlerCtx, pi);
89
+
90
+ if (!result.ok) handlerCtx.ui.notify(result.reason, "error");
91
+ } catch (err) {
92
+ handlerCtx.ui.notify(
93
+ `pi-presets-plus failed to activate preset "${registeredName}" from hotkey: ${err instanceof Error ? err.message : String(err)}.`,
94
+ "error",
95
+ );
96
+ }
97
+ },
98
+ });
99
+ }
100
+ }
101
+
102
+ function formatPresetSubject(preset: Pick<LoadedPreset, "name">): string {
103
+ return `Preset "${preset.name}"`;
104
+ }
package/src/index.ts ADDED
@@ -0,0 +1,171 @@
1
+ /**
2
+ * pi-presets-plus extension entry point.
3
+ *
4
+ * Owns lifecycle wiring with the pi host: command registration, custom
5
+ * message renderers, session-start pre-warming and restore, instruction
6
+ * injection, and self-call guards. It does NOT own storage, activation,
7
+ * or UI internals — those live in their dedicated modules.
8
+ */
9
+
10
+ import {
11
+ clearActive,
12
+ getActive,
13
+ setActive,
14
+ } from "./activation/active-state.js";
15
+ import {
16
+ handleModelSelectDrift,
17
+ syncDirtyFromCurrentState,
18
+ } from "./activation/drift-handlers.js";
19
+ import { snapshotPresetForDrift } from "./activation/drift.js";
20
+ import {
21
+ getArgumentCompletions,
22
+ handlePresetsCommand,
23
+ surfaceWarnings,
24
+ } from "./commands/presets/index.js";
25
+ import { applyPresetFlag, registerPresetFlag } from "./flag.js";
26
+ import { setRuntimeHotkeyBaseline } from "./hotkey-reload-baseline.js";
27
+ import { registerHotkeys, type CurrentPresetsLoader } from "./hotkeys.js";
28
+ import { ACTIVATED_MESSAGE_TYPE, renderActivatedMessage } from "./messages.js";
29
+ import { loadAll } from "./store/api.js";
30
+ import type { LoadedPreset, PresetScope } from "./types.js";
31
+ import { updateStatus } from "./ui/status.js";
32
+ import type {
33
+ ExtensionAPI,
34
+ ExtensionContext,
35
+ } from "@mariozechner/pi-coding-agent";
36
+
37
+ interface ActiveEntryData {
38
+ name: string | null;
39
+ scope?: PresetScope;
40
+ }
41
+
42
+ export default function presetsPlus(pi: ExtensionAPI) {
43
+ pi.registerMessageRenderer(ACTIVATED_MESSAGE_TYPE, renderActivatedMessage);
44
+ registerPresetFlag(pi);
45
+
46
+ pi.registerCommand("presets", {
47
+ description:
48
+ "Browse and switch presets that bundle a model, thinking level, tools, and system prompt. Run `/presets` to open the picker, or use `reload`, `clear`, or `status`.",
49
+ getArgumentCompletions: (prefix) => getArgumentCompletions(prefix),
50
+ handler: (args, ctx) => handlePresetsCommand(args, ctx, pi),
51
+ });
52
+
53
+ pi.on("session_start", async (_event, ctx) => {
54
+ try {
55
+ const { hotkeyAnalysis, presets, warnings } = await loadAll(ctx);
56
+
57
+ surfaceWarnings(ctx, warnings);
58
+ restoreActiveFromBranch(ctx, presets);
59
+ await applyPresetFlag(pi, ctx, presets);
60
+
61
+ const loadCurrentPresets: CurrentPresetsLoader = async (handlerCtx) =>
62
+ (await loadAll(handlerCtx)).presets;
63
+
64
+ setRuntimeHotkeyBaseline(presets);
65
+ registerHotkeys(pi, ctx, presets, hotkeyAnalysis, loadCurrentPresets);
66
+
67
+ updateStatus(ctx, getActive(), (name, scope) =>
68
+ presets.find(
69
+ (preset) => preset.name === name && preset.scope === scope,
70
+ ),
71
+ );
72
+ } catch (err) {
73
+ ctx.ui.notify(
74
+ `pi-presets-plus failed to load preset files: ${err instanceof Error ? err.message : String(err)}.`,
75
+ "error",
76
+ );
77
+ }
78
+ });
79
+
80
+ pi.on("before_agent_start", async (event, ctx) => {
81
+ const active = getActive();
82
+
83
+ if (!active) return undefined;
84
+
85
+ // Don't re-surface warnings here. They were already shown at
86
+ // session_start and on /presets reload; emitting them on every
87
+ // agent turn would be noisy when a preset file has issues.
88
+ const { presets } = await loadAll(ctx);
89
+
90
+ const preset = presets.find(
91
+ (candidate) =>
92
+ candidate.name === active.name && candidate.scope === active.scope,
93
+ );
94
+
95
+ if (!preset?.instructions) return undefined;
96
+
97
+ return { systemPrompt: `${event.systemPrompt}\n\n${preset.instructions}` };
98
+ });
99
+
100
+ pi.on("model_select", async (event, ctx) => {
101
+ await handleModelSelectDrift(event, ctx, pi);
102
+ });
103
+
104
+ pi.on("thinking_level_select", async (_event, ctx) => {
105
+ await syncDirtyFromCurrentState(ctx, pi);
106
+ });
107
+
108
+ pi.on("turn_start", async (_event, ctx) => {
109
+ await syncDirtyFromCurrentState(ctx, pi);
110
+ });
111
+ }
112
+
113
+ function restoreActiveFromBranch(
114
+ ctx: Pick<ExtensionContext, "sessionManager" | "ui">,
115
+ presets: readonly LoadedPreset[],
116
+ ): void {
117
+ const activeEntry = [...ctx.sessionManager.getBranch()]
118
+ .reverse()
119
+ .find(
120
+ (entry): entry is Extract<typeof entry, { type: "custom" }> =>
121
+ entry.type === "custom" && entry.customType === "presets-plus:active",
122
+ );
123
+
124
+ if (!activeEntry) {
125
+ clearActive();
126
+
127
+ return;
128
+ }
129
+
130
+ const data = activeEntry.data as ActiveEntryData | undefined;
131
+
132
+ if (!data || data.name === null) {
133
+ clearActive();
134
+
135
+ return;
136
+ }
137
+
138
+ const preset = presets.find(
139
+ (candidate) =>
140
+ candidate.name === data.name &&
141
+ candidate.scope === (data.scope ?? "user"),
142
+ );
143
+
144
+ if (!preset) {
145
+ ctx.ui.notify(
146
+ `Restored session referenced preset "${data.name}" which is not loaded. Not attaching.`,
147
+ "warning",
148
+ );
149
+ clearActive();
150
+
151
+ return;
152
+ }
153
+
154
+ if (preset.unavailable) {
155
+ ctx.ui.notify(
156
+ `Restored session referenced preset "${data.name}" which is unavailable (${preset.unavailable}). Not attaching.`,
157
+ "warning",
158
+ );
159
+ clearActive();
160
+
161
+ return;
162
+ }
163
+
164
+ setActive({
165
+ declared: snapshotPresetForDrift(preset),
166
+ dirty: false,
167
+ name: preset.name,
168
+ restore: { kind: "unknown" },
169
+ scope: preset.scope,
170
+ });
171
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Custom conversation message types and renderers for pi-presets-plus.
3
+ *
4
+ * Owns the message shapes and how they appear in the conversation log; it
5
+ * does NOT send messages or own activation logic.
6
+ */
7
+ import type { ThinkingLevel } from "./types.js";
8
+ import type { MessageRenderer } from "@mariozechner/pi-coding-agent";
9
+ import { Text } from "@mariozechner/pi-tui";
10
+
11
+ export const ACTIVATED_MESSAGE_TYPE = "presets-plus:activated";
12
+
13
+ interface ActivatedMessageDetails {
14
+ name: string;
15
+ model: string;
16
+ thinkingLevel: ThinkingLevel;
17
+ }
18
+
19
+ export function formatActivatedMessage(
20
+ details: ActivatedMessageDetails,
21
+ theme: Parameters<MessageRenderer<ActivatedMessageDetails>>[2],
22
+ ): string {
23
+ return theme.fg("muted", `Preset ${details.name} applied`);
24
+ }
25
+
26
+ export const renderActivatedMessage: MessageRenderer<
27
+ ActivatedMessageDetails
28
+ > = (message, _options, theme) => {
29
+ const details = message.details;
30
+
31
+ if (!details) return undefined;
32
+
33
+ return new Text(formatActivatedMessage(details, theme));
34
+ };