@chrysb/alphaclaw 0.8.3-beta.3 → 0.8.3-beta.5
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/lib/public/css/chat.css +8 -4
- package/lib/public/css/explorer.css +65 -22
- package/lib/public/dist/app.bundle.js +1830 -1801
- package/lib/public/js/app.js +0 -7
- package/lib/public/js/components/cron-tab/cron-job-settings-card.js +9 -2
- package/lib/public/js/components/google/gmail-setup-wizard.js +6 -2
- package/lib/public/js/components/icons.js +13 -0
- package/lib/public/js/components/routes/chat-route.js +57 -29
- package/lib/public/js/components/session-select-field.js +9 -2
- package/lib/public/js/components/sidebar.js +128 -25
- package/lib/public/js/components/webhooks/webhook-detail/index.js +5 -2
- package/lib/public/js/lib/session-keys.js +74 -0
- package/lib/public/js/lib/storage-keys.js +2 -1
- package/lib/server/chat-ws.js +93 -7
- package/lib/server/routes/system.js +36 -85
- package/package.json +1 -1
package/lib/public/js/app.js
CHANGED
|
@@ -217,13 +217,6 @@ const App = () => {
|
|
|
217
217
|
setSelectedChatSessionKey(sessionKey);
|
|
218
218
|
if (!isChatRoute) setLocation("/chat");
|
|
219
219
|
}}
|
|
220
|
-
onStartChat=${() => {
|
|
221
|
-
if (!isChatRoute) {
|
|
222
|
-
setLocation("/chat");
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
window.dispatchEvent(new Event("alphaclaw:chat-new"));
|
|
226
|
-
}}
|
|
227
220
|
/>
|
|
228
221
|
<div
|
|
229
222
|
class=${`sidebar-resizer ${shellState.isResizingSidebar ? "is-resizing" : ""}`}
|
|
@@ -4,6 +4,7 @@ import htm from "htm";
|
|
|
4
4
|
import { ActionButton } from "../action-button.js";
|
|
5
5
|
import { SegmentedControl } from "../segmented-control.js";
|
|
6
6
|
import { ToggleSwitch } from "../toggle-switch.js";
|
|
7
|
+
import { getSessionDisplayLabel } from "../../lib/session-keys.js";
|
|
7
8
|
import {
|
|
8
9
|
formatCronScheduleLabel,
|
|
9
10
|
formatNextRunRelativeMs,
|
|
@@ -89,7 +90,9 @@ export const CronJobSettingsCard = ({
|
|
|
89
90
|
if (!key) return;
|
|
90
91
|
if (key === selectedKey) selectedPresent = true;
|
|
91
92
|
const label = String(
|
|
92
|
-
sessionRow
|
|
93
|
+
getSessionDisplayLabel(sessionRow) ||
|
|
94
|
+
sessionRow?.key ||
|
|
95
|
+
"Session",
|
|
93
96
|
).trim();
|
|
94
97
|
const dedupeKey = label.toLowerCase();
|
|
95
98
|
if (seenLabels.has(dedupeKey)) return;
|
|
@@ -193,7 +196,11 @@ export const CronJobSettingsCard = ({
|
|
|
193
196
|
${deliverySessionOptions.map(
|
|
194
197
|
(sessionRow) => html`
|
|
195
198
|
<option value=${String(sessionRow?.key || "")}>
|
|
196
|
-
${String(
|
|
199
|
+
${String(
|
|
200
|
+
getSessionDisplayLabel(sessionRow) ||
|
|
201
|
+
sessionRow?.key ||
|
|
202
|
+
"Session",
|
|
203
|
+
)}
|
|
197
204
|
</option>
|
|
198
205
|
`,
|
|
199
206
|
)}
|
|
@@ -18,7 +18,10 @@ import {
|
|
|
18
18
|
kNoDestinationSessionValue,
|
|
19
19
|
useDestinationSessionSelection,
|
|
20
20
|
} from "../../hooks/use-destination-session-selection.js";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
getSessionDisplayLabel,
|
|
23
|
+
kDestinationSessionFilter,
|
|
24
|
+
} from "../../lib/session-keys.js";
|
|
22
25
|
|
|
23
26
|
const html = htm.bind(h);
|
|
24
27
|
|
|
@@ -419,7 +422,8 @@ export const GmailSetupWizard = ({
|
|
|
419
422
|
${selectableAgentSessions.map(
|
|
420
423
|
(sessionRow) => html`
|
|
421
424
|
<option value=${sessionRow.key}>
|
|
422
|
-
${sessionRow
|
|
425
|
+
${getSessionDisplayLabel(sessionRow) ||
|
|
426
|
+
sessionRow.key}
|
|
423
427
|
</option>
|
|
424
428
|
`,
|
|
425
429
|
)}
|
|
@@ -208,6 +208,19 @@ export const ChatVoiceLineIcon = ({ className = "" }) => html`
|
|
|
208
208
|
</svg>
|
|
209
209
|
`;
|
|
210
210
|
|
|
211
|
+
export const Chat4LineIcon = ({ className = "" }) => html`
|
|
212
|
+
<svg
|
|
213
|
+
class=${className}
|
|
214
|
+
viewBox="0 0 24 24"
|
|
215
|
+
fill="currentColor"
|
|
216
|
+
aria-hidden="true"
|
|
217
|
+
>
|
|
218
|
+
<path
|
|
219
|
+
d="M5.76282 17H20V5H4V18.3851L5.76282 17ZM6.45455 19L2 22.5V4C2 3.44772 2.44772 3 3 3H21C21.5523 3 22 3.44772 22 4V18C22 18.5523 21.5523 19 21 19H6.45455Z"
|
|
220
|
+
/>
|
|
221
|
+
</svg>
|
|
222
|
+
`;
|
|
223
|
+
|
|
211
224
|
export const FileMusicLineIcon = ({ className = "" }) => html`
|
|
212
225
|
<svg
|
|
213
226
|
class=${className}
|
|
@@ -2,6 +2,7 @@ import { h } from "preact";
|
|
|
2
2
|
import {
|
|
3
3
|
useCallback,
|
|
4
4
|
useEffect,
|
|
5
|
+
useLayoutEffect,
|
|
5
6
|
useMemo,
|
|
6
7
|
useRef,
|
|
7
8
|
useState,
|
|
@@ -10,13 +11,27 @@ import htm from "htm";
|
|
|
10
11
|
import { marked } from "marked";
|
|
11
12
|
import { authFetch } from "../../lib/api.js";
|
|
12
13
|
import { kChatSessionDraftsStorageKey } from "../../lib/storage-keys.js";
|
|
14
|
+
import { getSessionDisplayLabel } from "../../lib/session-keys.js";
|
|
13
15
|
import { showToast } from "../toast.js";
|
|
14
16
|
|
|
15
17
|
const html = htm.bind(h);
|
|
16
|
-
const kNewChatEventName = "alphaclaw:chat-new";
|
|
17
18
|
const kWsReconnectMaxAttempts = 8;
|
|
18
19
|
const kAutoscrollBottomThresholdPx = 40;
|
|
19
20
|
const kChatDebugQueryFlag = "chatDebug";
|
|
21
|
+
const kComposerMaxLines = 5;
|
|
22
|
+
const kComposerFontSizePx = 12;
|
|
23
|
+
const kComposerLineHeight = 1.4;
|
|
24
|
+
const kComposerPaddingYPx = 20;
|
|
25
|
+
|
|
26
|
+
const resizeComposerTextarea = (element) => {
|
|
27
|
+
if (!element) return;
|
|
28
|
+
const linePx = kComposerFontSizePx * kComposerLineHeight;
|
|
29
|
+
const minH = linePx + kComposerPaddingYPx;
|
|
30
|
+
const maxH = linePx * kComposerMaxLines + kComposerPaddingYPx;
|
|
31
|
+
element.style.height = "auto";
|
|
32
|
+
const next = Math.min(Math.max(element.scrollHeight, minH), maxH);
|
|
33
|
+
element.style.height = `${next}px`;
|
|
34
|
+
};
|
|
20
35
|
|
|
21
36
|
const buildMessage = ({
|
|
22
37
|
role = "assistant",
|
|
@@ -174,8 +189,10 @@ export const ChatRoute = ({ sessions = [], selectedSessionKey = "" }) => {
|
|
|
174
189
|
const [activeRunBySession, setActiveRunBySession] = useState({});
|
|
175
190
|
const [connectionError, setConnectionError] = useState("");
|
|
176
191
|
const [historyLoading, setHistoryLoading] = useState(false);
|
|
192
|
+
const [assistantStreamStarted, setAssistantStreamStarted] = useState(false);
|
|
177
193
|
const wsRef = useRef(null);
|
|
178
194
|
const threadRef = useRef(null);
|
|
195
|
+
const composerRef = useRef(null);
|
|
179
196
|
const reconnectTimerRef = useRef(null);
|
|
180
197
|
const reconnectAttemptsRef = useRef(0);
|
|
181
198
|
const selectedSessionKeyRef = useRef(selectedSessionKey);
|
|
@@ -206,6 +223,14 @@ export const ChatRoute = ({ sessions = [], selectedSessionKey = "" }) => {
|
|
|
206
223
|
selectedSessionKeyRef.current = selectedSessionKey;
|
|
207
224
|
}, [selectedSessionKey]);
|
|
208
225
|
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
setAssistantStreamStarted(false);
|
|
228
|
+
}, [selectedSessionKey]);
|
|
229
|
+
|
|
230
|
+
useLayoutEffect(() => {
|
|
231
|
+
resizeComposerTextarea(composerRef.current);
|
|
232
|
+
}, [draft, selectedSessionKey]);
|
|
233
|
+
|
|
209
234
|
useEffect(() => {
|
|
210
235
|
if (!selectedSessionKey) return;
|
|
211
236
|
setDraft(String(draftBySession[selectedSessionKey] || ""));
|
|
@@ -242,25 +267,6 @@ export const ChatRoute = ({ sessions = [], selectedSessionKey = "" }) => {
|
|
|
242
267
|
[messagesBySession, selectedSessionKey],
|
|
243
268
|
);
|
|
244
269
|
|
|
245
|
-
useEffect(() => {
|
|
246
|
-
const handleNewChat = () => {
|
|
247
|
-
if (!selectedSessionKey) return;
|
|
248
|
-
setMessagesBySession((currentMap) => ({
|
|
249
|
-
...currentMap,
|
|
250
|
-
[selectedSessionKey]: [],
|
|
251
|
-
}));
|
|
252
|
-
setDraft("");
|
|
253
|
-
setDraftBySession((currentMap) => ({
|
|
254
|
-
...currentMap,
|
|
255
|
-
[selectedSessionKey]: "",
|
|
256
|
-
}));
|
|
257
|
-
};
|
|
258
|
-
window.addEventListener(kNewChatEventName, handleNewChat);
|
|
259
|
-
return () => {
|
|
260
|
-
window.removeEventListener(kNewChatEventName, handleNewChat);
|
|
261
|
-
};
|
|
262
|
-
}, [selectedSessionKey]);
|
|
263
|
-
|
|
264
270
|
useEffect(() => {
|
|
265
271
|
let mounted = true;
|
|
266
272
|
|
|
@@ -294,6 +300,7 @@ export const ChatRoute = ({ sessions = [], selectedSessionKey = "" }) => {
|
|
|
294
300
|
setIsConnected(false);
|
|
295
301
|
setStreaming(false);
|
|
296
302
|
setSending(false);
|
|
303
|
+
setAssistantStreamStarted(false);
|
|
297
304
|
setHistoryLoading(false);
|
|
298
305
|
if (realtimeDisabledRef.current) return;
|
|
299
306
|
if (reconnectAttemptsRef.current >= kWsReconnectMaxAttempts) return;
|
|
@@ -368,6 +375,7 @@ export const ChatRoute = ({ sessions = [], selectedSessionKey = "" }) => {
|
|
|
368
375
|
if (!chunkSessionKey || !messageId) return;
|
|
369
376
|
setSending(false);
|
|
370
377
|
setStreaming(true);
|
|
378
|
+
setAssistantStreamStarted(true);
|
|
371
379
|
setMessagesBySession((currentMap) => {
|
|
372
380
|
const currentMessages = currentMap[chunkSessionKey] || [];
|
|
373
381
|
const lastMessage = currentMessages[currentMessages.length - 1];
|
|
@@ -424,6 +432,8 @@ export const ChatRoute = ({ sessions = [], selectedSessionKey = "" }) => {
|
|
|
424
432
|
payload.sessionKey || selectedSessionKeyRef.current || "",
|
|
425
433
|
);
|
|
426
434
|
if (!toolSessionKey) return;
|
|
435
|
+
setSending(false);
|
|
436
|
+
setAssistantStreamStarted(true);
|
|
427
437
|
const toolPhase = String(payload.phase || "").toLowerCase();
|
|
428
438
|
const toolCall =
|
|
429
439
|
payload?.toolCall && typeof payload.toolCall === "object"
|
|
@@ -552,6 +562,7 @@ export const ChatRoute = ({ sessions = [], selectedSessionKey = "" }) => {
|
|
|
552
562
|
);
|
|
553
563
|
const runId = String(payload.runId || "");
|
|
554
564
|
if (!nextSessionKey || !runId) return;
|
|
565
|
+
setSending(false);
|
|
555
566
|
setActiveRunBySession((currentMap) => ({
|
|
556
567
|
...currentMap,
|
|
557
568
|
[nextSessionKey]: runId,
|
|
@@ -572,6 +583,7 @@ export const ChatRoute = ({ sessions = [], selectedSessionKey = "" }) => {
|
|
|
572
583
|
}
|
|
573
584
|
setSending(false);
|
|
574
585
|
setStreaming(false);
|
|
586
|
+
setAssistantStreamStarted(false);
|
|
575
587
|
setHistoryLoading(false);
|
|
576
588
|
if (doneSessionKey && ws && ws.readyState === 1) {
|
|
577
589
|
setHistoryLoading(true);
|
|
@@ -592,6 +604,7 @@ export const ChatRoute = ({ sessions = [], selectedSessionKey = "" }) => {
|
|
|
592
604
|
if (payload.type === "error") {
|
|
593
605
|
setSending(false);
|
|
594
606
|
setStreaming(false);
|
|
607
|
+
setAssistantStreamStarted(false);
|
|
595
608
|
setHistoryLoading(false);
|
|
596
609
|
const errorSessionKey = String(
|
|
597
610
|
payload.sessionKey || selectedSessionKeyRef.current || "",
|
|
@@ -791,6 +804,7 @@ export const ChatRoute = ({ sessions = [], selectedSessionKey = "" }) => {
|
|
|
791
804
|
...currentMap,
|
|
792
805
|
[selectedSessionKey]: "",
|
|
793
806
|
}));
|
|
807
|
+
setAssistantStreamStarted(false);
|
|
794
808
|
setSending(true);
|
|
795
809
|
setMessagesBySession((currentMap) => ({
|
|
796
810
|
...currentMap,
|
|
@@ -829,8 +843,20 @@ export const ChatRoute = ({ sessions = [], selectedSessionKey = "" }) => {
|
|
|
829
843
|
});
|
|
830
844
|
setStreaming(false);
|
|
831
845
|
setSending(false);
|
|
846
|
+
setAssistantStreamStarted(false);
|
|
832
847
|
}, [appendDebugEvent, selectedSessionKey]);
|
|
833
848
|
|
|
849
|
+
const handleComposerKeyDown = useCallback(
|
|
850
|
+
(event) => {
|
|
851
|
+
if (event.key !== "Enter") return;
|
|
852
|
+
if (event.shiftKey) return;
|
|
853
|
+
if (event.isComposing) return;
|
|
854
|
+
event.preventDefault();
|
|
855
|
+
handleSend();
|
|
856
|
+
},
|
|
857
|
+
[handleSend],
|
|
858
|
+
);
|
|
859
|
+
|
|
834
860
|
const rawHistory = selectedSessionKey
|
|
835
861
|
? rawHistoryBySession[selectedSessionKey]
|
|
836
862
|
: null;
|
|
@@ -844,7 +870,8 @@ export const ChatRoute = ({ sessions = [], selectedSessionKey = "" }) => {
|
|
|
844
870
|
<div>
|
|
845
871
|
<div class="chat-route-title">Chat</div>
|
|
846
872
|
<div class="chat-route-subtitle">
|
|
847
|
-
${selectedSession
|
|
873
|
+
${getSessionDisplayLabel(selectedSession) ||
|
|
874
|
+
"Pick a session in the sidebar"}
|
|
848
875
|
</div>
|
|
849
876
|
${connectionError
|
|
850
877
|
? html`<div class="chat-route-warning">${connectionError}</div>`
|
|
@@ -1024,14 +1051,12 @@ ${JSON.stringify(
|
|
|
1024
1051
|
})()}
|
|
1025
1052
|
`,
|
|
1026
1053
|
)}
|
|
1027
|
-
${selectedSessionKey &&
|
|
1054
|
+
${selectedSessionKey &&
|
|
1055
|
+
(sending || streaming) &&
|
|
1056
|
+
!assistantStreamStarted
|
|
1028
1057
|
? html`
|
|
1029
1058
|
<div class="chat-bubble is-assistant chat-typing-indicator">
|
|
1030
|
-
<div class="chat-
|
|
1031
|
-
<span>Agent</span>
|
|
1032
|
-
<span>${isConnected ? "typing..." : "reconnecting..."}</span>
|
|
1033
|
-
</div>
|
|
1034
|
-
<div class="chat-typing-dots">
|
|
1059
|
+
<div class="chat-typing-dots" aria-hidden="true">
|
|
1035
1060
|
<span></span><span></span><span></span>
|
|
1036
1061
|
</div>
|
|
1037
1062
|
</div>
|
|
@@ -1055,12 +1080,15 @@ ${JSON.stringify(
|
|
|
1055
1080
|
<div class="chat-composer">
|
|
1056
1081
|
<textarea
|
|
1057
1082
|
class="chat-composer-input"
|
|
1083
|
+
ref=${composerRef}
|
|
1084
|
+
rows=${1}
|
|
1058
1085
|
placeholder=${selectedSessionKey
|
|
1059
|
-
? "
|
|
1086
|
+
? "Message… (Enter to send, Shift+Enter for newline)"
|
|
1060
1087
|
: "Select a session to start"}
|
|
1061
1088
|
value=${draft}
|
|
1062
1089
|
disabled=${!selectedSessionKey || sending || !isConnected}
|
|
1063
1090
|
oninput=${handleDraftInput}
|
|
1091
|
+
onkeydown=${handleComposerKeyDown}
|
|
1064
1092
|
></textarea>
|
|
1065
1093
|
<div class="chat-composer-actions">
|
|
1066
1094
|
${streaming
|
|
@@ -1085,7 +1113,7 @@ ${JSON.stringify(
|
|
|
1085
1113
|
!String(draft || "").trim()}
|
|
1086
1114
|
onclick=${handleSend}
|
|
1087
1115
|
>
|
|
1088
|
-
${sending
|
|
1116
|
+
${sending ? "Sending..." : "Send"}
|
|
1089
1117
|
</button>
|
|
1090
1118
|
</div>
|
|
1091
1119
|
</div>
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { h } from "preact";
|
|
2
2
|
import htm from "htm";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
getSessionDisplayLabel,
|
|
5
|
+
getSessionRowKey,
|
|
6
|
+
} from "../lib/session-keys.js";
|
|
4
7
|
|
|
5
8
|
const html = htm.bind(h);
|
|
6
9
|
|
|
@@ -54,7 +57,11 @@ export const SessionSelectField = ({
|
|
|
54
57
|
${sessions.map(
|
|
55
58
|
(sessionRow) => html`
|
|
56
59
|
<option value=${getSessionRowKey(sessionRow)}>
|
|
57
|
-
${String(
|
|
60
|
+
${String(
|
|
61
|
+
getSessionDisplayLabel(sessionRow) ||
|
|
62
|
+
getSessionRowKey(sessionRow) ||
|
|
63
|
+
"Session",
|
|
64
|
+
)}
|
|
58
65
|
</option>
|
|
59
66
|
`,
|
|
60
67
|
)}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { h } from "preact";
|
|
2
|
-
import { useEffect, useRef, useState } from "preact/hooks";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
|
3
3
|
import htm from "htm";
|
|
4
4
|
import {
|
|
5
5
|
AddLineIcon,
|
|
@@ -7,7 +7,8 @@ import {
|
|
|
7
7
|
BarChartLineIcon,
|
|
8
8
|
Brain2LineIcon,
|
|
9
9
|
BracesLineIcon,
|
|
10
|
-
|
|
10
|
+
Chat4LineIcon,
|
|
11
|
+
ChevronDownIcon,
|
|
11
12
|
ComputerLineIcon,
|
|
12
13
|
EyeLineIcon,
|
|
13
14
|
FolderLineIcon,
|
|
@@ -21,7 +22,17 @@ import { OverflowMenu, OverflowMenuItem } from "./overflow-menu.js";
|
|
|
21
22
|
import { UpdateActionButton } from "./update-action-button.js";
|
|
22
23
|
import { SidebarGitPanel } from "./sidebar-git-panel.js";
|
|
23
24
|
import { UpdateModal } from "./update-modal.js";
|
|
24
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
readUiSettings,
|
|
27
|
+
updateUiSettings,
|
|
28
|
+
writeUiSettings,
|
|
29
|
+
} from "../lib/ui-settings.js";
|
|
30
|
+
import {
|
|
31
|
+
getAgentIdFromSessionKey,
|
|
32
|
+
getSessionChannelForIcon,
|
|
33
|
+
getSessionDisplayLabel,
|
|
34
|
+
getSessionRowKey,
|
|
35
|
+
} from "../lib/session-keys.js";
|
|
25
36
|
|
|
26
37
|
const html = htm.bind(h);
|
|
27
38
|
const kBrowseBottomPanelUiSettingKey = "browseBottomPanelHeightPx";
|
|
@@ -29,6 +40,16 @@ const kBrowsePanelMinHeightPx = 120;
|
|
|
29
40
|
const kBrowseBottomMinHeightPx = 120;
|
|
30
41
|
const kBrowseResizerHeightPx = 6;
|
|
31
42
|
const kDefaultBrowseBottomPanelHeightPx = 260;
|
|
43
|
+
const kChatSidebarCollapsedAgentIdsKey = "chatSidebarCollapsedAgentIds";
|
|
44
|
+
const kChatChannelIconSrc = {
|
|
45
|
+
telegram: "/assets/icons/telegram.svg",
|
|
46
|
+
discord: "/assets/icons/discord.svg",
|
|
47
|
+
slack: "/assets/icons/slack.svg",
|
|
48
|
+
};
|
|
49
|
+
const readChatSidebarCollapsedAgentIds = () => {
|
|
50
|
+
const raw = readUiSettings()[kChatSidebarCollapsedAgentIdsKey];
|
|
51
|
+
return Array.isArray(raw) ? raw : [];
|
|
52
|
+
};
|
|
32
53
|
const kSidebarNavIconsById = {
|
|
33
54
|
cron: AlarmLineIcon,
|
|
34
55
|
usage: BarChartLineIcon,
|
|
@@ -97,7 +118,6 @@ export const AppSidebar = ({
|
|
|
97
118
|
chatSessions = [],
|
|
98
119
|
selectedChatSessionKey = "",
|
|
99
120
|
onSelectChatSession = () => {},
|
|
100
|
-
onStartChat = () => {},
|
|
101
121
|
}) => {
|
|
102
122
|
const browseLayoutRef = useRef(null);
|
|
103
123
|
const browseBottomPanelRef = useRef(null);
|
|
@@ -107,6 +127,53 @@ export const AppSidebar = ({
|
|
|
107
127
|
);
|
|
108
128
|
const [isResizingBrowsePanels, setIsResizingBrowsePanels] = useState(false);
|
|
109
129
|
const [updateModalOpen, setUpdateModalOpen] = useState(false);
|
|
130
|
+
const [collapsedChatAgentIds, setCollapsedChatAgentIds] = useState(() =>
|
|
131
|
+
readChatSidebarCollapsedAgentIds(),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const chatSessionGroups = useMemo(() => {
|
|
135
|
+
const rows = Array.isArray(chatSessions) ? chatSessions : [];
|
|
136
|
+
const order = [];
|
|
137
|
+
const byAgent = new Map();
|
|
138
|
+
for (const row of rows) {
|
|
139
|
+
const aid = String(
|
|
140
|
+
row.agentId ||
|
|
141
|
+
getAgentIdFromSessionKey(getSessionRowKey(row)) ||
|
|
142
|
+
"unknown",
|
|
143
|
+
);
|
|
144
|
+
if (!byAgent.has(aid)) {
|
|
145
|
+
byAgent.set(aid, {
|
|
146
|
+
agentId: aid,
|
|
147
|
+
agentLabel: String(row.agentLabel || "").trim() || aid,
|
|
148
|
+
sessions: [],
|
|
149
|
+
});
|
|
150
|
+
order.push(aid);
|
|
151
|
+
}
|
|
152
|
+
byAgent.get(aid).sessions.push(row);
|
|
153
|
+
}
|
|
154
|
+
const groups = order.map((aid) => byAgent.get(aid));
|
|
155
|
+
groups.sort((a, b) => {
|
|
156
|
+
if (a.agentId === "main" && b.agentId !== "main") return -1;
|
|
157
|
+
if (b.agentId === "main" && a.agentId !== "main") return 1;
|
|
158
|
+
return a.agentLabel.localeCompare(b.agentLabel);
|
|
159
|
+
});
|
|
160
|
+
return groups;
|
|
161
|
+
}, [chatSessions]);
|
|
162
|
+
|
|
163
|
+
const toggleChatAgentCollapsed = (agentId) => {
|
|
164
|
+
const id = String(agentId || "");
|
|
165
|
+
setCollapsedChatAgentIds((prev) => {
|
|
166
|
+
const next = new Set(prev);
|
|
167
|
+
if (next.has(id)) next.delete(id);
|
|
168
|
+
else next.add(id);
|
|
169
|
+
const arr = Array.from(next);
|
|
170
|
+
updateUiSettings((s) => ({
|
|
171
|
+
...s,
|
|
172
|
+
[kChatSidebarCollapsedAgentIdsKey]: arr,
|
|
173
|
+
}));
|
|
174
|
+
return arr;
|
|
175
|
+
});
|
|
176
|
+
};
|
|
110
177
|
|
|
111
178
|
useEffect(() => {
|
|
112
179
|
const settings = readUiSettings();
|
|
@@ -219,7 +286,7 @@ export const AppSidebar = ({
|
|
|
219
286
|
title="Chat"
|
|
220
287
|
onclick=${() => onSelectSidebarTab("chat")}
|
|
221
288
|
>
|
|
222
|
-
<${
|
|
289
|
+
<${Chat4LineIcon} className="sidebar-tab-icon" />
|
|
223
290
|
</button>
|
|
224
291
|
</div>
|
|
225
292
|
<div
|
|
@@ -306,31 +373,67 @@ export const AppSidebar = ({
|
|
|
306
373
|
>
|
|
307
374
|
<div class="sidebar-chat-header">
|
|
308
375
|
<div class="sidebar-label sidebar-chat-label">Sessions</div>
|
|
309
|
-
<button
|
|
310
|
-
type="button"
|
|
311
|
-
class="sidebar-chat-new-button"
|
|
312
|
-
onclick=${onStartChat}
|
|
313
|
-
title="New chat"
|
|
314
|
-
aria-label="New chat"
|
|
315
|
-
>
|
|
316
|
-
<${AddLineIcon} className="sidebar-chat-new-icon" />
|
|
317
|
-
</button>
|
|
318
376
|
</div>
|
|
319
377
|
<div class="sidebar-chat-sessions-list">
|
|
320
378
|
${chatSessions.length === 0
|
|
321
379
|
? html`<div class="sidebar-chat-empty">No sessions found</div>`
|
|
322
|
-
:
|
|
323
|
-
(
|
|
324
|
-
<
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
380
|
+
: chatSessionGroups.map(
|
|
381
|
+
(group) => html`
|
|
382
|
+
<div key=${group.agentId} class="sidebar-chat-agent-group">
|
|
383
|
+
<button
|
|
384
|
+
type="button"
|
|
385
|
+
class="sidebar-chat-agent-toggle"
|
|
386
|
+
onclick=${() => toggleChatAgentCollapsed(group.agentId)}
|
|
387
|
+
aria-expanded=${!collapsedChatAgentIds.includes(
|
|
388
|
+
group.agentId,
|
|
389
|
+
)}
|
|
332
390
|
>
|
|
333
|
-
|
|
391
|
+
<span
|
|
392
|
+
class=${`sidebar-chat-agent-chevron ${collapsedChatAgentIds.includes(group.agentId) ? "is-collapsed" : ""}`}
|
|
393
|
+
aria-hidden="true"
|
|
394
|
+
>
|
|
395
|
+
<${ChevronDownIcon} className="sidebar-chat-agent-chevron-icon" />
|
|
396
|
+
</span>
|
|
397
|
+
<span class="sidebar-chat-agent-label">${group.agentLabel}</span>
|
|
398
|
+
</button>
|
|
399
|
+
${collapsedChatAgentIds.includes(group.agentId)
|
|
400
|
+
? null
|
|
401
|
+
: html`
|
|
402
|
+
<div class="sidebar-chat-agent-sessions">
|
|
403
|
+
${group.sessions.map((sessionRow) => {
|
|
404
|
+
const displayLabel = getSessionDisplayLabel(sessionRow);
|
|
405
|
+
const channelIconSrc =
|
|
406
|
+
kChatChannelIconSrc[
|
|
407
|
+
String(
|
|
408
|
+
getSessionChannelForIcon(sessionRow) || "",
|
|
409
|
+
).toLowerCase()
|
|
410
|
+
] || "";
|
|
411
|
+
return html`
|
|
412
|
+
<button
|
|
413
|
+
key=${sessionRow.key}
|
|
414
|
+
class=${`sidebar-chat-session-item ${selectedChatSessionKey === sessionRow.key ? "active" : ""}`}
|
|
415
|
+
onclick=${() =>
|
|
416
|
+
onSelectChatSession(sessionRow.key)}
|
|
417
|
+
title=${displayLabel}
|
|
418
|
+
>
|
|
419
|
+
${channelIconSrc
|
|
420
|
+
? html`<img
|
|
421
|
+
src=${channelIconSrc}
|
|
422
|
+
alt=""
|
|
423
|
+
width="12"
|
|
424
|
+
height="12"
|
|
425
|
+
class="sidebar-chat-session-channel-icon"
|
|
426
|
+
/>`
|
|
427
|
+
: null}
|
|
428
|
+
<span class="sidebar-chat-session-name"
|
|
429
|
+
>${displayLabel}</span
|
|
430
|
+
>
|
|
431
|
+
</button>
|
|
432
|
+
`;
|
|
433
|
+
})}
|
|
434
|
+
</div>
|
|
435
|
+
`}
|
|
436
|
+
</div>
|
|
334
437
|
`,
|
|
335
438
|
)}
|
|
336
439
|
</div>
|
|
@@ -6,7 +6,10 @@ import { Badge } from "../../badge.js";
|
|
|
6
6
|
import { ConfirmDialog } from "../../confirm-dialog.js";
|
|
7
7
|
import { showToast } from "../../toast.js";
|
|
8
8
|
import { kNoDestinationSessionValue } from "../../../hooks/use-destination-session-selection.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
getSessionDisplayLabel,
|
|
11
|
+
getSessionRowKey,
|
|
12
|
+
} from "../../../lib/session-keys.js";
|
|
10
13
|
import { formatDateTime } from "../helpers.js";
|
|
11
14
|
import { RequestHistory } from "../request-history/index.js";
|
|
12
15
|
import { useWebhookDetail } from "./use-webhook-detail.js";
|
|
@@ -267,7 +270,7 @@ export const WebhookDetail = ({
|
|
|
267
270
|
(sessionRow) => html`
|
|
268
271
|
<option value=${getSessionRowKey(sessionRow)}>
|
|
269
272
|
${String(
|
|
270
|
-
sessionRow
|
|
273
|
+
getSessionDisplayLabel(sessionRow) ||
|
|
271
274
|
getSessionRowKey(sessionRow) ||
|
|
272
275
|
"Session",
|
|
273
276
|
)}
|
|
@@ -55,3 +55,77 @@ export const getDestinationFromSession = (sessionRow = null) => {
|
|
|
55
55
|
...(agentId ? { agentId } : {}),
|
|
56
56
|
};
|
|
57
57
|
};
|
|
58
|
+
|
|
59
|
+
/** Matches server `parseChannelFromSessionKey` for icon routing when `channel` is absent (cached rows). */
|
|
60
|
+
export const parseChannelFromSessionKey = (sessionKey = "") => {
|
|
61
|
+
const k = String(sessionKey || "");
|
|
62
|
+
if (k.includes(":telegram:")) return "telegram";
|
|
63
|
+
if (k.includes(":discord:")) return "discord";
|
|
64
|
+
if (k.includes(":slack:")) return "slack";
|
|
65
|
+
return "";
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const getTopicIdsFromSessionKey = (sessionKey = "") => {
|
|
69
|
+
const normalizedSessionKey = getNormalizedSessionKey(sessionKey);
|
|
70
|
+
const topicMatch = normalizedSessionKey.match(
|
|
71
|
+
/:telegram:group:([^:]+):topic:([^:]+)$/,
|
|
72
|
+
);
|
|
73
|
+
return {
|
|
74
|
+
groupId: String(topicMatch?.[1] || "").trim(),
|
|
75
|
+
topicId: String(topicMatch?.[2] || "").trim(),
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const getSessionKind = (sessionKey = "") => {
|
|
80
|
+
const normalizedSessionKey = getNormalizedSessionKey(sessionKey);
|
|
81
|
+
if (!normalizedSessionKey) return "other";
|
|
82
|
+
if (normalizedSessionKey === "main" || normalizedSessionKey.endsWith(":main")) {
|
|
83
|
+
return "main";
|
|
84
|
+
}
|
|
85
|
+
if (/:telegram:group:([^:]+):topic:([^:]+)$/.test(normalizedSessionKey)) {
|
|
86
|
+
return "topic";
|
|
87
|
+
}
|
|
88
|
+
if (normalizedSessionKey.includes(":slash:")) return "slash";
|
|
89
|
+
if (normalizedSessionKey.includes(":subagent:")) return "subagent";
|
|
90
|
+
if (/:direct:([^:]+)$/.test(normalizedSessionKey)) return "direct";
|
|
91
|
+
return "other";
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const getSessionDisplayLabel = (sessionRow = null) => {
|
|
95
|
+
const key = getSessionRowKey(sessionRow);
|
|
96
|
+
const kind = getSessionKind(key);
|
|
97
|
+
if (kind === "main") return "Main Thread";
|
|
98
|
+
|
|
99
|
+
const doctorMatch = key.match(/(?:^|:)doctor:(\d+)$/);
|
|
100
|
+
if (doctorMatch) return `Doctor Run #${doctorMatch[1]}`;
|
|
101
|
+
if (/(?:^|:)doctor(?::|$)/.test(key)) return "Doctor Run";
|
|
102
|
+
|
|
103
|
+
if (kind === "topic") {
|
|
104
|
+
const { groupId, topicId } = getTopicIdsFromSessionKey(key);
|
|
105
|
+
const topicName = String(sessionRow?.topicName || "").trim();
|
|
106
|
+
const groupName = String(sessionRow?.groupName || "").trim();
|
|
107
|
+
const topicLabel = topicName || (topicId ? `Topic ${topicId}` : "Topic");
|
|
108
|
+
const groupLabel = groupName || groupId;
|
|
109
|
+
return groupLabel ? `${topicLabel} - ${groupLabel}` : topicLabel;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (kind === "direct") {
|
|
113
|
+
const directMatch = key.match(/:direct:([^:]+)$/);
|
|
114
|
+
const directTarget = String(directMatch?.[1] || "").trim();
|
|
115
|
+
if (parseChannelFromSessionKey(key) === "telegram") {
|
|
116
|
+
return "Direct message";
|
|
117
|
+
}
|
|
118
|
+
return directTarget ? `Direct ${directTarget}` : "Direct";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return key || "Session";
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** Channel id for platform icons; prefers API `channel`, else parses from key / replyChannel. */
|
|
125
|
+
export const getSessionChannelForIcon = (sessionRow = null) => {
|
|
126
|
+
const fromRow = String(sessionRow?.channel || "").trim();
|
|
127
|
+
if (fromRow) return fromRow;
|
|
128
|
+
const fromReply = String(sessionRow?.replyChannel || "").trim();
|
|
129
|
+
if (fromReply) return fromReply;
|
|
130
|
+
return parseChannelFromSessionKey(getSessionRowKey(sessionRow));
|
|
131
|
+
};
|
|
@@ -24,7 +24,8 @@ export const kTelegramWorkspaceStorageKey = "alphaclaw.telegram.workspaceState";
|
|
|
24
24
|
export const kTelegramWorkspaceCacheKey = "alphaclaw.telegram.workspaceCache";
|
|
25
25
|
|
|
26
26
|
// --- Agent sessions (shared across session pickers) ---
|
|
27
|
-
|
|
27
|
+
// Bump version when session row shape changes so stale cache is not reused.
|
|
28
|
+
export const kAgentSessionsCacheKey = "alphaclaw.agent.sessionsCache.v3";
|
|
28
29
|
export const kAgentLastSessionKey = "alphaclaw.agent.lastSessionKey";
|
|
29
30
|
|
|
30
31
|
// --- Chat ---
|