Skip to content

Commit e3ff8c4

Browse files
committed
refactor(ui): split app modules
1 parent ce59e2d commit e3ff8c4

10 files changed

Lines changed: 1193 additions & 836 deletions

ui/src/ui/app-chat.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { abortChatRun, loadChatHistory, sendChatMessage } from "./controllers/chat";
2+
import { loadSessions } from "./controllers/sessions";
3+
import { generateUUID } from "./uuid";
4+
import { resetToolStream } from "./app-tool-stream";
5+
import { scheduleChatScroll } from "./app-scroll";
6+
import { setLastActiveSessionKey } from "./app-settings";
7+
import type { ClawdbotApp } from "./app";
8+
9+
type ChatHost = {
10+
connected: boolean;
11+
chatMessage: string;
12+
chatQueue: Array<{ id: string; text: string; createdAt: number }>;
13+
chatRunId: string | null;
14+
chatSending: boolean;
15+
sessionKey: string;
16+
};
17+
18+
export function isChatBusy(host: ChatHost) {
19+
return host.chatSending || Boolean(host.chatRunId);
20+
}
21+
22+
export function isChatStopCommand(text: string) {
23+
const trimmed = text.trim();
24+
if (!trimmed) return false;
25+
const normalized = trimmed.toLowerCase();
26+
if (normalized === "/stop") return true;
27+
return (
28+
normalized === "stop" ||
29+
normalized === "esc" ||
30+
normalized === "abort" ||
31+
normalized === "wait" ||
32+
normalized === "exit"
33+
);
34+
}
35+
36+
export async function handleAbortChat(host: ChatHost) {
37+
if (!host.connected) return;
38+
host.chatMessage = "";
39+
await abortChatRun(host as unknown as ClawdbotApp);
40+
}
41+
42+
function enqueueChatMessage(host: ChatHost, text: string) {
43+
const trimmed = text.trim();
44+
if (!trimmed) return;
45+
host.chatQueue = [
46+
...host.chatQueue,
47+
{
48+
id: generateUUID(),
49+
text: trimmed,
50+
createdAt: Date.now(),
51+
},
52+
];
53+
}
54+
55+
async function sendChatMessageNow(
56+
host: ChatHost,
57+
message: string,
58+
opts?: { previousDraft?: string; restoreDraft?: boolean },
59+
) {
60+
resetToolStream(host as unknown as Parameters<typeof resetToolStream>[0]);
61+
const ok = await sendChatMessage(host as unknown as ClawdbotApp, message);
62+
if (!ok && opts?.previousDraft != null) {
63+
host.chatMessage = opts.previousDraft;
64+
}
65+
if (ok) {
66+
setLastActiveSessionKey(host as unknown as Parameters<typeof setLastActiveSessionKey>[0], host.sessionKey);
67+
}
68+
if (ok && opts?.restoreDraft && opts.previousDraft?.trim()) {
69+
host.chatMessage = opts.previousDraft;
70+
}
71+
scheduleChatScroll(host as unknown as Parameters<typeof scheduleChatScroll>[0]);
72+
if (ok && !host.chatRunId) {
73+
void flushChatQueue(host);
74+
}
75+
return ok;
76+
}
77+
78+
async function flushChatQueue(host: ChatHost) {
79+
if (!host.connected || isChatBusy(host)) return;
80+
const [next, ...rest] = host.chatQueue;
81+
if (!next) return;
82+
host.chatQueue = rest;
83+
const ok = await sendChatMessageNow(host, next.text);
84+
if (!ok) {
85+
host.chatQueue = [next, ...host.chatQueue];
86+
}
87+
}
88+
89+
export function removeQueuedMessage(host: ChatHost, id: string) {
90+
host.chatQueue = host.chatQueue.filter((item) => item.id !== id);
91+
}
92+
93+
export async function handleSendChat(
94+
host: ChatHost,
95+
messageOverride?: string,
96+
opts?: { restoreDraft?: boolean },
97+
) {
98+
if (!host.connected) return;
99+
const previousDraft = host.chatMessage;
100+
const message = (messageOverride ?? host.chatMessage).trim();
101+
if (!message) return;
102+
103+
if (isChatStopCommand(message)) {
104+
await handleAbortChat(host);
105+
return;
106+
}
107+
108+
if (messageOverride == null) {
109+
host.chatMessage = "";
110+
}
111+
112+
if (isChatBusy(host)) {
113+
enqueueChatMessage(host, message);
114+
return;
115+
}
116+
117+
await sendChatMessageNow(host, message, {
118+
previousDraft: messageOverride == null ? previousDraft : undefined,
119+
restoreDraft: Boolean(messageOverride && opts?.restoreDraft),
120+
});
121+
}
122+
123+
export async function refreshChat(host: ChatHost) {
124+
await Promise.all([
125+
loadChatHistory(host as unknown as ClawdbotApp),
126+
loadSessions(host as unknown as ClawdbotApp),
127+
]);
128+
scheduleChatScroll(host as unknown as Parameters<typeof scheduleChatScroll>[0], true);
129+
}
130+
131+
export const flushChatQueueForEvent = flushChatQueue;

