@pi-unipi/notify 0.1.7 → 0.1.9
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.
- package/commands.ts +8 -6
- package/events.ts +61 -44
- package/index.ts +2 -1
- package/package.json +1 -1
- package/skills/configure-notify/SKILL.md +43 -6
- package/tools.ts +11 -16
- package/tui/ntfy-setup.ts +68 -15
- package/tui/settings-overlay.ts +21 -6
package/commands.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { TelegramSetupOverlay } from "./tui/telegram-setup.js";
|
|
|
13
13
|
import { NtfySetupOverlay } from "./tui/ntfy-setup.js";
|
|
14
14
|
import { RecapModelSelectorOverlay } from "./tui/recap-model-selector.js";
|
|
15
15
|
import { loadConfig } from "./settings.js";
|
|
16
|
+
import { loadNtfyConfig } from "./ntfy-config.js";
|
|
16
17
|
import { sendNativeNotification } from "./platforms/native.js";
|
|
17
18
|
import { sendGotifyNotification } from "./platforms/gotify.js";
|
|
18
19
|
import { sendTelegramNotification } from "./platforms/telegram.js";
|
|
@@ -310,16 +311,17 @@ export function registerNotifyCommands(pi: ExtensionAPI): void {
|
|
|
310
311
|
}
|
|
311
312
|
}
|
|
312
313
|
|
|
313
|
-
// ntfy
|
|
314
|
-
|
|
314
|
+
// ntfy — resolved from project/global ntfy.json
|
|
315
|
+
const ntfyConfig = loadNtfyConfig(process.cwd());
|
|
316
|
+
if (ntfyConfig.enabled && ntfyConfig.serverUrl && ntfyConfig.topic) {
|
|
315
317
|
try {
|
|
316
318
|
await sendNtfyNotification(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
+
ntfyConfig.serverUrl,
|
|
320
|
+
ntfyConfig.topic,
|
|
319
321
|
title,
|
|
320
322
|
message,
|
|
321
|
-
|
|
322
|
-
|
|
323
|
+
ntfyConfig.priority,
|
|
324
|
+
ntfyConfig.token
|
|
323
325
|
);
|
|
324
326
|
results.push("✓ ntfy: sent");
|
|
325
327
|
} catch (err) {
|
package/events.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
9
9
|
import { UNIPI_EVENTS, emitEvent } from "@pi-unipi/core";
|
|
10
10
|
import type { NotifyConfig, NotifyPlatform, NotifyDispatchResult } from "./types.js";
|
|
11
|
+
import { loadNtfyConfig } from "./ntfy-config.js";
|
|
11
12
|
import { sendNativeNotification } from "./platforms/native.js";
|
|
12
13
|
import { sendGotifyNotification } from "./platforms/gotify.js";
|
|
13
14
|
import { sendTelegramNotification } from "./platforms/telegram.js";
|
|
@@ -47,7 +48,8 @@ export const BUILTIN_EVENTS: Record<
|
|
|
47
48
|
*/
|
|
48
49
|
export function registerEventListeners(
|
|
49
50
|
pi: ExtensionAPI,
|
|
50
|
-
config: NotifyConfig
|
|
51
|
+
config: NotifyConfig,
|
|
52
|
+
cwd: string
|
|
51
53
|
): void {
|
|
52
54
|
// Register built-in events (except agent_end which has custom logic)
|
|
53
55
|
for (const [eventKey, def] of Object.entries(BUILTIN_EVENTS)) {
|
|
@@ -56,10 +58,15 @@ export function registerEventListeners(
|
|
|
56
58
|
const eventConfig = config.events[eventKey];
|
|
57
59
|
if (!eventConfig?.enabled) continue;
|
|
58
60
|
|
|
59
|
-
const handler =
|
|
61
|
+
const handler = (payload: unknown) => {
|
|
60
62
|
const title = `Pi — ${def.label}`;
|
|
61
63
|
const message = buildEventMessage(eventKey, payload);
|
|
62
|
-
|
|
64
|
+
// Fire-and-forget: don't block the event emitter
|
|
65
|
+
dispatchNotification(pi, title, message, eventConfig.platforms, eventKey, config, cwd).catch(
|
|
66
|
+
() => {
|
|
67
|
+
// Silently ignore — background notification failure is non-blocking.
|
|
68
|
+
}
|
|
69
|
+
);
|
|
63
70
|
};
|
|
64
71
|
|
|
65
72
|
(pi as any).on(def.hook, handler);
|
|
@@ -68,43 +75,48 @@ export function registerEventListeners(
|
|
|
68
75
|
// agent_end — custom handler with session name and recap support
|
|
69
76
|
const agentEndConfig = config.events["agent_end"];
|
|
70
77
|
if (agentEndConfig?.enabled) {
|
|
71
|
-
const handler =
|
|
78
|
+
const handler = (payload: unknown) => {
|
|
79
|
+
// Fire-and-forget: build message and dispatch in background,
|
|
80
|
+
// don't block agent_end from completing
|
|
72
81
|
const sessionName = pi.getSessionName?.();
|
|
73
82
|
const title = `Pi — ${BUILTIN_EVENTS.agent_end.label}`;
|
|
74
|
-
let message: string;
|
|
75
83
|
|
|
76
84
|
if (config.recap.enabled) {
|
|
77
|
-
// Recap mode: summarize
|
|
85
|
+
// Recap mode: summarize asynchronously, then dispatch
|
|
78
86
|
const lastText = extractLastAssistantText(payload);
|
|
79
87
|
if (lastText && sessionCtx?.modelRegistry) {
|
|
80
88
|
const provider = extractProvider(config.recap.model);
|
|
81
89
|
const modelId = extractModelId(config.recap.model);
|
|
82
90
|
const model = sessionCtx.modelRegistry.find(provider, modelId);
|
|
83
91
|
if (model) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
message
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
92
|
+
sessionCtx.modelRegistry.getApiKeyAndHeaders(model)
|
|
93
|
+
.then((apiKeyResult) => {
|
|
94
|
+
const apiKey = apiKeyResult.ok ? (apiKeyResult as { apiKey?: string }).apiKey : undefined;
|
|
95
|
+
if (apiKey) {
|
|
96
|
+
return summarizeLastMessage(lastText, apiKey, model.baseUrl, model.api, modelId)
|
|
97
|
+
.then((recap) => sessionName ? `${sessionName}: ${recap}` : recap);
|
|
98
|
+
}
|
|
99
|
+
return buildAgentEndMessage(sessionName);
|
|
100
|
+
})
|
|
101
|
+
.catch(() => buildAgentEndMessage(sessionName))
|
|
102
|
+
.then((message) =>
|
|
103
|
+
dispatchNotification(pi, title, message, agentEndConfig.platforms, "agent_end", config, cwd)
|
|
104
|
+
)
|
|
105
|
+
.catch(() => {
|
|
106
|
+
// Silently ignore — background agent_end notification failure is non-blocking.
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
98
109
|
}
|
|
99
|
-
} else {
|
|
100
|
-
message = buildAgentEndMessage(sessionName);
|
|
101
110
|
}
|
|
102
|
-
} else {
|
|
103
|
-
// No recap: use session name based message
|
|
104
|
-
message = buildAgentEndMessage(sessionName);
|
|
105
111
|
}
|
|
106
112
|
|
|
107
|
-
|
|
113
|
+
// No recap or recap unavailable: dispatch immediately in background
|
|
114
|
+
const message = buildAgentEndMessage(sessionName);
|
|
115
|
+
dispatchNotification(pi, title, message, agentEndConfig.platforms, "agent_end", config, cwd).catch(
|
|
116
|
+
() => {
|
|
117
|
+
// Silently ignore — background agent_end notification failure is non-blocking.
|
|
118
|
+
}
|
|
119
|
+
);
|
|
108
120
|
};
|
|
109
121
|
|
|
110
122
|
(pi as any).on("agent_end", handler);
|
|
@@ -122,12 +134,12 @@ export function registerEventListeners(
|
|
|
122
134
|
}
|
|
123
135
|
|
|
124
136
|
/** Get all platforms that are currently enabled in config */
|
|
125
|
-
function getEnabledPlatforms(config: NotifyConfig): NotifyPlatform[] {
|
|
137
|
+
function getEnabledPlatforms(config: NotifyConfig, ntfyEnabled: boolean): NotifyPlatform[] {
|
|
126
138
|
const enabled: NotifyPlatform[] = [];
|
|
127
139
|
if (config.native.enabled) enabled.push("native");
|
|
128
140
|
if (config.gotify.enabled) enabled.push("gotify");
|
|
129
141
|
if (config.telegram.enabled) enabled.push("telegram");
|
|
130
|
-
if (
|
|
142
|
+
if (ntfyEnabled) enabled.push("ntfy");
|
|
131
143
|
return enabled;
|
|
132
144
|
}
|
|
133
145
|
|
|
@@ -144,34 +156,35 @@ export async function dispatchNotification(
|
|
|
144
156
|
message: string,
|
|
145
157
|
eventPlatforms: NotifyPlatform[],
|
|
146
158
|
eventType: string,
|
|
147
|
-
config: NotifyConfig
|
|
159
|
+
config: NotifyConfig,
|
|
160
|
+
cwd: string
|
|
148
161
|
): Promise<NotifyDispatchResult> {
|
|
162
|
+
// Resolve ntfy config from project/global ntfy.json
|
|
163
|
+
const ntfyConfig = loadNtfyConfig(cwd);
|
|
164
|
+
|
|
149
165
|
// Resolve platforms: event-specific → all enabled → global defaults
|
|
150
166
|
const platforms =
|
|
151
167
|
eventPlatforms.length > 0
|
|
152
168
|
? eventPlatforms
|
|
153
|
-
: getEnabledPlatforms(config).length > 0
|
|
154
|
-
? getEnabledPlatforms(config)
|
|
169
|
+
: getEnabledPlatforms(config, ntfyConfig.enabled).length > 0
|
|
170
|
+
? getEnabledPlatforms(config, ntfyConfig.enabled)
|
|
155
171
|
: config.defaultPlatforms;
|
|
156
172
|
|
|
157
173
|
const enabledPlatforms = platforms.filter((p) => {
|
|
158
174
|
if (p === "native") return config.native.enabled;
|
|
159
175
|
if (p === "gotify") return config.gotify.enabled;
|
|
160
176
|
if (p === "telegram") return config.telegram.enabled;
|
|
161
|
-
if (p === "ntfy") return
|
|
177
|
+
if (p === "ntfy") return ntfyConfig.enabled;
|
|
162
178
|
return false;
|
|
163
179
|
});
|
|
164
180
|
|
|
165
181
|
const results = await Promise.all(
|
|
166
182
|
enabledPlatforms.map(async (platform) => {
|
|
167
183
|
try {
|
|
168
|
-
await sendToPlatform(platform, title, message, config);
|
|
184
|
+
await sendToPlatform(platform, title, message, config, cwd);
|
|
169
185
|
return { platform, success: true };
|
|
170
186
|
} catch (err) {
|
|
171
|
-
|
|
172
|
-
`[notify] Failed to send via ${platform}:`,
|
|
173
|
-
err instanceof Error ? err.message : err
|
|
174
|
-
);
|
|
187
|
+
// Silently ignore — platform send failure is tracked in results.
|
|
175
188
|
return {
|
|
176
189
|
platform,
|
|
177
190
|
success: false,
|
|
@@ -199,7 +212,8 @@ async function sendToPlatform(
|
|
|
199
212
|
platform: NotifyPlatform,
|
|
200
213
|
title: string,
|
|
201
214
|
message: string,
|
|
202
|
-
config: NotifyConfig
|
|
215
|
+
config: NotifyConfig,
|
|
216
|
+
cwd: string
|
|
203
217
|
): Promise<void> {
|
|
204
218
|
switch (platform) {
|
|
205
219
|
case "native":
|
|
@@ -230,19 +244,22 @@ async function sendToPlatform(
|
|
|
230
244
|
message
|
|
231
245
|
);
|
|
232
246
|
break;
|
|
233
|
-
case "ntfy":
|
|
234
|
-
|
|
247
|
+
case "ntfy": {
|
|
248
|
+
const ntfyConfig = loadNtfyConfig(cwd);
|
|
249
|
+
if (!ntfyConfig.enabled) return;
|
|
250
|
+
if (!ntfyConfig.serverUrl || !ntfyConfig.topic) {
|
|
235
251
|
throw new Error("ntfy: serverUrl and topic are required");
|
|
236
252
|
}
|
|
237
253
|
await sendNtfyNotification(
|
|
238
|
-
|
|
239
|
-
|
|
254
|
+
ntfyConfig.serverUrl,
|
|
255
|
+
ntfyConfig.topic,
|
|
240
256
|
title,
|
|
241
257
|
message,
|
|
242
|
-
|
|
243
|
-
|
|
258
|
+
ntfyConfig.priority,
|
|
259
|
+
ntfyConfig.token
|
|
244
260
|
);
|
|
245
261
|
break;
|
|
262
|
+
}
|
|
246
263
|
}
|
|
247
264
|
}
|
|
248
265
|
|
package/index.ts
CHANGED
|
@@ -42,8 +42,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
42
42
|
// Session lifecycle — register events and announce module
|
|
43
43
|
pi.on("session_start", async (_event, ctx) => {
|
|
44
44
|
setSessionContext(ctx);
|
|
45
|
+
const cwd = process.cwd();
|
|
45
46
|
const config = loadConfig();
|
|
46
|
-
registerEventListeners(pi, config);
|
|
47
|
+
registerEventListeners(pi, config, cwd);
|
|
47
48
|
|
|
48
49
|
emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
|
|
49
50
|
name: MODULES.NOTIFY,
|
package/package.json
CHANGED
|
@@ -16,9 +16,13 @@ Help users configure the `@pi-unipi/notify` notification system.
|
|
|
16
16
|
- User wants to change which events trigger notifications
|
|
17
17
|
- User asks about notification settings
|
|
18
18
|
|
|
19
|
-
## Config
|
|
19
|
+
## Config locations
|
|
20
20
|
|
|
21
|
-
`~/.unipi/config/notify/config.json`
|
|
21
|
+
**Main config (platforms + events):** `~/.unipi/config/notify/config.json`
|
|
22
|
+
|
|
23
|
+
**ntfy config (dedicated file):**
|
|
24
|
+
- Global: `~/.unipi/config/notify/ntfy.json`
|
|
25
|
+
- Project: `<project>/.unipi/config/notify/ntfy.json`
|
|
22
26
|
|
|
23
27
|
## Config structure
|
|
24
28
|
|
|
@@ -54,7 +58,8 @@ Help users configure the `@pi-unipi/notify` notification system.
|
|
|
54
58
|
"topic": null,
|
|
55
59
|
"token": null,
|
|
56
60
|
"priority": 3
|
|
57
|
-
}
|
|
61
|
+
},
|
|
62
|
+
"NOTE": "ntfy section is legacy — migrated to ntfy.json on first run"
|
|
58
63
|
}
|
|
59
64
|
```
|
|
60
65
|
|
|
@@ -92,9 +97,41 @@ Requires:
|
|
|
92
97
|
- `priority` — 1-5 (default: 3)
|
|
93
98
|
|
|
94
99
|
**Setup options:**
|
|
95
|
-
1. **Interactive overlay:** Run `/unipi:notify-set-ntfy` for guided setup with connection test
|
|
96
|
-
2. **Manual config:** Edit `
|
|
97
|
-
3. **Agent can write config:** Read the current
|
|
100
|
+
1. **Interactive overlay:** Run `/unipi:notify-set-ntfy` for guided setup with scope selection and connection test
|
|
101
|
+
2. **Manual config:** Edit `ntfy.json` directly (see Project-Level ntfy Config below)
|
|
102
|
+
3. **Agent can write config:** Read the current ntfy.json, merge changes, write back
|
|
103
|
+
|
|
104
|
+
### Project-Level ntfy Config
|
|
105
|
+
|
|
106
|
+
ntfy uses dedicated `ntfy.json` files at both global and project scope, with full override semantics.
|
|
107
|
+
|
|
108
|
+
**File locations:**
|
|
109
|
+
- Global: `~/.unipi/config/notify/ntfy.json` (all projects)
|
|
110
|
+
- Project: `<project>/.unipi/config/notify/ntfy.json` (this project only)
|
|
111
|
+
|
|
112
|
+
**Resolution order (at dispatch time):**
|
|
113
|
+
1. Project `ntfy.json` exists → use it (full override)
|
|
114
|
+
2. No project config → use global `ntfy.json`
|
|
115
|
+
3. Neither exists → ntfy is effectively disabled
|
|
116
|
+
|
|
117
|
+
**ntfy.json shape:**
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"enabled": true,
|
|
121
|
+
"serverUrl": "https://ntfy.sh",
|
|
122
|
+
"topic": "my-project-alerts",
|
|
123
|
+
"token": null,
|
|
124
|
+
"priority": 3
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Scope selection in wizard:** When running `/unipi:notify-set-ntfy`, the wizard now asks where to save the config (Global or Project). Re-running the wizard pre-selects the current scope and pre-fills existing values.
|
|
129
|
+
|
|
130
|
+
**Settings overlay:** The ntfy line in `/unipi:notify-settings` shows topic, priority, and scope label (`[project]`, `[global]`, or "Not configured").
|
|
131
|
+
|
|
132
|
+
**Migration:** On first run, if `config.json` has ntfy settings and `ntfy.json` doesn't exist, settings are automatically migrated to `ntfy.json`. The legacy `config.json` ntfy section is left untouched for backward compatibility.
|
|
133
|
+
|
|
134
|
+
**Manual config:** Edit the appropriate `ntfy.json` file directly with the fields above.
|
|
98
135
|
|
|
99
136
|
## Commands
|
|
100
137
|
|
package/tools.ts
CHANGED
|
@@ -9,7 +9,6 @@ import { Type } from "@sinclair/typebox";
|
|
|
9
9
|
import { NOTIFY_TOOLS } from "@pi-unipi/core";
|
|
10
10
|
import { loadConfig } from "./settings.js";
|
|
11
11
|
import { dispatchNotification } from "./events.js";
|
|
12
|
-
import type { NotifyDispatchResult } from "./types.js";
|
|
13
12
|
|
|
14
13
|
/** Schema for notify_user tool parameters */
|
|
15
14
|
const NotifyUserSchema = Type.Object({
|
|
@@ -43,7 +42,7 @@ export function registerNotifyTools(pi: ExtensionAPI): void {
|
|
|
43
42
|
"Send a notification to the user's configured platforms (native OS, Gotify, Telegram, ntfy). " +
|
|
44
43
|
"Use for critical errors, completion of long-running tasks, or when the user explicitly asked to be notified.",
|
|
45
44
|
parameters: NotifyUserSchema,
|
|
46
|
-
async execute(_toolCallId, params, _signal, _onUpdate,
|
|
45
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx: ExtensionContext) {
|
|
47
46
|
const {
|
|
48
47
|
message,
|
|
49
48
|
title,
|
|
@@ -64,32 +63,28 @@ export function registerNotifyTools(pi: ExtensionAPI): void {
|
|
|
64
63
|
// Resolve platforms — use params.platforms or global defaults
|
|
65
64
|
const notifPlatforms = platforms || config.defaultPlatforms;
|
|
66
65
|
|
|
67
|
-
//
|
|
68
|
-
const
|
|
66
|
+
// Fire-and-forget: dispatch in background so the tool doesn't block the agent
|
|
67
|
+
const cwd = process.cwd();
|
|
68
|
+
dispatchNotification(
|
|
69
69
|
pi,
|
|
70
70
|
notifTitle,
|
|
71
71
|
message,
|
|
72
72
|
notifPlatforms,
|
|
73
73
|
"agent_tool",
|
|
74
|
-
config
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
(r) => `${r.platform}: ${r.success ? "✓ sent" : `✗ ${r.error || "failed"}`}`
|
|
80
|
-
);
|
|
74
|
+
config,
|
|
75
|
+
cwd
|
|
76
|
+
).catch(() => {
|
|
77
|
+
// Silently ignore — background dispatch failure is non-blocking.
|
|
78
|
+
});
|
|
81
79
|
|
|
82
80
|
return {
|
|
83
81
|
content: [
|
|
84
82
|
{
|
|
85
83
|
type: "text" as const,
|
|
86
|
-
text: `Notification
|
|
84
|
+
text: `Notification sending to ${notifPlatforms.length} platform(s): ${notifPlatforms.join(", ")}`,
|
|
87
85
|
},
|
|
88
86
|
],
|
|
89
|
-
details: {
|
|
90
|
-
platforms: result.results.map((r) => r.platform),
|
|
91
|
-
allSuccess: result.allSuccess,
|
|
92
|
-
},
|
|
87
|
+
details: { platforms: notifPlatforms },
|
|
93
88
|
};
|
|
94
89
|
},
|
|
95
90
|
});
|
package/tui/ntfy-setup.ts
CHANGED
|
@@ -10,10 +10,11 @@ import type { Component } from "@mariozechner/pi-tui";
|
|
|
10
10
|
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
|
11
11
|
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
12
12
|
import { sendNtfyNotification } from "../platforms/ntfy.js";
|
|
13
|
-
import {
|
|
13
|
+
import { loadNtfyConfig, saveNtfyConfig, getNtfyConfigScope } from "../ntfy-config.js";
|
|
14
14
|
|
|
15
15
|
type SetupPhase =
|
|
16
16
|
| "instructions"
|
|
17
|
+
| "scope"
|
|
17
18
|
| "server-url"
|
|
18
19
|
| "topic"
|
|
19
20
|
| "token"
|
|
@@ -28,6 +29,8 @@ type SetupPhase =
|
|
|
28
29
|
*/
|
|
29
30
|
export class NtfySetupOverlay implements Component {
|
|
30
31
|
private phase: SetupPhase = "instructions";
|
|
32
|
+
private scope: "global" | "project" = "global";
|
|
33
|
+
private scopeIndex = 0; // 0 = global, 1 = project
|
|
31
34
|
private serverUrl = "";
|
|
32
35
|
private topic = "";
|
|
33
36
|
private token = "";
|
|
@@ -41,12 +44,18 @@ export class NtfySetupOverlay implements Component {
|
|
|
41
44
|
private theme: Theme | null = null;
|
|
42
45
|
|
|
43
46
|
constructor() {
|
|
44
|
-
//
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
// Determine current scope and pre-fill from resolved config
|
|
48
|
+
const cwd = process.cwd();
|
|
49
|
+
const existingScope = getNtfyConfigScope(cwd);
|
|
50
|
+
if (existingScope !== "none") {
|
|
51
|
+
this.scope = existingScope;
|
|
52
|
+
this.scopeIndex = existingScope === "project" ? 1 : 0;
|
|
53
|
+
}
|
|
54
|
+
const config = loadNtfyConfig(cwd);
|
|
55
|
+
if (config.serverUrl) this.serverUrl = config.serverUrl;
|
|
56
|
+
if (config.topic) this.topic = config.topic;
|
|
57
|
+
if (config.token) this.token = config.token;
|
|
58
|
+
if (config.priority) this.priority = String(config.priority);
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
setTheme(theme: Theme): void {
|
|
@@ -59,6 +68,21 @@ export class NtfySetupOverlay implements Component {
|
|
|
59
68
|
switch (this.phase) {
|
|
60
69
|
case "instructions":
|
|
61
70
|
if (data === "\r" || data === " ") {
|
|
71
|
+
this.phase = "scope";
|
|
72
|
+
} else if (data === "\x1b") {
|
|
73
|
+
this.onClose?.();
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
|
|
77
|
+
case "scope":
|
|
78
|
+
if (data === "\x1b[A" || data === "k") {
|
|
79
|
+
// Up
|
|
80
|
+
this.scopeIndex = Math.max(0, this.scopeIndex - 1);
|
|
81
|
+
} else if (data === "\x1b[B" || data === "j") {
|
|
82
|
+
// Down
|
|
83
|
+
this.scopeIndex = Math.min(1, this.scopeIndex + 1);
|
|
84
|
+
} else if (data === "\r" || data === " ") {
|
|
85
|
+
this.scope = this.scopeIndex === 1 ? "project" : "global";
|
|
62
86
|
this.phase = this.serverUrl ? "topic" : "server-url";
|
|
63
87
|
} else if (data === "\x1b") {
|
|
64
88
|
this.onClose?.();
|
|
@@ -206,14 +230,13 @@ export class NtfySetupOverlay implements Component {
|
|
|
206
230
|
}
|
|
207
231
|
|
|
208
232
|
private saveConfig(): void {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
},
|
|
233
|
+
const cwd = process.cwd();
|
|
234
|
+
saveNtfyConfig(this.scope, cwd, {
|
|
235
|
+
enabled: true,
|
|
236
|
+
serverUrl: this.serverUrl.replace(/\/$/, ""),
|
|
237
|
+
topic: this.topic,
|
|
238
|
+
token: this.token || undefined,
|
|
239
|
+
priority: parseInt(this.priority, 10) || 3,
|
|
217
240
|
});
|
|
218
241
|
}
|
|
219
242
|
|
|
@@ -336,6 +359,36 @@ export class NtfySetupOverlay implements Component {
|
|
|
336
359
|
);
|
|
337
360
|
break;
|
|
338
361
|
|
|
362
|
+
case "scope": {
|
|
363
|
+
lines.push(
|
|
364
|
+
this.frameLine(
|
|
365
|
+
this.fg("dim", "Where should this config be saved?"),
|
|
366
|
+
innerWidth
|
|
367
|
+
)
|
|
368
|
+
);
|
|
369
|
+
lines.push(this.frameLine("", innerWidth));
|
|
370
|
+
const options = ["Global (all projects)", "Project (this project only)"];
|
|
371
|
+
for (let i = 0; i < options.length; i++) {
|
|
372
|
+
const isSelected = i === this.scopeIndex;
|
|
373
|
+
const label = isSelected ? this.bold(options[i]) : this.fg("dim", options[i]);
|
|
374
|
+
lines.push(
|
|
375
|
+
this.frameLine(
|
|
376
|
+
` ${isSelected ? this.fg("accent", "▸") : " "} ${label}`,
|
|
377
|
+
innerWidth
|
|
378
|
+
)
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
lines.push(this.frameLine("", innerWidth));
|
|
382
|
+
lines.push(this.ruleLine(innerWidth));
|
|
383
|
+
lines.push(
|
|
384
|
+
this.frameLine(
|
|
385
|
+
this.fg("dim", "↑↓ select · Enter confirm · Esc cancel"),
|
|
386
|
+
innerWidth
|
|
387
|
+
)
|
|
388
|
+
);
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
|
|
339
392
|
case "server-url":
|
|
340
393
|
lines.push(
|
|
341
394
|
this.frameLine(
|
package/tui/settings-overlay.ts
CHANGED
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
saveConfig,
|
|
14
14
|
validateConfig,
|
|
15
15
|
} from "../settings.js";
|
|
16
|
-
import
|
|
16
|
+
import { loadNtfyConfig, saveNtfyConfig, getNtfyConfigScope } from "../ntfy-config.js";
|
|
17
|
+
import type { NotifyConfig, NtfyConfig } from "../types.js";
|
|
17
18
|
|
|
18
19
|
/** Section types */
|
|
19
20
|
type Section = "platforms" | "events" | "recap";
|
|
@@ -23,6 +24,8 @@ type Section = "platforms" | "events" | "recap";
|
|
|
23
24
|
*/
|
|
24
25
|
export class NotifySettingsOverlay implements Component {
|
|
25
26
|
private config: NotifyConfig;
|
|
27
|
+
private ntfyConfig: NtfyConfig;
|
|
28
|
+
private ntfyScope: "project" | "global" | "none";
|
|
26
29
|
private section: Section = "platforms";
|
|
27
30
|
private selectedIndex = 0;
|
|
28
31
|
private error: string | null = null;
|
|
@@ -35,6 +38,9 @@ export class NotifySettingsOverlay implements Component {
|
|
|
35
38
|
|
|
36
39
|
constructor() {
|
|
37
40
|
this.config = loadConfig();
|
|
41
|
+
const cwd = process.cwd();
|
|
42
|
+
this.ntfyConfig = loadNtfyConfig(cwd);
|
|
43
|
+
this.ntfyScope = getNtfyConfigScope(cwd);
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
setTheme(theme: Theme): void {
|
|
@@ -93,7 +99,10 @@ export class NotifySettingsOverlay implements Component {
|
|
|
93
99
|
"ntfy",
|
|
94
100
|
];
|
|
95
101
|
const key = platforms[this.selectedIndex];
|
|
96
|
-
if (key) {
|
|
102
|
+
if (key === "ntfy") {
|
|
103
|
+
// ntfy toggle updates the resolved ntfy config
|
|
104
|
+
this.ntfyConfig.enabled = !this.ntfyConfig.enabled;
|
|
105
|
+
} else if (key) {
|
|
97
106
|
this.config[key].enabled = !this.config[key].enabled;
|
|
98
107
|
}
|
|
99
108
|
} else if (this.section === "recap") {
|
|
@@ -115,6 +124,10 @@ export class NotifySettingsOverlay implements Component {
|
|
|
115
124
|
}
|
|
116
125
|
this.error = null;
|
|
117
126
|
saveConfig(this.config);
|
|
127
|
+
// Save ntfy config to its own file if scope is known
|
|
128
|
+
if (this.ntfyScope !== "none") {
|
|
129
|
+
saveNtfyConfig(this.ntfyScope, process.cwd(), this.ntfyConfig);
|
|
130
|
+
}
|
|
118
131
|
this.saved = true;
|
|
119
132
|
setTimeout(() => this.onClose?.(), 500);
|
|
120
133
|
}
|
|
@@ -237,9 +250,9 @@ export class NotifySettingsOverlay implements Component {
|
|
|
237
250
|
{
|
|
238
251
|
key: "ntfy",
|
|
239
252
|
label: "ntfy",
|
|
240
|
-
detail: this.
|
|
241
|
-
? `
|
|
242
|
-
: "
|
|
253
|
+
detail: this.ntfyScope !== "none"
|
|
254
|
+
? `Topic: ${this.ntfyConfig.topic ?? "—"} · P${this.ntfyConfig.priority} · [${this.ntfyScope}]`
|
|
255
|
+
: "Not configured",
|
|
243
256
|
},
|
|
244
257
|
];
|
|
245
258
|
|
|
@@ -248,7 +261,9 @@ export class NotifySettingsOverlay implements Component {
|
|
|
248
261
|
const isSelected = i === this.selectedIndex;
|
|
249
262
|
const toggleOn = this.fg("success", "●");
|
|
250
263
|
const toggleOff = this.fg("dim", "○");
|
|
251
|
-
|
|
264
|
+
// ntfy enabled state comes from resolved ntfy.json, not config.json
|
|
265
|
+
const isEnabled = p.key === "ntfy" ? this.ntfyConfig.enabled : this.config[p.key].enabled;
|
|
266
|
+
const toggle = isEnabled ? toggleOn : toggleOff;
|
|
252
267
|
const label = isSelected ? this.bold(p.label) : this.fg("dim", p.label);
|
|
253
268
|
|
|
254
269
|
lines.push(
|