Merge 1e0123b671 into 718bc3f9c8
This commit is contained in:
commit
707baecbb4
BIN
moltbot-2026.1.27-beta.3-patched.tgz
Normal file
BIN
moltbot-2026.1.27-beta.3-patched.tgz
Normal file
Binary file not shown.
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "moltbot",
|
"name": "moltbot",
|
||||||
"version": "2026.1.27-beta.1",
|
"version": "2026.1.27-beta.3-patched",
|
||||||
"description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent",
|
"description": "WhatsApp gateway CLI (Baileys web) with Pi RPC agent",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@ -17,8 +17,14 @@ export type GatewayAuthResult = {
|
|||||||
method?: "token" | "password" | "tailscale" | "device-token";
|
method?: "token" | "password" | "tailscale" | "device-token";
|
||||||
user?: string;
|
user?: string;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
|
tokenHint?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function maskToken(token: string): string {
|
||||||
|
if (token.length <= 8) return token.slice(0, 2) + "..." + token.slice(-2);
|
||||||
|
return token.slice(0, 4) + "..." + token.slice(-4);
|
||||||
|
}
|
||||||
|
|
||||||
type ConnectAuth = {
|
type ConnectAuth = {
|
||||||
token?: string;
|
token?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
@ -229,7 +235,11 @@ export async function authorizeGatewayConnect(params: {
|
|||||||
return { ok: false, reason: "token_missing" };
|
return { ok: false, reason: "token_missing" };
|
||||||
}
|
}
|
||||||
if (!safeEqual(connectAuth.token, auth.token)) {
|
if (!safeEqual(connectAuth.token, auth.token)) {
|
||||||
return { ok: false, reason: "token_mismatch" };
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: "token_mismatch",
|
||||||
|
tokenHint: `got=${maskToken(connectAuth.token)} expected=${maskToken(auth.token)}`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return { ok: true, method: "token" };
|
return { ok: true, method: "token" };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,18 @@
|
|||||||
import { normalizeVerboseLevel } from "../auto-reply/thinking.js";
|
import { normalizeVerboseLevel } from "../auto-reply/thinking.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import { type AgentEventPayload, getAgentRunContext } from "../infra/agent-events.js";
|
import { type AgentEventPayload, getAgentRunContext } from "../infra/agent-events.js";
|
||||||
|
import { isTruthyEnvValue } from "../infra/env.js";
|
||||||
import { resolveHeartbeatVisibility } from "../infra/heartbeat-visibility.js";
|
import { resolveHeartbeatVisibility } from "../infra/heartbeat-visibility.js";
|
||||||
import { loadSessionEntry } from "./session-utils.js";
|
import { loadSessionEntry } from "./session-utils.js";
|
||||||
import { formatForLog } from "./ws-log.js";
|
import { formatForLog } from "./ws-log.js";
|
||||||
|
|
||||||
|
const debugChatEnabled = isTruthyEnvValue(process.env.CLAWDBOT_DEBUG_CHAT);
|
||||||
|
const debugChat = (message: string) => {
|
||||||
|
if (debugChatEnabled) {
|
||||||
|
console.log(`[DEBUG] ${message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if webchat broadcasts should be suppressed for heartbeat runs.
|
* Check if webchat broadcasts should be suppressed for heartbeat runs.
|
||||||
* Returns true if the run is a heartbeat and showOk is false.
|
* Returns true if the run is a heartbeat and showOk is false.
|
||||||
@ -134,10 +142,16 @@ export function createAgentEventHandler({
|
|||||||
clearAgentRunContext,
|
clearAgentRunContext,
|
||||||
}: AgentEventHandlerOptions) {
|
}: AgentEventHandlerOptions) {
|
||||||
const emitChatDelta = (sessionKey: string, clientRunId: string, seq: number, text: string) => {
|
const emitChatDelta = (sessionKey: string, clientRunId: string, seq: number, text: string) => {
|
||||||
|
debugChat(
|
||||||
|
`emitChatDelta: sessionKey=${sessionKey} runId=${clientRunId} textLen=${text.length}`,
|
||||||
|
);
|
||||||
chatRunState.buffers.set(clientRunId, text);
|
chatRunState.buffers.set(clientRunId, text);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const last = chatRunState.deltaSentAt.get(clientRunId) ?? 0;
|
const last = chatRunState.deltaSentAt.get(clientRunId) ?? 0;
|
||||||
if (now - last < 150) return;
|
if (now - last < 150) {
|
||||||
|
debugChat(`emitChatDelta: throttled (${now - last}ms < 150ms)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
chatRunState.deltaSentAt.set(clientRunId, now);
|
chatRunState.deltaSentAt.set(clientRunId, now);
|
||||||
const payload = {
|
const payload = {
|
||||||
runId: clientRunId,
|
runId: clientRunId,
|
||||||
@ -151,9 +165,13 @@ export function createAgentEventHandler({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
// Suppress webchat broadcast for heartbeat runs when showOk is false
|
// Suppress webchat broadcast for heartbeat runs when showOk is false
|
||||||
if (!shouldSuppressHeartbeatBroadcast(clientRunId)) {
|
const suppressed = shouldSuppressHeartbeatBroadcast(clientRunId);
|
||||||
|
debugChat(`emitChatDelta: heartbeatSuppressed=${suppressed}`);
|
||||||
|
if (!suppressed) {
|
||||||
|
debugChat(`emitChatDelta: calling broadcast("chat")`);
|
||||||
broadcast("chat", payload, { dropIfSlow: true });
|
broadcast("chat", payload, { dropIfSlow: true });
|
||||||
}
|
}
|
||||||
|
debugChat(`emitChatDelta: calling nodeSendToSession`);
|
||||||
nodeSendToSession(sessionKey, "chat", payload);
|
nodeSendToSession(sessionKey, "chat", payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -216,8 +234,11 @@ export function createAgentEventHandler({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (evt: AgentEventPayload) => {
|
return (evt: AgentEventPayload) => {
|
||||||
|
debugChat(`agent event: runId=${evt.runId} stream=${evt.stream} seq=${evt.seq}`);
|
||||||
const chatLink = chatRunState.registry.peek(evt.runId);
|
const chatLink = chatRunState.registry.peek(evt.runId);
|
||||||
|
debugChat(`chatLink: ${chatLink ? JSON.stringify(chatLink) : "null"}`);
|
||||||
const sessionKey = chatLink?.sessionKey ?? resolveSessionKeyForRun(evt.runId);
|
const sessionKey = chatLink?.sessionKey ?? resolveSessionKeyForRun(evt.runId);
|
||||||
|
debugChat(`sessionKey: ${sessionKey}`);
|
||||||
const clientRunId = chatLink?.clientRunId ?? evt.runId;
|
const clientRunId = chatLink?.clientRunId ?? evt.runId;
|
||||||
const isAborted =
|
const isAborted =
|
||||||
chatRunState.abortedRuns.has(clientRunId) || chatRunState.abortedRuns.has(evt.runId);
|
chatRunState.abortedRuns.has(clientRunId) || chatRunState.abortedRuns.has(evt.runId);
|
||||||
@ -249,7 +270,11 @@ export function createAgentEventHandler({
|
|||||||
|
|
||||||
if (sessionKey) {
|
if (sessionKey) {
|
||||||
nodeSendToSession(sessionKey, "agent", agentPayload);
|
nodeSendToSession(sessionKey, "agent", agentPayload);
|
||||||
|
debugChat(
|
||||||
|
`checking emitChatDelta: isAborted=${isAborted} stream=${evt.stream} hasText=${typeof evt.data?.text === "string"} text="${String(evt.data?.text ?? "").slice(0, 50)}..."`,
|
||||||
|
);
|
||||||
if (!isAborted && evt.stream === "assistant" && typeof evt.data?.text === "string") {
|
if (!isAborted && evt.stream === "assistant" && typeof evt.data?.text === "string") {
|
||||||
|
debugChat(`calling emitChatDelta`);
|
||||||
emitChatDelta(sessionKey, clientRunId, evt.seq, evt.data.text);
|
emitChatDelta(sessionKey, clientRunId, evt.seq, evt.data.text);
|
||||||
} else if (!isAborted && (lifecyclePhase === "end" || lifecyclePhase === "error")) {
|
} else if (!isAborted && (lifecyclePhase === "end" || lifecyclePhase === "error")) {
|
||||||
if (chatLink) {
|
if (chatLink) {
|
||||||
|
|||||||
@ -1,6 +1,14 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { isTruthyEnvValue } from "../../infra/env.js";
|
||||||
|
|
||||||
|
const debugChatEnabled = isTruthyEnvValue(process.env.CLAWDBOT_DEBUG_CHAT);
|
||||||
|
const debugChat = (message: string) => {
|
||||||
|
if (debugChatEnabled) {
|
||||||
|
console.log(`[DEBUG] ${message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
import { CURRENT_SESSION_VERSION } from "@mariozechner/pi-coding-agent";
|
import { CURRENT_SESSION_VERSION } from "@mariozechner/pi-coding-agent";
|
||||||
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
|
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
|
||||||
@ -9,6 +17,7 @@ import { resolveThinkingDefault } from "../../agents/model-selection.js";
|
|||||||
import { resolveAgentTimeoutMs } from "../../agents/timeout.js";
|
import { resolveAgentTimeoutMs } from "../../agents/timeout.js";
|
||||||
import { dispatchInboundMessage } from "../../auto-reply/dispatch.js";
|
import { dispatchInboundMessage } from "../../auto-reply/dispatch.js";
|
||||||
import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.js";
|
import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.js";
|
||||||
|
import { registerAgentRunContext } from "../../infra/agent-events.js";
|
||||||
import {
|
import {
|
||||||
extractShortModelName,
|
extractShortModelName,
|
||||||
type ResponsePrefixContext,
|
type ResponsePrefixContext,
|
||||||
@ -199,8 +208,12 @@ export const chatHandlers: GatewayRequestHandlers = {
|
|||||||
};
|
};
|
||||||
const { cfg, storePath, entry } = loadSessionEntry(sessionKey);
|
const { cfg, storePath, entry } = loadSessionEntry(sessionKey);
|
||||||
const sessionId = entry?.sessionId;
|
const sessionId = entry?.sessionId;
|
||||||
|
debugChat(
|
||||||
|
`chat.history: sessionKey="${sessionKey}" sessionId="${sessionId}" storePath="${storePath}" hasEntry=${!!entry}`,
|
||||||
|
);
|
||||||
const rawMessages =
|
const rawMessages =
|
||||||
sessionId && storePath ? readSessionMessages(sessionId, storePath, entry?.sessionFile) : [];
|
sessionId && storePath ? readSessionMessages(sessionId, storePath, entry?.sessionFile) : [];
|
||||||
|
debugChat(`chat.history: rawMessages.length=${rawMessages.length}`);
|
||||||
const hardMax = 1000;
|
const hardMax = 1000;
|
||||||
const defaultLimit = 200;
|
const defaultLimit = 200;
|
||||||
const requested = typeof limit === "number" ? limit : defaultLimit;
|
const requested = typeof limit === "number" ? limit : defaultLimit;
|
||||||
@ -362,13 +375,14 @@ export const chatHandlers: GatewayRequestHandlers = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { cfg, entry } = loadSessionEntry(p.sessionKey);
|
const { cfg, entry, canonicalKey } = loadSessionEntry(p.sessionKey);
|
||||||
const timeoutMs = resolveAgentTimeoutMs({
|
const timeoutMs = resolveAgentTimeoutMs({
|
||||||
cfg,
|
cfg,
|
||||||
overrideMs: p.timeoutMs,
|
overrideMs: p.timeoutMs,
|
||||||
});
|
});
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const clientRunId = p.idempotencyKey;
|
const clientRunId = p.idempotencyKey;
|
||||||
|
registerAgentRunContext(clientRunId, { sessionKey: p.sessionKey });
|
||||||
|
|
||||||
const sendPolicy = resolveSendPolicy({
|
const sendPolicy = resolveSendPolicy({
|
||||||
cfg,
|
cfg,
|
||||||
@ -430,6 +444,10 @@ export const chatHandlers: GatewayRequestHandlers = {
|
|||||||
startedAtMs: now,
|
startedAtMs: now,
|
||||||
expiresAtMs: resolveChatRunExpiresAtMs({ now, timeoutMs }),
|
expiresAtMs: resolveChatRunExpiresAtMs({ now, timeoutMs }),
|
||||||
});
|
});
|
||||||
|
context.addChatRun(clientRunId, {
|
||||||
|
sessionKey: p.sessionKey,
|
||||||
|
clientRunId,
|
||||||
|
});
|
||||||
|
|
||||||
const ackPayload = {
|
const ackPayload = {
|
||||||
runId: clientRunId,
|
runId: clientRunId,
|
||||||
@ -449,7 +467,7 @@ export const chatHandlers: GatewayRequestHandlers = {
|
|||||||
BodyForCommands: commandBody,
|
BodyForCommands: commandBody,
|
||||||
RawBody: parsedMessage,
|
RawBody: parsedMessage,
|
||||||
CommandBody: commandBody,
|
CommandBody: commandBody,
|
||||||
SessionKey: p.sessionKey,
|
SessionKey: canonicalKey,
|
||||||
Provider: INTERNAL_MESSAGE_CHANNEL,
|
Provider: INTERNAL_MESSAGE_CHANNEL,
|
||||||
Surface: INTERNAL_MESSAGE_CHANNEL,
|
Surface: INTERNAL_MESSAGE_CHANNEL,
|
||||||
OriginatingChannel: INTERNAL_MESSAGE_CHANNEL,
|
OriginatingChannel: INTERNAL_MESSAGE_CHANNEL,
|
||||||
|
|||||||
@ -14,7 +14,24 @@ import { defaultRuntime } from "../runtime.js";
|
|||||||
import { deliveryContextFromSession, mergeDeliveryContext } from "../utils/delivery-context.js";
|
import { deliveryContextFromSession, mergeDeliveryContext } from "../utils/delivery-context.js";
|
||||||
import { loadSessionEntry } from "./session-utils.js";
|
import { loadSessionEntry } from "./session-utils.js";
|
||||||
|
|
||||||
export async function scheduleRestartSentinelWake(params: { deps: CliDeps }) {
|
export type AddChatRunFn = (
|
||||||
|
runId: string,
|
||||||
|
entry: { sessionKey: string; clientRunId: string },
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export type BroadcastFn = (
|
||||||
|
event: string,
|
||||||
|
payload: unknown,
|
||||||
|
opts?: { dropIfSlow?: boolean },
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
export interface RestartSentinelParams {
|
||||||
|
deps: CliDeps;
|
||||||
|
addChatRun?: AddChatRunFn;
|
||||||
|
broadcast?: BroadcastFn;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function scheduleRestartSentinelWake(params: RestartSentinelParams) {
|
||||||
const sentinel = await consumeRestartSentinel();
|
const sentinel = await consumeRestartSentinel();
|
||||||
if (!sentinel) return;
|
if (!sentinel) return;
|
||||||
const payload = sentinel.payload;
|
const payload = sentinel.payload;
|
||||||
@ -61,7 +78,27 @@ export async function scheduleRestartSentinelWake(params: { deps: CliDeps }) {
|
|||||||
const channel = channelRaw ? normalizeChannelId(channelRaw) : null;
|
const channel = channelRaw ? normalizeChannelId(channelRaw) : null;
|
||||||
const to = origin?.to;
|
const to = origin?.to;
|
||||||
if (!channel || !to) {
|
if (!channel || !to) {
|
||||||
|
// No outbound channel (e.g., webchat sessions use WebSocket, not channel delivery).
|
||||||
|
// For webchat: register the run with addChatRun so responses stream to the client.
|
||||||
enqueueSystemEvent(message, { sessionKey });
|
enqueueSystemEvent(message, { sessionKey });
|
||||||
|
if (params.addChatRun && params.broadcast) {
|
||||||
|
const runId = `restart-${Date.now()}`;
|
||||||
|
params.addChatRun(runId, { sessionKey, clientRunId: runId });
|
||||||
|
try {
|
||||||
|
await agentCommand(
|
||||||
|
{
|
||||||
|
message: `System: ${summary}`,
|
||||||
|
sessionKey,
|
||||||
|
runId,
|
||||||
|
deliver: false,
|
||||||
|
},
|
||||||
|
defaultRuntime,
|
||||||
|
params.deps,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// Agent turn failed; system event already queued as fallback context
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,8 @@ import type { loadMoltbotPlugins } from "../plugins/loader.js";
|
|||||||
import { type PluginServicesHandle, startPluginServices } from "../plugins/services.js";
|
import { type PluginServicesHandle, startPluginServices } from "../plugins/services.js";
|
||||||
import { startBrowserControlServerIfEnabled } from "./server-browser.js";
|
import { startBrowserControlServerIfEnabled } from "./server-browser.js";
|
||||||
import {
|
import {
|
||||||
|
type AddChatRunFn,
|
||||||
|
type BroadcastFn,
|
||||||
scheduleRestartSentinelWake,
|
scheduleRestartSentinelWake,
|
||||||
shouldWakeFromRestartSentinel,
|
shouldWakeFromRestartSentinel,
|
||||||
} from "./server-restart-sentinel.js";
|
} from "./server-restart-sentinel.js";
|
||||||
@ -37,6 +39,8 @@ export async function startGatewaySidecars(params: {
|
|||||||
};
|
};
|
||||||
logChannels: { info: (msg: string) => void; error: (msg: string) => void };
|
logChannels: { info: (msg: string) => void; error: (msg: string) => void };
|
||||||
logBrowser: { error: (msg: string) => void };
|
logBrowser: { error: (msg: string) => void };
|
||||||
|
addChatRun?: AddChatRunFn;
|
||||||
|
broadcast?: BroadcastFn;
|
||||||
}) {
|
}) {
|
||||||
// Start clawd browser control server (unless disabled via config).
|
// Start clawd browser control server (unless disabled via config).
|
||||||
let browserControl: Awaited<ReturnType<typeof startBrowserControlServerIfEnabled>> = null;
|
let browserControl: Awaited<ReturnType<typeof startBrowserControlServerIfEnabled>> = null;
|
||||||
@ -152,7 +156,11 @@ export async function startGatewaySidecars(params: {
|
|||||||
|
|
||||||
if (shouldWakeFromRestartSentinel()) {
|
if (shouldWakeFromRestartSentinel()) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
void scheduleRestartSentinelWake({ deps: params.deps });
|
void scheduleRestartSentinelWake({
|
||||||
|
deps: params.deps,
|
||||||
|
addChatRun: params.addChatRun,
|
||||||
|
broadcast: params.broadcast,
|
||||||
|
});
|
||||||
}, 750);
|
}, 750);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -503,6 +503,8 @@ export async function startGatewayServer(
|
|||||||
logHooks,
|
logHooks,
|
||||||
logChannels,
|
logChannels,
|
||||||
logBrowser,
|
logBrowser,
|
||||||
|
addChatRun,
|
||||||
|
broadcast,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { applyHotReload, requestGatewayRestart } = createGatewayReloadHandlers({
|
const { applyHotReload, requestGatewayRestart } = createGatewayReloadHandlers({
|
||||||
|
|||||||
@ -605,6 +605,7 @@ export function attachGatewayWsMessageHandler(params: {
|
|||||||
authMode: resolvedAuth.mode,
|
authMode: resolvedAuth.mode,
|
||||||
authProvided,
|
authProvided,
|
||||||
authReason: authResult.reason,
|
authReason: authResult.reason,
|
||||||
|
authTokenHint: authResult.tokenHint,
|
||||||
allowTailscale: resolvedAuth.allowTailscale,
|
allowTailscale: resolvedAuth.allowTailscale,
|
||||||
client: connectParams.client.id,
|
client: connectParams.client.id,
|
||||||
clientDisplayName: connectParams.client.displayName,
|
clientDisplayName: connectParams.client.displayName,
|
||||||
|
|||||||
@ -56,6 +56,33 @@ describe("buildTelegramMessageContext dm thread sessions", () => {
|
|||||||
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:main:thread:42");
|
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:main:thread:42");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("sets ThreadLabel for dm topics for display in Sessions tab", async () => {
|
||||||
|
const ctx = await buildContext({
|
||||||
|
message_id: 1,
|
||||||
|
chat: { id: 1234, type: "private" },
|
||||||
|
date: 1700000000,
|
||||||
|
text: "hello",
|
||||||
|
message_thread_id: 42,
|
||||||
|
from: { id: 42, first_name: "Alice" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ctx).not.toBeNull();
|
||||||
|
expect(ctx?.ctxPayload?.ThreadLabel).toBe("Thread: 42");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not set ThreadLabel for dm without thread id", async () => {
|
||||||
|
const ctx = await buildContext({
|
||||||
|
message_id: 1,
|
||||||
|
chat: { id: 1234, type: "private" },
|
||||||
|
date: 1700000000,
|
||||||
|
text: "hello",
|
||||||
|
from: { id: 42, first_name: "Alice" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ctx).not.toBeNull();
|
||||||
|
expect(ctx?.ctxPayload?.ThreadLabel).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps legacy dm session key when no thread id", async () => {
|
it("keeps legacy dm session key when no thread id", async () => {
|
||||||
const ctx = await buildContext({
|
const ctx = await buildContext({
|
||||||
message_id: 2,
|
message_id: 2,
|
||||||
|
|||||||
@ -161,6 +161,9 @@ export const buildTelegramMessageContext = async ({
|
|||||||
isForum,
|
isForum,
|
||||||
messageThreadId,
|
messageThreadId,
|
||||||
});
|
});
|
||||||
|
// Effective thread ID for outbound delivery: groups use forum-resolved; DMs use raw messageThreadId
|
||||||
|
// (private chats never set is_forum=true per Telegram Bot API, so resolvedThreadId is always undefined for DMs)
|
||||||
|
const effectiveThreadId = isGroup ? resolvedThreadId : messageThreadId;
|
||||||
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId);
|
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId);
|
||||||
const peerId = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId);
|
const peerId = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId);
|
||||||
const route = resolveAgentRoute({
|
const route = resolveAgentRoute({
|
||||||
@ -203,7 +206,8 @@ export const buildTelegramMessageContext = async ({
|
|||||||
const sendTyping = async () => {
|
const sendTyping = async () => {
|
||||||
await withTelegramApiErrorLogging({
|
await withTelegramApiErrorLogging({
|
||||||
operation: "sendChatAction",
|
operation: "sendChatAction",
|
||||||
fn: () => bot.api.sendChatAction(chatId, "typing", buildTypingThreadParams(resolvedThreadId)),
|
fn: () =>
|
||||||
|
bot.api.sendChatAction(chatId, "typing", buildTypingThreadParams(effectiveThreadId)),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -212,7 +216,11 @@ export const buildTelegramMessageContext = async ({
|
|||||||
await withTelegramApiErrorLogging({
|
await withTelegramApiErrorLogging({
|
||||||
operation: "sendChatAction",
|
operation: "sendChatAction",
|
||||||
fn: () =>
|
fn: () =>
|
||||||
bot.api.sendChatAction(chatId, "record_voice", buildTypingThreadParams(resolvedThreadId)),
|
bot.api.sendChatAction(
|
||||||
|
chatId,
|
||||||
|
"record_voice",
|
||||||
|
buildTypingThreadParams(effectiveThreadId),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logVerbose(`telegram record_voice cue failed for chat ${chatId}: ${String(err)}`);
|
logVerbose(`telegram record_voice cue failed for chat ${chatId}: ${String(err)}`);
|
||||||
@ -606,6 +614,8 @@ export const buildTelegramMessageContext = async ({
|
|||||||
// For groups: use resolvedThreadId (forum topics only); for DMs: use raw messageThreadId
|
// For groups: use resolvedThreadId (forum topics only); for DMs: use raw messageThreadId
|
||||||
MessageThreadId: isGroup ? resolvedThreadId : messageThreadId,
|
MessageThreadId: isGroup ? resolvedThreadId : messageThreadId,
|
||||||
IsForum: isForum,
|
IsForum: isForum,
|
||||||
|
// DM thread label for display in Sessions tab (e.g., "Sender Name (Thread: 42)")
|
||||||
|
ThreadLabel: !isGroup && messageThreadId != null ? `Thread: ${messageThreadId}` : undefined,
|
||||||
// Originating channel for reply routing.
|
// Originating channel for reply routing.
|
||||||
OriginatingChannel: "telegram" as const,
|
OriginatingChannel: "telegram" as const,
|
||||||
OriginatingTo: `telegram:${chatId}`,
|
OriginatingTo: `telegram:${chatId}`,
|
||||||
@ -656,7 +666,7 @@ export const buildTelegramMessageContext = async ({
|
|||||||
msg,
|
msg,
|
||||||
chatId,
|
chatId,
|
||||||
isGroup,
|
isGroup,
|
||||||
resolvedThreadId,
|
resolvedThreadId: effectiveThreadId,
|
||||||
isForum,
|
isForum,
|
||||||
historyKey,
|
historyKey,
|
||||||
historyLimit,
|
historyLimit,
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import type { Tab } from "./navigation";
|
|||||||
import type { UiSettings } from "./storage";
|
import type { UiSettings } from "./storage";
|
||||||
import { handleAgentEvent, resetToolStream, type AgentEventPayload } from "./app-tool-stream";
|
import { handleAgentEvent, resetToolStream, type AgentEventPayload } from "./app-tool-stream";
|
||||||
import { flushChatQueueForEvent } from "./app-chat";
|
import { flushChatQueueForEvent } from "./app-chat";
|
||||||
|
import { debug } from "./debug";
|
||||||
import {
|
import {
|
||||||
applySettings,
|
applySettings,
|
||||||
loadCron,
|
loadCron,
|
||||||
@ -81,7 +82,12 @@ function normalizeSessionKeyForDefaults(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applySessionDefaults(host: GatewayHost, defaults?: SessionDefaultsSnapshot) {
|
function applySessionDefaults(host: GatewayHost, defaults?: SessionDefaultsSnapshot) {
|
||||||
if (!defaults?.mainSessionKey) return;
|
debug(`applySessionDefaults: defaults=${JSON.stringify(defaults)}`);
|
||||||
|
debug(`applySessionDefaults: host.sessionKey="${host.sessionKey}" host.settings.sessionKey="${host.settings.sessionKey}"`);
|
||||||
|
if (!defaults?.mainSessionKey) {
|
||||||
|
debug(`applySessionDefaults: no mainSessionKey, returning early`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const resolvedSessionKey = normalizeSessionKeyForDefaults(host.sessionKey, defaults);
|
const resolvedSessionKey = normalizeSessionKeyForDefaults(host.sessionKey, defaults);
|
||||||
const resolvedSettingsSessionKey = normalizeSessionKeyForDefaults(
|
const resolvedSettingsSessionKey = normalizeSessionKeyForDefaults(
|
||||||
host.settings.sessionKey,
|
host.settings.sessionKey,
|
||||||
@ -91,6 +97,7 @@ function applySessionDefaults(host: GatewayHost, defaults?: SessionDefaultsSnaps
|
|||||||
host.settings.lastActiveSessionKey,
|
host.settings.lastActiveSessionKey,
|
||||||
defaults,
|
defaults,
|
||||||
);
|
);
|
||||||
|
debug(`applySessionDefaults: resolved="${resolvedSessionKey}" settingsResolved="${resolvedSettingsSessionKey}"`);
|
||||||
const nextSessionKey = resolvedSessionKey || resolvedSettingsSessionKey || host.sessionKey;
|
const nextSessionKey = resolvedSessionKey || resolvedSettingsSessionKey || host.sessionKey;
|
||||||
const nextSettings = {
|
const nextSettings = {
|
||||||
...host.settings,
|
...host.settings,
|
||||||
@ -100,7 +107,9 @@ function applySessionDefaults(host: GatewayHost, defaults?: SessionDefaultsSnaps
|
|||||||
const shouldUpdateSettings =
|
const shouldUpdateSettings =
|
||||||
nextSettings.sessionKey !== host.settings.sessionKey ||
|
nextSettings.sessionKey !== host.settings.sessionKey ||
|
||||||
nextSettings.lastActiveSessionKey !== host.settings.lastActiveSessionKey;
|
nextSettings.lastActiveSessionKey !== host.settings.lastActiveSessionKey;
|
||||||
|
debug(`applySessionDefaults: nextSessionKey="${nextSessionKey}" shouldUpdate=${shouldUpdateSettings}`);
|
||||||
if (nextSessionKey !== host.sessionKey) {
|
if (nextSessionKey !== host.sessionKey) {
|
||||||
|
debug(`applySessionDefaults: updating host.sessionKey from "${host.sessionKey}" to "${nextSessionKey}"`);
|
||||||
host.sessionKey = nextSessionKey;
|
host.sessionKey = nextSessionKey;
|
||||||
}
|
}
|
||||||
if (shouldUpdateSettings) {
|
if (shouldUpdateSettings) {
|
||||||
@ -189,13 +198,18 @@ function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const state = handleChatEvent(host as unknown as MoltbotApp, payload);
|
const state = handleChatEvent(host as unknown as MoltbotApp, payload);
|
||||||
|
// Force Lit to re-render after chatStream mutation
|
||||||
|
if (state === "delta") {
|
||||||
|
(host as unknown as MoltbotApp).requestUpdate();
|
||||||
|
}
|
||||||
if (state === "final" || state === "error" || state === "aborted") {
|
if (state === "final" || state === "error" || state === "aborted") {
|
||||||
resetToolStream(host as unknown as Parameters<typeof resetToolStream>[0]);
|
resetToolStream(host as unknown as Parameters<typeof resetToolStream>[0]);
|
||||||
void flushChatQueueForEvent(
|
void flushChatQueueForEvent(
|
||||||
host as unknown as Parameters<typeof flushChatQueueForEvent>[0],
|
host as unknown as Parameters<typeof flushChatQueueForEvent>[0],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (state === "final") void loadChatHistory(host as unknown as MoltbotApp);
|
// Don't call loadChatHistory - we already added the message from the final payload
|
||||||
|
// loadChatHistory returns 0 messages because the session transcript file doesn't exist
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -35,6 +35,11 @@ type LifecycleHost = {
|
|||||||
|
|
||||||
export function handleConnected(host: LifecycleHost) {
|
export function handleConnected(host: LifecycleHost) {
|
||||||
host.basePath = inferBasePath();
|
host.basePath = inferBasePath();
|
||||||
|
// Apply URL settings FIRST so sessionKey is read before syncTabWithLocation
|
||||||
|
// overwrites it with the localStorage value
|
||||||
|
applySettingsFromUrl(
|
||||||
|
host as unknown as Parameters<typeof applySettingsFromUrl>[0],
|
||||||
|
);
|
||||||
syncTabWithLocation(
|
syncTabWithLocation(
|
||||||
host as unknown as Parameters<typeof syncTabWithLocation>[0],
|
host as unknown as Parameters<typeof syncTabWithLocation>[0],
|
||||||
true,
|
true,
|
||||||
@ -46,9 +51,6 @@ export function handleConnected(host: LifecycleHost) {
|
|||||||
host as unknown as Parameters<typeof attachThemeListener>[0],
|
host as unknown as Parameters<typeof attachThemeListener>[0],
|
||||||
);
|
);
|
||||||
window.addEventListener("popstate", host.popStateHandler);
|
window.addEventListener("popstate", host.popStateHandler);
|
||||||
applySettingsFromUrl(
|
|
||||||
host as unknown as Parameters<typeof applySettingsFromUrl>[0],
|
|
||||||
);
|
|
||||||
connectGateway(host as unknown as Parameters<typeof connectGateway>[0]);
|
connectGateway(host as unknown as Parameters<typeof connectGateway>[0]);
|
||||||
startNodesPolling(host as unknown as Parameters<typeof startNodesPolling>[0]);
|
startNodesPolling(host as unknown as Parameters<typeof startNodesPolling>[0]);
|
||||||
if (host.tab === "logs") {
|
if (host.tab === "logs") {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { html, nothing } from "lit";
|
|||||||
import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway";
|
import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway";
|
||||||
import type { AppViewState } from "./app-view-state";
|
import type { AppViewState } from "./app-view-state";
|
||||||
import { parseAgentSessionKey } from "../../../src/routing/session-key.js";
|
import { parseAgentSessionKey } from "../../../src/routing/session-key.js";
|
||||||
|
import { debug } from "./debug";
|
||||||
import {
|
import {
|
||||||
TAB_GROUPS,
|
TAB_GROUPS,
|
||||||
iconForTab,
|
iconForTab,
|
||||||
@ -427,7 +428,7 @@ export function renderApp(state: AppViewState) {
|
|||||||
: nothing}
|
: nothing}
|
||||||
|
|
||||||
${state.tab === "chat"
|
${state.tab === "chat"
|
||||||
? renderChat({
|
? (() => { debug(`RENDER chatStream="${state.chatStream?.slice(0, 50) ?? "null"}..." chatRunId="${state.chatRunId}" messages=${state.chatMessages?.length ?? 0}`); return renderChat({
|
||||||
sessionKey: state.sessionKey,
|
sessionKey: state.sessionKey,
|
||||||
onSessionKeyChange: (next) => {
|
onSessionKeyChange: (next) => {
|
||||||
state.sessionKey = next;
|
state.sessionKey = next;
|
||||||
@ -497,7 +498,7 @@ export function renderApp(state: AppViewState) {
|
|||||||
onSplitRatioChange: (ratio: number) => state.handleSplitRatioChange(ratio),
|
onSplitRatioChange: (ratio: number) => state.handleSplitRatioChange(ratio),
|
||||||
assistantName: state.assistantName,
|
assistantName: state.assistantName,
|
||||||
assistantAvatar: state.assistantAvatar,
|
assistantAvatar: state.assistantAvatar,
|
||||||
})
|
}); })()
|
||||||
: nothing}
|
: nothing}
|
||||||
|
|
||||||
${state.tab === "config"
|
${state.tab === "config"
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { startThemeTransition, type ThemeTransitionContext } from "./theme-trans
|
|||||||
import { scheduleChatScroll, scheduleLogsScroll } from "./app-scroll";
|
import { scheduleChatScroll, scheduleLogsScroll } from "./app-scroll";
|
||||||
import { startLogsPolling, stopLogsPolling, startDebugPolling, stopDebugPolling } from "./app-polling";
|
import { startLogsPolling, stopLogsPolling, startDebugPolling, stopDebugPolling } from "./app-polling";
|
||||||
import { refreshChat } from "./app-chat";
|
import { refreshChat } from "./app-chat";
|
||||||
|
import { setDebugEnabled } from "./debug";
|
||||||
import type { MoltbotApp } from "./app";
|
import type { MoltbotApp } from "./app";
|
||||||
|
|
||||||
type SettingsHost = {
|
type SettingsHost = {
|
||||||
@ -43,6 +44,7 @@ export function applySettings(host: SettingsHost, next: UiSettings) {
|
|||||||
};
|
};
|
||||||
host.settings = normalized;
|
host.settings = normalized;
|
||||||
saveSettings(normalized);
|
saveSettings(normalized);
|
||||||
|
setDebugEnabled(normalized.debugLogs);
|
||||||
if (next.theme !== host.theme) {
|
if (next.theme !== host.theme) {
|
||||||
host.theme = next.theme;
|
host.theme = next.theme;
|
||||||
applyResolvedTheme(host, resolveTheme(next.theme));
|
applyResolvedTheme(host, resolveTheme(next.theme));
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { extractText } from "../chat/message-extract";
|
|||||||
import type { GatewayBrowserClient } from "../gateway";
|
import type { GatewayBrowserClient } from "../gateway";
|
||||||
import { generateUUID } from "../uuid";
|
import { generateUUID } from "../uuid";
|
||||||
import type { ChatAttachment } from "../ui-types";
|
import type { ChatAttachment } from "../ui-types";
|
||||||
|
import { debug } from "../debug";
|
||||||
|
|
||||||
export type ChatState = {
|
export type ChatState = {
|
||||||
client: GatewayBrowserClient | null;
|
client: GatewayBrowserClient | null;
|
||||||
@ -28,7 +29,11 @@ export type ChatEventPayload = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function loadChatHistory(state: ChatState) {
|
export async function loadChatHistory(state: ChatState) {
|
||||||
if (!state.client || !state.connected) return;
|
debug(`loadChatHistory: starting, sessionKey="${state.sessionKey}"`);
|
||||||
|
if (!state.client || !state.connected) {
|
||||||
|
debug(`loadChatHistory: aborted - not connected`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
state.chatLoading = true;
|
state.chatLoading = true;
|
||||||
state.lastError = null;
|
state.lastError = null;
|
||||||
try {
|
try {
|
||||||
@ -36,9 +41,11 @@ export async function loadChatHistory(state: ChatState) {
|
|||||||
sessionKey: state.sessionKey,
|
sessionKey: state.sessionKey,
|
||||||
limit: 200,
|
limit: 200,
|
||||||
})) as { messages?: unknown[]; thinkingLevel?: string | null };
|
})) as { messages?: unknown[]; thinkingLevel?: string | null };
|
||||||
|
debug(`loadChatHistory: got ${res.messages?.length ?? 0} messages`);
|
||||||
state.chatMessages = Array.isArray(res.messages) ? res.messages : [];
|
state.chatMessages = Array.isArray(res.messages) ? res.messages : [];
|
||||||
state.chatThinkingLevel = res.thinkingLevel ?? null;
|
state.chatThinkingLevel = res.thinkingLevel ?? null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
debug(`loadChatHistory: error - ${err}`);
|
||||||
state.lastError = String(err);
|
state.lastError = String(err);
|
||||||
} finally {
|
} finally {
|
||||||
state.chatLoading = false;
|
state.chatLoading = false;
|
||||||
@ -159,8 +166,13 @@ export function handleChatEvent(
|
|||||||
state: ChatState,
|
state: ChatState,
|
||||||
payload?: ChatEventPayload,
|
payload?: ChatEventPayload,
|
||||||
) {
|
) {
|
||||||
|
debug(`handleChatEvent: payload.sessionKey="${payload?.sessionKey}" state.sessionKey="${state.sessionKey}"`);
|
||||||
|
debug(`handleChatEvent: payload.runId="${payload?.runId}" state.chatRunId="${state.chatRunId}" payload.state="${payload?.state}"`);
|
||||||
if (!payload) return null;
|
if (!payload) return null;
|
||||||
if (payload.sessionKey !== state.sessionKey) return null;
|
if (payload.sessionKey !== state.sessionKey) {
|
||||||
|
debug(`handleChatEvent: sessionKey mismatch, ignoring event`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Final from another run (e.g. sub-agent announce): refresh history to show new message.
|
// Final from another run (e.g. sub-agent announce): refresh history to show new message.
|
||||||
// See https://github.com/moltbot/moltbot/issues/1909
|
// See https://github.com/moltbot/moltbot/issues/1909
|
||||||
@ -169,19 +181,27 @@ export function handleChatEvent(
|
|||||||
state.chatRunId &&
|
state.chatRunId &&
|
||||||
payload.runId !== state.chatRunId
|
payload.runId !== state.chatRunId
|
||||||
) {
|
) {
|
||||||
|
debug(`handleChatEvent: runId mismatch (payload=${payload.runId} state=${state.chatRunId}), dropping non-final`);
|
||||||
if (payload.state === "final") return "final";
|
if (payload.state === "final") return "final";
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.state === "delta") {
|
if (payload.state === "delta") {
|
||||||
const next = extractText(payload.message);
|
const next = extractText(payload.message);
|
||||||
|
debug(`handleChatEvent: delta event, extracted text="${next?.slice(0, 50)}..."`);
|
||||||
if (typeof next === "string") {
|
if (typeof next === "string") {
|
||||||
const current = state.chatStream ?? "";
|
const current = state.chatStream ?? "";
|
||||||
if (!current || next.length >= current.length) {
|
if (!current || next.length >= current.length) {
|
||||||
|
debug(`handleChatEvent: updating chatStream (len ${current.length} -> ${next.length})`);
|
||||||
state.chatStream = next;
|
state.chatStream = next;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (payload.state === "final") {
|
} else if (payload.state === "final") {
|
||||||
|
// Add the final message to chatMessages if present
|
||||||
|
if (payload.message) {
|
||||||
|
debug(`handleChatEvent: final with message, adding to chatMessages`);
|
||||||
|
state.chatMessages = [...state.chatMessages, payload.message];
|
||||||
|
}
|
||||||
state.chatStream = null;
|
state.chatStream = null;
|
||||||
state.chatRunId = null;
|
state.chatRunId = null;
|
||||||
state.chatStreamStartedAt = null;
|
state.chatStreamStartedAt = null;
|
||||||
|
|||||||
38
ui/src/ui/debug.ts
Normal file
38
ui/src/ui/debug.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Debug logging utility for the UI.
|
||||||
|
* Controlled by the debugLogs setting in UI settings.
|
||||||
|
* Can also be enabled via:
|
||||||
|
* - localStorage.setItem('moltbot-ui-debug', 'true')
|
||||||
|
* - window.MOLTBOT_UI_DEBUG = true
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
MOLTBOT_UI_DEBUG?: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let debugEnabled = false;
|
||||||
|
|
||||||
|
export function setDebugEnabled(enabled: boolean): void {
|
||||||
|
debugEnabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDebugEnabled(): boolean {
|
||||||
|
if (debugEnabled) return true;
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
if (window.MOLTBOT_UI_DEBUG) return true;
|
||||||
|
try {
|
||||||
|
return localStorage.getItem("moltbot-ui-debug") === "true";
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function debug(message: string, ...args: unknown[]): void {
|
||||||
|
if (isDebugEnabled()) {
|
||||||
|
console.log(`[DEBUG UI] ${message}`, ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -220,7 +220,9 @@ export class GatewayBrowserClient {
|
|||||||
this.opts.onHello?.(hello);
|
this.opts.onHello?.(hello);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
if (canFallbackToShared && deviceIdentity) {
|
// Always clear device auth token on connect failure when we have device identity.
|
||||||
|
// This handles stale tokens from previous gateway sessions that cause token_mismatch.
|
||||||
|
if (deviceIdentity) {
|
||||||
clearDeviceAuthToken({ deviceId: deviceIdentity.deviceId, role });
|
clearDeviceAuthToken({ deviceId: deviceIdentity.deviceId, role });
|
||||||
}
|
}
|
||||||
this.ws?.close(CONNECT_FAILED_CLOSE_CODE, "connect failed");
|
this.ws?.close(CONNECT_FAILED_CLOSE_CODE, "connect failed");
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export type UiSettings = {
|
|||||||
splitRatio: number; // Sidebar split ratio (0.4 to 0.7, default 0.6)
|
splitRatio: number; // Sidebar split ratio (0.4 to 0.7, default 0.6)
|
||||||
navCollapsed: boolean; // Collapsible sidebar state
|
navCollapsed: boolean; // Collapsible sidebar state
|
||||||
navGroupsCollapsed: Record<string, boolean>; // Which nav groups are collapsed
|
navGroupsCollapsed: Record<string, boolean>; // Which nav groups are collapsed
|
||||||
|
debugLogs: boolean; // Enable verbose debug logging in browser console
|
||||||
};
|
};
|
||||||
|
|
||||||
export function loadSettings(): UiSettings {
|
export function loadSettings(): UiSettings {
|
||||||
@ -32,6 +33,7 @@ export function loadSettings(): UiSettings {
|
|||||||
splitRatio: 0.6,
|
splitRatio: 0.6,
|
||||||
navCollapsed: false,
|
navCollapsed: false,
|
||||||
navGroupsCollapsed: {},
|
navGroupsCollapsed: {},
|
||||||
|
debugLogs: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -84,6 +86,10 @@ export function loadSettings(): UiSettings {
|
|||||||
parsed.navGroupsCollapsed !== null
|
parsed.navGroupsCollapsed !== null
|
||||||
? parsed.navGroupsCollapsed
|
? parsed.navGroupsCollapsed
|
||||||
: defaults.navGroupsCollapsed,
|
: defaults.navGroupsCollapsed,
|
||||||
|
debugLogs:
|
||||||
|
typeof parsed.debugLogs === "boolean"
|
||||||
|
? parsed.debugLogs
|
||||||
|
: defaults.debugLogs,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return defaults;
|
return defaults;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user