ui/src/ui/app-connections.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {
2+
loadChannels,
3+
logoutWhatsApp,
4+
saveDiscordConfig,
5+
saveIMessageConfig,
6+
saveSlackConfig,
7+
saveSignalConfig,
8+
saveTelegramConfig,
9+
startWhatsAppLogin,
10+
waitWhatsAppLogin,
11+
} from "./controllers/connections";
12+
import { loadConfig } from "./controllers/config";
13+
import type { ClawdbotApp } from "./app";
14+
15+
export async function handleWhatsAppStart(host: ClawdbotApp, force: boolean) {
16+
await startWhatsAppLogin(host, force);
17+
await loadChannels(host, true);
18+
}
19+
20+
export async function handleWhatsAppWait(host: ClawdbotApp) {
21+
await waitWhatsAppLogin(host);
22+
await loadChannels(host, true);
23+
}
24+
25+
export async function handleWhatsAppLogout(host: ClawdbotApp) {
26+
await logoutWhatsApp(host);
27+
await loadChannels(host, true);
28+
}
29+
30+
export async function handleTelegramSave(host: ClawdbotApp) {
31+
await saveTelegramConfig(host);
32+
await loadConfig(host);
33+
await loadChannels(host, true);
34+
}
35+
36+
export async function handleDiscordSave(host: ClawdbotApp) {
37+
await saveDiscordConfig(host);
38+
await loadConfig(host);
39+
await loadChannels(host, true);
40+
}
41+
42+
export async function handleSlackSave(host: ClawdbotApp) {
43+
await saveSlackConfig(host);
44+
await loadConfig(host);
45+
await loadChannels(host, true);
46+
}
47+
48+
export async function handleSignalSave(host: ClawdbotApp) {
49+
await saveSignalConfig(host);
50+
await loadConfig(host);
51+
await loadChannels(host, true);
52+
}
53+
54+
export async function handleIMessageSave(host: ClawdbotApp) {
55+
await saveIMessageConfig(host);
56+
await loadConfig(host);
57+
await loadChannels(host, true);
58+
}

ui/src/ui/app-defaults.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { LogLevel } from "./types";
2+
import type { CronFormState } from "./ui-types";
3+
4+
export const DEFAULT_LOG_LEVEL_FILTERS: Record<LogLevel, boolean> = {
5+
trace: true,
6+
debug: true,
7+
info: true,
8+
warn: true,
9+
error: true,
10+
fatal: true,
11+
};
12+
13+
export const DEFAULT_CRON_FORM: CronFormState = {
14+
name: "",
15+
description: "",
16+
agentId: "",
17+
enabled: true,
18+
scheduleKind: "every",
19+
scheduleAt: "",
20+
everyAmount: "30",
21+
everyUnit: "minutes",
22+
cronExpr: "0 7 * * *",
23+
cronTz: "",
24+
sessionTarget: "main",
25+
wakeMode: "next-heartbeat",
26+
payloadKind: "systemEvent",
27+
payloadText: "",
28+
deliver: false,
29+
channel: "last",
30+
to: "",
31+
timeoutSeconds: "",
32+
postToMainPrefix: "",
33+
};

