@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.
@@ -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?.label || sessionRow?.key || "Session",
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(sessionRow?.label || sessionRow?.key || "Session")}
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 { kDestinationSessionFilter } from "../../lib/session-keys.js";
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.label || sessionRow.key}
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?.label || "Pick a session in the sidebar"}
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 && (sending || streaming)
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-bubble-meta">
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
- ? "Type a message..."
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 || streaming ? "Sending..." : "Send"}
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 { getSessionRowKey } from "../lib/session-keys.js";
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(sessionRow?.label || getSessionRowKey(sessionRow) || "Session")}
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
- ChatVoiceLineIcon,
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 { readUiSettings, writeUiSettings } from "../lib/ui-settings.js";
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
- <${ChatVoiceLineIcon} className="sidebar-tab-icon" />
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
- : chatSessions.map(
323
- (sessionRow) => html`
324
- <button
325
- key=${sessionRow.key}
326
- class=${`sidebar-chat-session-item ${selectedChatSessionKey === sessionRow.key ? "active" : ""}`}
327
- onclick=${() => onSelectChatSession(sessionRow.key)}
328
- title=${sessionRow.label || sessionRow.key}
329
- >
330
- <span class="sidebar-chat-session-name"
331
- >${sessionRow.label || sessionRow.key}</span
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
- </button>
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 { getSessionRowKey } from "../../../lib/session-keys.js";
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?.label ||
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
- export const kAgentSessionsCacheKey = "alphaclaw.agent.sessionsCache";
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 ---