mixdog 0.9.66 → 0.9.67
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/package.json +1 -1
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +10 -0
- package/src/runtime/channels/lib/config.mjs +7 -0
- package/src/runtime/channels/lib/status-snapshot.mjs +6 -30
- package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +203 -0
- package/src/runtime/channels/lib/webhook.mjs +23 -201
- package/src/runtime/shared/config.mjs +0 -9
- package/src/runtime/shared/time-format.mjs +56 -0
- package/src/runtime/shared/tool-card-model.mjs +740 -0
- package/src/session-runtime/channel-config-api.mjs +5 -0
- package/src/session-runtime/lifecycle-api.mjs +5 -0
- package/src/session-runtime/prewarm.mjs +13 -7
- package/src/session-runtime/runtime-core.mjs +28 -9
- package/src/session-runtime/session-text.mjs +6 -0
- package/src/session-runtime/settings-api.mjs +0 -12
- package/src/standalone/channel-admin.mjs +35 -21
- package/src/tui/App.jsx +0 -15
- package/src/tui/app/channel-pickers.mjs +18 -42
- package/src/tui/app/doctor.mjs +0 -1
- package/src/tui/components/ToolExecution.jsx +47 -196
- package/src/tui/components/tool-execution/surface-detail.mjs +33 -394
- package/src/tui/components/tool-execution/text-format.mjs +27 -104
- package/src/tui/dist/index.mjs +306 -185
- package/src/tui/engine/live-share.mjs +9 -0
- package/src/tui/engine/session-api-ext.mjs +0 -10
- package/src/tui/time-format.mjs +4 -51
- package/src/runtime/channels/lib/webhook/ngrok.mjs +0 -181
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
channelSetup,
|
|
3
3
|
deleteSchedule,
|
|
4
4
|
deleteWebhook,
|
|
5
|
+
getWebhookSecret,
|
|
5
6
|
setChannelAsync,
|
|
6
7
|
saveSchedule,
|
|
7
8
|
saveWebhook,
|
|
@@ -68,6 +69,10 @@ export function createChannelConfigApi({ flushBackendSave, channels, reloadChann
|
|
|
68
69
|
reloadChannelsSoon();
|
|
69
70
|
return result;
|
|
70
71
|
},
|
|
72
|
+
// Read-only secret fetch for the webhook editor; no worker reload.
|
|
73
|
+
async getWebhookSecret(name) {
|
|
74
|
+
return getWebhookSecret(name);
|
|
75
|
+
},
|
|
71
76
|
async runScheduleNow(name) {
|
|
72
77
|
const id = String(name || '').trim();
|
|
73
78
|
const schedule = await getSchedule(id);
|
|
@@ -89,6 +89,11 @@ export function createLifecycleApi(deps) {
|
|
|
89
89
|
return {
|
|
90
90
|
id: s.id,
|
|
91
91
|
updatedAt: s.updatedAt,
|
|
92
|
+
// Conversation-activity timestamp for Recent ordering. Without this the
|
|
93
|
+
// desktop falls back to updatedAt, which detach/resume bookkeeping
|
|
94
|
+
// bumps — clicking a session reshuffled the sidebar (the row just left
|
|
95
|
+
// jumped to the top).
|
|
96
|
+
lastUsedAt: Number(s.lastUsedAt) || 0,
|
|
92
97
|
cwd: s.cwd || '',
|
|
93
98
|
model: s.model,
|
|
94
99
|
provider: s.provider,
|
|
@@ -15,6 +15,7 @@ export function createPrewarmSchedulers({
|
|
|
15
15
|
getSession,
|
|
16
16
|
isRemoteEnabled,
|
|
17
17
|
channelsEnabled,
|
|
18
|
+
hasActiveAutomation,
|
|
18
19
|
getCodeGraphModule,
|
|
19
20
|
createCurrentSession,
|
|
20
21
|
channels,
|
|
@@ -111,25 +112,30 @@ export function createPrewarmSchedulers({
|
|
|
111
112
|
bootProfile('channels:start-skipped');
|
|
112
113
|
return;
|
|
113
114
|
}
|
|
114
|
-
if (!channelsEnabled()) {
|
|
115
|
-
bootProfile('channels:start-disabled');
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
115
|
if (timers.channelStartTimer || state.channelStartPromise || isCloseRequested()) return;
|
|
119
116
|
bootProfile('channels:start-scheduled', { delayMs });
|
|
120
|
-
timers.channelStartTimer = setTimeout(() => {
|
|
117
|
+
timers.channelStartTimer = setTimeout(() => void (async () => {
|
|
121
118
|
timers.channelStartTimer = null;
|
|
122
119
|
if (isCloseRequested()) return;
|
|
120
|
+
// Channels-module and remote toggles gate MESSAGING; automation
|
|
121
|
+
// (enabled schedules/webhooks) keeps the worker boot alive — its
|
|
122
|
+
// backend runs headless when messaging is off or unconfigured.
|
|
123
|
+
const automation = await hasActiveAutomation().catch(() => false);
|
|
124
|
+
if (!channelsEnabled() && !automation) {
|
|
125
|
+
bootProfile('channels:start-disabled');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
123
128
|
// A deferred start may straddle a stopRemote(); re-check before booting so
|
|
124
129
|
// a turned-off session neither starts channels nor keeps rescheduling.
|
|
125
|
-
if (!isRemoteEnabled()) return;
|
|
130
|
+
if (!isRemoteEnabled() && !automation) return;
|
|
131
|
+
if (isCloseRequested()) return;
|
|
126
132
|
if (getActiveTurnCount() > 0 || getSessionCreatePromise()) {
|
|
127
133
|
bootProfile('channels:start-deferred', { reason: getActiveTurnCount() > 0 ? 'turn-active' : 'session-create' });
|
|
128
134
|
scheduleChannelStart(backgroundBusyRetryMs);
|
|
129
135
|
return;
|
|
130
136
|
}
|
|
131
137
|
void invokeChannelStart();
|
|
132
|
-
}, delayMs);
|
|
138
|
+
})(), delayMs);
|
|
133
139
|
timers.channelStartTimer.unref?.();
|
|
134
140
|
}
|
|
135
141
|
|
|
@@ -65,13 +65,12 @@ import {
|
|
|
65
65
|
deleteWebhook,
|
|
66
66
|
forgetDiscordToken,
|
|
67
67
|
forgetTelegramToken,
|
|
68
|
-
|
|
68
|
+
hasActiveAutomation,
|
|
69
69
|
setChannel,
|
|
70
70
|
saveDiscordToken,
|
|
71
71
|
saveTelegramToken,
|
|
72
72
|
saveSchedule,
|
|
73
73
|
saveWebhook,
|
|
74
|
-
saveWebhookAuthtoken,
|
|
75
74
|
setBackend,
|
|
76
75
|
setBackendAsync,
|
|
77
76
|
setScheduleEnabled,
|
|
@@ -1694,6 +1693,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1694
1693
|
getSession: () => session,
|
|
1695
1694
|
isRemoteEnabled: () => remoteEnabled,
|
|
1696
1695
|
channelsEnabled,
|
|
1696
|
+
hasActiveAutomation,
|
|
1697
1697
|
getCodeGraphModule,
|
|
1698
1698
|
createCurrentSession,
|
|
1699
1699
|
channels,
|
|
@@ -1884,10 +1884,6 @@ export async function createMixdogSessionRuntime({
|
|
|
1884
1884
|
bootProfile('channels:start-skipped');
|
|
1885
1885
|
return true;
|
|
1886
1886
|
}
|
|
1887
|
-
if (!channelsEnabled()) {
|
|
1888
|
-
bootProfile('channels:start-disabled');
|
|
1889
|
-
return true;
|
|
1890
|
-
}
|
|
1891
1887
|
if (closeRequested) return true;
|
|
1892
1888
|
if (prewarmTimers.channelStartTimer) {
|
|
1893
1889
|
clearTimeout(prewarmTimers.channelStartTimer);
|
|
@@ -1895,6 +1891,14 @@ export async function createMixdogSessionRuntime({
|
|
|
1895
1891
|
}
|
|
1896
1892
|
bootProfile('channels:start-scheduled', { delayMs: 0, immediate: true });
|
|
1897
1893
|
void (async () => {
|
|
1894
|
+
// Channels-module toggle gates MESSAGING only: automation (schedules/
|
|
1895
|
+
// webhooks) still boots the worker — its backend runs headless when
|
|
1896
|
+
// messaging is off or unconfigured (see channels/lib/config.mjs).
|
|
1897
|
+
if (!channelsEnabled() && !(await hasActiveAutomation().catch(() => false))) {
|
|
1898
|
+
bootProfile('channels:start-disabled');
|
|
1899
|
+
remoteClaimPending = false;
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1898
1902
|
// A backend switch may still be pending or writing asynchronously. Drain
|
|
1899
1903
|
// it before the worker reads config; never race it with a sync lock wait.
|
|
1900
1904
|
try { await flushBackendSave(); } catch {}
|
|
@@ -2035,9 +2039,9 @@ export async function createMixdogSessionRuntime({
|
|
|
2035
2039
|
// early /remote (startRemote clears it), stopRemote(), and close() all
|
|
2036
2040
|
// cancel it through the existing clearTimeout paths. Runtime /remote calls
|
|
2037
2041
|
// still start immediately (user-initiated, UI already painted).
|
|
2042
|
+
const remoteAutoStartDelayMs = envDelayMs('MIXDOG_REMOTE_AUTOSTART_DELAY_MS', 1_500, { min: 0, max: 60_000 });
|
|
2038
2043
|
if (remoteEnabled || remoteAutoStartRequested) {
|
|
2039
2044
|
const remoteStartIntent = remoteEnabled ? 'explicit' : 'auto';
|
|
2040
|
-
const remoteAutoStartDelayMs = envDelayMs('MIXDOG_REMOTE_AUTOSTART_DELAY_MS', 1_500, { min: 0, max: 60_000 });
|
|
2041
2045
|
bootProfile('channels:autostart-deferred', { delayMs: remoteAutoStartDelayMs, intent: remoteStartIntent });
|
|
2042
2046
|
prewarmTimers.channelStartTimer = setTimeout(() => {
|
|
2043
2047
|
prewarmTimers.channelStartTimer = null;
|
|
@@ -2048,6 +2052,23 @@ export async function createMixdogSessionRuntime({
|
|
|
2048
2052
|
startRemote({ intent: remoteStartIntent });
|
|
2049
2053
|
}, remoteAutoStartDelayMs);
|
|
2050
2054
|
prewarmTimers.channelStartTimer.unref?.();
|
|
2055
|
+
} else {
|
|
2056
|
+
// Automation decoupling (user decision): enabled schedules/webhooks boot
|
|
2057
|
+
// the worker on their own — no remote flag, no remote.autoStart, no
|
|
2058
|
+
// messaging backend required. Claim-if-vacant auto intent reuses the
|
|
2059
|
+
// exact autoStart semantics (a live owner elsewhere wins silently).
|
|
2060
|
+
prewarmTimers.channelStartTimer = setTimeout(() => {
|
|
2061
|
+
prewarmTimers.channelStartTimer = null;
|
|
2062
|
+
if (closeRequested || remoteEnabled) return;
|
|
2063
|
+
void hasActiveAutomation()
|
|
2064
|
+
.then((active) => {
|
|
2065
|
+
if (!active || closeRequested || remoteEnabled) return;
|
|
2066
|
+
bootProfile('channels:automation-autostart');
|
|
2067
|
+
startRemote({ intent: 'auto' });
|
|
2068
|
+
})
|
|
2069
|
+
.catch(() => { /* automation probe is best-effort */ });
|
|
2070
|
+
}, remoteAutoStartDelayMs);
|
|
2071
|
+
prewarmTimers.channelStartTimer.unref?.();
|
|
2051
2072
|
}
|
|
2052
2073
|
|
|
2053
2074
|
// Pure settings-delegate methods (onboarding status/skip, autoClear, profile,
|
|
@@ -2103,8 +2124,6 @@ export async function createMixdogSessionRuntime({
|
|
|
2103
2124
|
forgetDiscordToken,
|
|
2104
2125
|
saveTelegramToken,
|
|
2105
2126
|
forgetTelegramToken,
|
|
2106
|
-
saveWebhookAuthtoken,
|
|
2107
|
-
forgetWebhookAuthtoken,
|
|
2108
2127
|
setBackend,
|
|
2109
2128
|
});
|
|
2110
2129
|
|
|
@@ -39,6 +39,12 @@ const SYNTHETIC_SESSION_TEXT_PATTERNS = Object.freeze([
|
|
|
39
39
|
/^\[mixdog-runtime\]/i,
|
|
40
40
|
/^\[(?:truncated|request interrupted by user)\]$/i,
|
|
41
41
|
/^a previous model worked on this task and produced the compacted handoff summary below\b/i,
|
|
42
|
+
// Compact/auto-clear re-seed handoff variants: these lead the FIRST user
|
|
43
|
+
// message of every post-compaction session, so titling from them painted
|
|
44
|
+
// "Re-attached after compaction…" rows in Recent (user report). Skipping
|
|
45
|
+
// them titles the session from its first REAL user message instead.
|
|
46
|
+
/^re-attached after compaction\b/i,
|
|
47
|
+
/^reference files:\s/i,
|
|
42
48
|
/^the async (?:agent|shell) task\b/i,
|
|
43
49
|
]);
|
|
44
50
|
|
|
@@ -56,8 +56,6 @@ export function createSettingsApi({
|
|
|
56
56
|
forgetDiscordToken,
|
|
57
57
|
saveTelegramToken,
|
|
58
58
|
forgetTelegramToken,
|
|
59
|
-
saveWebhookAuthtoken,
|
|
60
|
-
forgetWebhookAuthtoken,
|
|
61
59
|
setBackend,
|
|
62
60
|
}) {
|
|
63
61
|
return {
|
|
@@ -378,15 +376,5 @@ export function createSettingsApi({
|
|
|
378
376
|
scheduleBackendSave(value);
|
|
379
377
|
return { ok: true, backend: value };
|
|
380
378
|
},
|
|
381
|
-
saveWebhookAuthtoken(token) {
|
|
382
|
-
const result = saveWebhookAuthtoken(token);
|
|
383
|
-
reloadChannelsSoon();
|
|
384
|
-
return result;
|
|
385
|
-
},
|
|
386
|
-
forgetWebhookAuthtoken() {
|
|
387
|
-
const result = forgetWebhookAuthtoken();
|
|
388
|
-
reloadChannelsSoon();
|
|
389
|
-
return result;
|
|
390
|
-
},
|
|
391
379
|
};
|
|
392
380
|
}
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
diagnoseDiscordTokenValue,
|
|
16
16
|
getDiscordToken,
|
|
17
17
|
getTelegramToken,
|
|
18
|
-
getWebhookAuthtoken,
|
|
19
18
|
hasStoredSecret,
|
|
20
19
|
readSection,
|
|
21
20
|
saveSecret,
|
|
@@ -34,10 +33,12 @@ import {
|
|
|
34
33
|
import {
|
|
35
34
|
listEndpoints as dbListEndpoints,
|
|
36
35
|
loadEndpointConfig as dbLoadEndpoint,
|
|
36
|
+
readEndpointSecret as dbReadEndpointSecret,
|
|
37
37
|
upsertEndpoint as dbUpsertEndpoint,
|
|
38
38
|
deleteEndpoint as dbDeleteEndpoint,
|
|
39
39
|
setEndpointEnabled as dbSetEndpointEnabled,
|
|
40
40
|
} from '../runtime/shared/webhooks-db.mjs';
|
|
41
|
+
import { readHookPublicBase } from '../runtime/channels/lib/webhook/relay-tunnel.mjs';
|
|
41
42
|
|
|
42
43
|
const NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
43
44
|
const DEFAULT_CHANNELS = Object.freeze({
|
|
@@ -197,18 +198,6 @@ export function forgetTelegramToken() {
|
|
|
197
198
|
return { ok: true };
|
|
198
199
|
}
|
|
199
200
|
|
|
200
|
-
export function saveWebhookAuthtoken(token) {
|
|
201
|
-
const value = String(token || '').trim();
|
|
202
|
-
if (!value) throw new Error('Webhook/ngrok authtoken is required');
|
|
203
|
-
saveSecret(SECRET_ACCOUNTS.webhookAuth, value);
|
|
204
|
-
return { ok: true, configured: Boolean(getWebhookAuthtoken()) };
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export function forgetWebhookAuthtoken() {
|
|
208
|
-
deleteSecret(SECRET_ACCOUNTS.webhookAuth);
|
|
209
|
-
return { ok: true };
|
|
210
|
-
}
|
|
211
|
-
|
|
212
201
|
// Single-channel write: persists `cfg.channel`, keeping the per-backend id
|
|
213
202
|
// fields (discordChannelId/telegramChatId) so a backend switch retains both
|
|
214
203
|
// ids. `backend` selects which per-backend field the id updates; when omitted
|
|
@@ -254,7 +243,6 @@ export function setWebhookConfig(patch = {}) {
|
|
|
254
243
|
...(Object.prototype.hasOwnProperty.call(patch, 'enabled') ? { enabled: patch.enabled === true } : {}),
|
|
255
244
|
...(patch.port ? { port: Number(patch.port) || 3333 } : {}),
|
|
256
245
|
...(patch.domain ? { domain: String(patch.domain).trim() } : {}),
|
|
257
|
-
...(patch.ngrokDomain ? { ngrokDomain: String(patch.ngrokDomain).trim() } : {}),
|
|
258
246
|
},
|
|
259
247
|
}));
|
|
260
248
|
}
|
|
@@ -267,7 +255,6 @@ export async function setWebhookConfigAsync(patch = {}) {
|
|
|
267
255
|
...(Object.prototype.hasOwnProperty.call(patch, 'enabled') ? { enabled: patch.enabled === true } : {}),
|
|
268
256
|
...(patch.port ? { port: Number(patch.port) || 3333 } : {}),
|
|
269
257
|
...(patch.domain ? { domain: String(patch.domain).trim() } : {}),
|
|
270
|
-
...(patch.ngrokDomain ? { ngrokDomain: String(patch.ngrokDomain).trim() } : {}),
|
|
271
258
|
},
|
|
272
259
|
}));
|
|
273
260
|
}
|
|
@@ -486,7 +473,13 @@ export async function saveWebhook({
|
|
|
486
473
|
if (overwrite !== true && (await dbLoadEndpoint(id))) {
|
|
487
474
|
throw new Error(`webhook "${id}" already exists`);
|
|
488
475
|
}
|
|
489
|
-
|
|
476
|
+
// Secret semantics: an explicit value always wins; an EMPTY value on an
|
|
477
|
+
// overwrite PRESERVES the stored secret (editing instructions must not
|
|
478
|
+
// silently rotate the key the external service was configured with); only
|
|
479
|
+
// a brand-new endpoint mints a random secret.
|
|
480
|
+
const secretValue = String(secret || '').trim()
|
|
481
|
+
|| (overwrite === true ? String((await dbReadEndpointSecret(id)) || '').trim() : '')
|
|
482
|
+
|| randomBytes(24).toString('hex');
|
|
490
483
|
const saved = await dbUpsertEndpoint({
|
|
491
484
|
name: id,
|
|
492
485
|
description: String(description || '').trim(),
|
|
@@ -522,11 +515,32 @@ export async function setWebhookEnabled(name, enabled) {
|
|
|
522
515
|
return { name: id, enabled: enabled !== false };
|
|
523
516
|
}
|
|
524
517
|
|
|
518
|
+
// Explicit single-purpose secret read for the editor's copy affordance (the
|
|
519
|
+
// list path only ever exposes a presence flag). Local surfaces only — the
|
|
520
|
+
// desktop blocks this capability over the remote bridge.
|
|
521
|
+
export async function getWebhookSecret(name) {
|
|
522
|
+
const id = assertName(name, 'webhook name');
|
|
523
|
+
return { name: id, secret: (await dbReadEndpointSecret(id)) || '' };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Automation presence: any enabled schedule or webhook endpoint. Drives the
|
|
527
|
+
// worker boot decision independently of the messaging channels — schedules
|
|
528
|
+
// and webhooks run sessions, so they must not require Discord/Telegram
|
|
529
|
+
// tokens or an explicit remote toggle.
|
|
530
|
+
export async function hasActiveAutomation() {
|
|
531
|
+
try {
|
|
532
|
+
const [schedules, webhooks] = await Promise.all([listSchedules(), listWebhooks()]);
|
|
533
|
+
return schedules.some((entry) => entry?.enabled !== false && entry?.status !== 'done')
|
|
534
|
+
|| webhooks.some((entry) => entry?.enabled !== false);
|
|
535
|
+
} catch {
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
525
540
|
export async function channelSetup(config = null) {
|
|
526
541
|
const cfg = normalizeChannelsConfig(config || readSection('channels'));
|
|
527
542
|
const discordToken = getDiscordToken();
|
|
528
543
|
const discordProblem = diagnoseDiscordTokenValue(discordToken, cfg);
|
|
529
|
-
const webhookAuth = getWebhookAuthtoken();
|
|
530
544
|
const telegramToken = getTelegramToken();
|
|
531
545
|
return {
|
|
532
546
|
backend: cfg.backend || 'discord',
|
|
@@ -546,9 +560,9 @@ export async function channelSetup(config = null) {
|
|
|
546
560
|
},
|
|
547
561
|
webhook: {
|
|
548
562
|
...(cfg.webhook || {}),
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
563
|
+
// Relay-tunnel public base (null until the channel worker first
|
|
564
|
+
// connects and mints its hook identity).
|
|
565
|
+
publicUrl: readHookPublicBase(),
|
|
552
566
|
},
|
|
553
567
|
channel: getChannel(cfg),
|
|
554
568
|
schedules: await listSchedules(),
|
|
@@ -560,7 +574,7 @@ async function renderChannelStatus(config = null) {
|
|
|
560
574
|
const setup = await channelSetup(config);
|
|
561
575
|
const lines = [];
|
|
562
576
|
lines.push(`discord ${setup.discord.status}${setup.discord.problem ? ` (${setup.discord.problem})` : ''}`);
|
|
563
|
-
lines.push(`webhook ${setup.webhook.enabled === false ? 'disabled' : 'enabled'} ·
|
|
577
|
+
lines.push(`webhook ${setup.webhook.enabled === false ? 'disabled' : 'enabled'} · port ${setup.webhook.port || 3333}${setup.webhook.publicUrl ? ` · ${setup.webhook.publicUrl}` : ''}`);
|
|
564
578
|
lines.push(`channel ${setup.channel.channelId || '(unset)'}`);
|
|
565
579
|
lines.push('schedules');
|
|
566
580
|
if (setup.schedules.length === 0) lines.push(' (none)');
|
package/src/tui/App.jsx
CHANGED
|
@@ -1881,21 +1881,6 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
1881
1881
|
resumeAfterChannelPrompt(channelPrompt);
|
|
1882
1882
|
return true;
|
|
1883
1883
|
}
|
|
1884
|
-
if (channelPrompt.kind === 'webhook-token') {
|
|
1885
|
-
if (!commandText) return false;
|
|
1886
|
-
store.saveWebhookAuthtoken(commandText);
|
|
1887
|
-
resumeAfterChannelPrompt(channelPrompt);
|
|
1888
|
-
return true;
|
|
1889
|
-
}
|
|
1890
|
-
if (channelPrompt.kind === 'webhook-domain') {
|
|
1891
|
-
if (!commandText) return false;
|
|
1892
|
-
Promise.resolve(store.setWebhookConfig?.({ ngrokDomain: commandText }))
|
|
1893
|
-
.then(() => resumeAfterChannelPrompt(channelPrompt))
|
|
1894
|
-
.catch((e) => {
|
|
1895
|
-
store.pushNotice(`webhook config update failed: ${e?.message || e}`, 'error');
|
|
1896
|
-
});
|
|
1897
|
-
return true;
|
|
1898
|
-
}
|
|
1899
1884
|
const parts = commandText.split('|').map((part) => part.trim());
|
|
1900
1885
|
if (channelPrompt.kind === 'channel-add') {
|
|
1901
1886
|
// Single-channel: the UI asks only for the channel id. Legacy
|
|
@@ -271,46 +271,30 @@ export function createChannelPickers({
|
|
|
271
271
|
const returnTo = typeof options.returnTo === 'function'
|
|
272
272
|
? options.returnTo
|
|
273
273
|
: () => setPicker(null);
|
|
274
|
-
const
|
|
274
|
+
const publicUrl = setup.webhook?.publicUrl || '';
|
|
275
275
|
const items = [
|
|
276
276
|
{
|
|
277
|
-
value: 'endpoint-
|
|
278
|
-
label: '
|
|
279
|
-
description:
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
value: 'endpoint-authtoken',
|
|
284
|
-
label: 'authtoken',
|
|
285
|
-
description: setup.webhook?.authenticated === true ? 'Set' : 'Not set · Enter authtoken',
|
|
286
|
-
_action: 'endpoint-authtoken',
|
|
277
|
+
value: 'endpoint-url',
|
|
278
|
+
label: 'Public URL',
|
|
279
|
+
description: publicUrl
|
|
280
|
+
? `${publicUrl}/webhook/<name>`
|
|
281
|
+
: 'Assigned when the channel worker connects to the relay',
|
|
282
|
+
_action: 'endpoint-url',
|
|
287
283
|
},
|
|
288
284
|
];
|
|
289
285
|
setPicker({
|
|
290
286
|
title: 'Webhook endpoint',
|
|
291
|
-
description: '
|
|
287
|
+
description: 'Served through the Mixdog relay — no tunnel setup needed. Toggle individual webhooks in /webhooks.',
|
|
292
288
|
help: '↑/↓ Select · Enter Edit · Esc Back',
|
|
293
289
|
indexMode: 'always',
|
|
294
290
|
labelWidth: 18,
|
|
295
291
|
items,
|
|
296
292
|
onSelect: (_value, item) => {
|
|
297
293
|
try {
|
|
298
|
-
if (item._action === 'endpoint-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
hint: 'Paste the reserved ngrok domain (e.g. my-app.ngrok-free.app).',
|
|
303
|
-
afterSave: () => void openChannelSetupPicker('webhook-endpoint', options),
|
|
304
|
-
});
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
if (item._action === 'endpoint-authtoken') {
|
|
308
|
-
openChannelPrompt({
|
|
309
|
-
kind: 'webhook-token',
|
|
310
|
-
label: 'Webhook/ngrok authtoken',
|
|
311
|
-
hint: 'Paste the webhook/ngrok authtoken. It is stored in the OS keychain.',
|
|
312
|
-
afterSave: () => void openChannelSetupPicker('webhook-endpoint', options),
|
|
313
|
-
});
|
|
294
|
+
if (item._action === 'endpoint-url') {
|
|
295
|
+
store.pushNotice(publicUrl
|
|
296
|
+
? `Webhook base: ${publicUrl}/webhook/<name>`
|
|
297
|
+
: 'URL not assigned yet — start channels once and reopen.', 'info');
|
|
314
298
|
}
|
|
315
299
|
} catch (e) {
|
|
316
300
|
store.pushNotice(`webhook endpoint failed: ${e?.message || e}`, 'error');
|
|
@@ -465,20 +449,12 @@ export function createChannelPickers({
|
|
|
465
449
|
{
|
|
466
450
|
value: 'webhook-endpoint',
|
|
467
451
|
label: 'Webhook endpoint',
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const hasDomain = Boolean(setup.webhook?.ngrokDomain || setup.webhook?.domain);
|
|
475
|
-
const hasAuth = setup.webhook?.authenticated === true;
|
|
476
|
-
const needs = [
|
|
477
|
-
...(hasDomain ? [] : ['domain']),
|
|
478
|
-
...(hasAuth ? [] : ['authtoken']),
|
|
479
|
-
];
|
|
480
|
-
return needs.length ? `Needs ${needs.join(' + ')}` : 'ngrok domain and authtoken set';
|
|
481
|
-
})(),
|
|
452
|
+
// Relay tunnel: public exposure is automatic, so the endpoint is
|
|
453
|
+
// "On" whenever the webhook server itself is enabled.
|
|
454
|
+
meta: setup.webhook?.enabled === false ? 'Off' : 'On',
|
|
455
|
+
description: setup.webhook?.publicUrl
|
|
456
|
+
? setup.webhook.publicUrl
|
|
457
|
+
: 'Mixdog relay tunnel — URL assigned on first run',
|
|
482
458
|
_action: 'webhook-endpoint',
|
|
483
459
|
},
|
|
484
460
|
];
|
package/src/tui/app/doctor.mjs
CHANGED
|
@@ -165,7 +165,6 @@ export async function buildDoctorReport(runtime = {}, getState = () => ({})) {
|
|
|
165
165
|
const tokens = [];
|
|
166
166
|
if (setup.discord?.authenticated) tokens.push('discord');
|
|
167
167
|
if (setup.telegram?.authenticated) tokens.push('telegram');
|
|
168
|
-
if (setup.webhook?.authenticated) tokens.push('webhook');
|
|
169
168
|
const running = worker.running === true;
|
|
170
169
|
const detail = `enabled · worker ${running ? 'running' : 'stopped'} · tokens: ${tokens.length ? tokens.join(', ') : 'none'}`;
|
|
171
170
|
row(running ? 'ok' : 'warn', detail);
|