ui/src/ui/app-gateway.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { loadChatHistory } from "./controllers/chat";
2+
import { loadNodes } from "./controllers/nodes";
3+
import type { GatewayEventFrame, GatewayHelloOk } from "./gateway";
4+
import { GatewayBrowserClient } from "./gateway";
5+
import type { EventLogEntry } from "./app-events";
6+
import type { PresenceEntry, HealthSnapshot, StatusSummary } from "./types";
7+
import type { Tab } from "./navigation";
8+
import { handleAgentEvent, resetToolStream, type AgentEventPayload } from "./app-tool-stream";
9+
import { flushChatQueueForEvent } from "./app-chat";
10+
import { loadCron, refreshActiveTab, setLastActiveSessionKey } from "./app-settings";
11+
import { handleChatEvent, type ChatEventPayload } from "./controllers/chat";
12+
import type { ClawdbotApp } from "./app";
13+
14+
type GatewayHost = {
15+
settings: { gatewayUrl: string; token: string };
16+
password: string;
17+
client: GatewayBrowserClient | null;
18+
connected: boolean;
19+
hello: GatewayHelloOk | null;
20+
lastError: string | null;
21+
eventLogBuffer: EventLogEntry[];
22+
eventLog: EventLogEntry[];
23+
tab: Tab;
24+
presenceEntries: PresenceEntry[];
25+
presenceError: string | null;
26+
presenceStatus: StatusSummary | null;
27+
debugHealth: HealthSnapshot | null;
28+
sessionKey: string;
29+
chatRunId: string | null;
30+
};
31+
32+
export function connectGateway(host: GatewayHost) {
33+
host.lastError = null;
34+
host.hello = null;
35+
host.connected = false;
36+
37+
host.client?.stop();
38+
host.client = new GatewayBrowserClient({
39+
url: host.settings.gatewayUrl,
40+
token: host.settings.token.trim() ? host.settings.token : undefined,
41+
password: host.password.trim() ? host.password : undefined,
42+
clientName: "clawdbot-control-ui",
43+
mode: "webchat",
44+
onHello: (hello) => {
45+
host.connected = true;
46+
host.hello = hello;
47+
applySnapshot(host, hello);
48+
void loadNodes(host as unknown as ClawdbotApp, { quiet: true });
49+
void refreshActiveTab(host as unknown as Parameters<typeof refreshActiveTab>[0]);
50+
},
51+
onClose: ({ code, reason }) => {
52+
host.connected = false;
53+
host.lastError = `disconnected (${code}): ${reason || "no reason"}`;
54+
},
55+
onEvent: (evt) => handleGatewayEvent(host, evt),
56+
onGap: ({ expected, received }) => {
57+
host.lastError = `event gap detected (expected seq ${expected}, got ${received}); refresh recommended`;
58+
},
59+
});
60+
host.client.start();
61+
}
62+
63+
export function handleGatewayEvent(host: GatewayHost, evt: GatewayEventFrame) {
64+
host.eventLogBuffer = [
65+
{ ts: Date.now(), event: evt.event, payload: evt.payload },
66+
...host.eventLogBuffer,
67+
].slice(0, 250);
68+
if (host.tab === "debug") {
69+
host.eventLog = host.eventLogBuffer;
70+
}
71+
72+
if (evt.event === "agent") {
73+
handleAgentEvent(
74+
host as unknown as Parameters<typeof handleAgentEvent>[0],
75+
evt.payload as AgentEventPayload | undefined,
76+
);
77+
return;
78+
}
79+
80+
if (evt.event === "chat") {
81+
const payload = evt.payload as ChatEventPayload | undefined;
82+
if (payload?.sessionKey) {
83+
setLastActiveSessionKey(
84+
host as unknown as Parameters<typeof setLastActiveSessionKey>[0],
85+
payload.sessionKey,
86+
);
87+
}
88+
const state = handleChatEvent(host as unknown as ClawdbotApp, payload);
89+
if (state === "final" || state === "error" || state === "aborted") {
90+
resetToolStream(host as unknown as Parameters<typeof resetToolStream>[0]);
91+
void flushChatQueueForEvent(
92+
host as unknown as Parameters<typeof flushChatQueueForEvent>[0],
93+
);
94+
}
95+
if (state === "final") void loadChatHistory(host as unknown as ClawdbotApp);
96+
return;
97+
}
98+
99+
if (evt.event === "presence") {
100+
const payload = evt.payload as { presence?: PresenceEntry[] } | undefined;
101+
if (payload?.presence && Array.isArray(payload.presence)) {
102+
host.presenceEntries = payload.presence;
103+
host.presenceError = null;
104+
host.presenceStatus = null;
105+
}
106+
return;
107+
}
108+
109+
if (evt.event === "cron" && host.tab === "cron") {
110+
void loadCron(host as unknown as Parameters<typeof loadCron>[0]);
111+
}
112+
}
113+
114+
export function applySnapshot(host: GatewayHost, hello: GatewayHelloOk) {
115+
const snapshot = hello.snapshot as
116+
| { presence?: PresenceEntry[]; health?: HealthSnapshot }
117+
| undefined;
118+
if (snapshot?.presence && Array.isArray(snapshot.presence)) {
119+
host.presenceEntries = snapshot.presence;
120+
}
121+
if (snapshot?.health) {
122+
host.debugHealth = snapshot.health;
123+
}
124+
}

0 commit comments

Comments
 (0)