@chrysb/alphaclaw 0.4.6-beta.1 → 0.4.6-beta.3

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.
Files changed (28) hide show
  1. package/lib/public/css/explorer.css +4 -7
  2. package/lib/public/css/shell.css +14 -2
  3. package/lib/public/css/theme.css +4 -0
  4. package/lib/public/js/app.js +62 -38
  5. package/lib/public/js/components/doctor/findings-list.js +190 -12
  6. package/lib/public/js/components/doctor/fix-card-modal.js +20 -73
  7. package/lib/public/js/components/doctor/helpers.js +7 -27
  8. package/lib/public/js/components/doctor/index.js +5 -5
  9. package/lib/public/js/components/file-tree.js +1 -1
  10. package/lib/public/js/components/file-viewer/constants.js +4 -3
  11. package/lib/public/js/components/file-viewer/editor-surface.js +1 -0
  12. package/lib/public/js/components/file-viewer/index.js +4 -0
  13. package/lib/public/js/components/file-viewer/storage.js +1 -4
  14. package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +130 -17
  15. package/lib/public/js/components/file-viewer/use-file-viewer.js +4 -0
  16. package/lib/public/js/components/google/gmail-setup-wizard.js +18 -51
  17. package/lib/public/js/components/google/gmail-watch-toggle.js +4 -1
  18. package/lib/public/js/components/onboarding/use-welcome-storage.js +2 -1
  19. package/lib/public/js/components/telegram-workspace/index.js +5 -2
  20. package/lib/public/js/hooks/useAgentSessions.js +128 -0
  21. package/lib/public/js/lib/browse-draft-state.js +9 -13
  22. package/lib/public/js/lib/storage-keys.js +28 -0
  23. package/lib/public/js/lib/ui-settings.js +3 -1
  24. package/lib/server/doctor/normalize.js +57 -23
  25. package/lib/server/doctor/prompt.js +30 -4
  26. package/lib/server/doctor/service.js +46 -0
  27. package/lib/server/doctor/workspace-fingerprint.js +46 -6
  28. package/package.json +1 -1
@@ -43,7 +43,7 @@ const DoctorEmptyStateIcon = () => html`
43
43
  </svg>
44
44
  `;
45
45
 
46
- export const DoctorTab = ({ isActive = false }) => {
46
+ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
47
47
  const statusPoll = usePolling(fetchDoctorStatus, kIdlePollMs, {
48
48
  enabled: isActive,
49
49
  });
@@ -327,10 +327,8 @@ export const DoctorTab = ({ isActive = false }) => {
327
327
  })}
328
328
  </span>
329
329
  </span>
330
- <span
331
- class=${`text-xs ${changeLabel.meaningful ? "text-yellow-300" : "text-gray-500"}`}
332
- >
333
- ${changeLabel.text}
330
+ <span class="text-xs text-gray-500">
331
+ ${changeLabel}
334
332
  </span>
335
333
  </div>
336
334
  `
@@ -481,6 +479,8 @@ export const DoctorTab = ({ isActive = false }) => {
481
479
  busyCardId=${busyCardId}
482
480
  onAskAgentFix=${setFixCard}
483
481
  onUpdateStatus=${handleUpdateStatus}
482
+ onOpenFile=${onOpenFile}
483
+ changedPaths=${doctorStatus?.changeSummary?.changedPaths || []}
484
484
  showRunMeta=${selectedRunFilter === "all"}
485
485
  hideEmptyState=${selectedRunIsInProgress}
486
486
  />
@@ -52,7 +52,7 @@ const kTreeIndentPx = 9;
52
52
  const kFolderBasePaddingPx = 10;
53
53
  const kFileBasePaddingPx = 14;
54
54
  const kTreeRefreshIntervalMs = 5000;
55
- const kExpandedFoldersStorageKey = "alphaclaw.browse.expandedFolders";
55
+ import { kExpandedFoldersStorageKey } from "../lib/storage-keys.js";
56
56
 
57
57
  const readStoredExpandedPaths = () => {
58
58
  try {
@@ -1,6 +1,7 @@
1
- export const kFileViewerModeStorageKey = "alphaclaw.browse.fileViewerMode";
2
- export const kLegacyFileViewerModeStorageKey = "alphaclawBrowseFileViewerMode";
3
- export const kEditorSelectionStorageKey = "alphaclaw.browse.editorSelectionByPath";
1
+ export {
2
+ kFileViewerModeStorageKey,
3
+ kEditorSelectionStorageKey,
4
+ } from "../../lib/storage-keys.js";
4
5
  export const kLoadingIndicatorDelayMs = 1000;
5
6
  export const kFileRefreshIntervalMs = 5000;
6
7
  export const kSqlitePageSize = 50;
@@ -63,6 +63,7 @@ export const EditorSurface = ({
63
63
  <div
64
64
  class="file-viewer-editor-line-num"
65
65
  key=${lineNumber}
66
+ data-line-row
66
67
  ref=${(element) => {
67
68
  editorLineNumberRowRefs.current[lineNumber - 1] = element;
68
69
  }}
@@ -20,6 +20,8 @@ export const FileViewer = ({
20
20
  filePath = "",
21
21
  isPreviewOnly = false,
22
22
  browseView = "edit",
23
+ lineTarget = 0,
24
+ lineEndTarget = 0,
23
25
  onRequestEdit = () => {},
24
26
  onRequestClearSelection = () => {},
25
27
  }) => {
@@ -28,6 +30,8 @@ export const FileViewer = ({
28
30
  filePath,
29
31
  isPreviewOnly,
30
32
  browseView,
33
+ lineTarget,
34
+ lineEndTarget,
31
35
  onRequestClearSelection,
32
36
  onRequestEdit,
33
37
  });
@@ -1,15 +1,12 @@
1
1
  import {
2
2
  kEditorSelectionStorageKey,
3
3
  kFileViewerModeStorageKey,
4
- kLegacyFileViewerModeStorageKey,
5
4
  } from "./constants.js";
6
5
 
7
6
  export const readStoredFileViewerMode = () => {
8
7
  try {
9
8
  const storedMode = String(
10
- window.localStorage.getItem(kFileViewerModeStorageKey) ||
11
- window.localStorage.getItem(kLegacyFileViewerModeStorageKey) ||
12
- "",
9
+ window.localStorage.getItem(kFileViewerModeStorageKey) || "",
13
10
  ).trim();
14
11
  return storedMode === "preview" ? "preview" : "edit";
15
12
  } catch {
@@ -1,8 +1,58 @@
1
- import { useEffect } from "https://esm.sh/preact/hooks";
1
+ import { useEffect, useRef } from "https://esm.sh/preact/hooks";
2
2
  import { readStoredEditorSelection } from "./storage.js";
3
3
  import { clampSelectionIndex } from "./utils.js";
4
4
  import { getScrollRatio } from "./scroll-sync.js";
5
5
 
6
+ const getCharOffsetForLine = (text, lineNumber) => {
7
+ const lines = String(text || "").split("\n");
8
+ const targetIndex = Math.max(0, Math.min(lineNumber - 1, lines.length - 1));
9
+ let offset = 0;
10
+ for (let i = 0; i < targetIndex; i += 1) offset += lines[i].length + 1;
11
+ return offset;
12
+ };
13
+
14
+ const scrollEditorToLine = ({
15
+ lineIndex,
16
+ textareaElement,
17
+ editorLineNumbersRef,
18
+ editorHighlightRef,
19
+ viewScrollRatioRef,
20
+ }) => {
21
+ const computedStyle = window.getComputedStyle(textareaElement);
22
+ const parsedLineHeight = Number.parseFloat(computedStyle.lineHeight || "");
23
+ const lineHeight =
24
+ Number.isFinite(parsedLineHeight) && parsedLineHeight > 0 ? parsedLineHeight : 20;
25
+ const nextScrollTop = Math.max(
26
+ 0,
27
+ lineIndex * lineHeight - textareaElement.clientHeight * 0.4,
28
+ );
29
+ textareaElement.scrollTop = nextScrollTop;
30
+ if (editorLineNumbersRef.current) {
31
+ editorLineNumbersRef.current.scrollTop = nextScrollTop;
32
+ }
33
+ if (editorHighlightRef.current) {
34
+ editorHighlightRef.current.scrollTop = nextScrollTop;
35
+ }
36
+ viewScrollRatioRef.current = getScrollRatio(textareaElement);
37
+ };
38
+
39
+ const clearLineHighlights = (lineNumbersContainer) => {
40
+ if (!lineNumbersContainer) return;
41
+ const highlighted = lineNumbersContainer.querySelectorAll(".line-highlight-flash");
42
+ for (const row of highlighted) row.classList.remove("line-highlight-flash");
43
+ };
44
+
45
+ const highlightLineRange = (lineNumbersContainer, startIndex, endIndex) => {
46
+ if (!lineNumbersContainer) return;
47
+ clearLineHighlights(lineNumbersContainer);
48
+ const rows = lineNumbersContainer.querySelectorAll("[data-line-row]");
49
+ const safeEnd = Math.min(endIndex, rows.length - 1);
50
+ for (let i = startIndex; i <= safeEnd; i += 1) {
51
+ const row = rows[i];
52
+ if (row) row.classList.add("line-highlight-flash");
53
+ }
54
+ };
55
+
6
56
  export const useEditorSelectionRestore = ({
7
57
  canEditFile,
8
58
  isEditBlocked,
@@ -13,11 +63,81 @@ export const useEditorSelectionRestore = ({
13
63
  restoredSelectionPathRef,
14
64
  viewMode,
15
65
  content,
66
+ lineTarget = 0,
67
+ lineEndTarget = 0,
16
68
  editorTextareaRef,
17
69
  editorLineNumbersRef,
18
70
  editorHighlightRef,
19
71
  viewScrollRatioRef,
20
72
  }) => {
73
+ const appliedLineTargetRef = useRef("");
74
+
75
+ useEffect(() => {
76
+ if (lineTarget && lineTarget >= 1) return;
77
+ if (!appliedLineTargetRef.current) return;
78
+ clearLineHighlights(editorLineNumbersRef.current);
79
+ appliedLineTargetRef.current = "";
80
+ }, [lineTarget, normalizedPath, editorLineNumbersRef]);
81
+
82
+ useEffect(() => {
83
+ if (isEditBlocked || !canEditFile || loading || !hasSelectedPath) return () => {};
84
+ if (loadedFilePathRef.current !== normalizedPath) return () => {};
85
+ if (viewMode !== "edit") return () => {};
86
+ if (!lineTarget || lineTarget < 1) return () => {};
87
+ const effectiveEnd = lineEndTarget && lineEndTarget >= lineTarget ? lineEndTarget : lineTarget;
88
+ const lineKey = `${normalizedPath}:${lineTarget}-${effectiveEnd}`;
89
+ if (appliedLineTargetRef.current === lineKey) return () => {};
90
+ let frameId = 0;
91
+ let attempts = 0;
92
+ const applyLineTarget = () => {
93
+ const textareaElement = editorTextareaRef.current;
94
+ if (!textareaElement) {
95
+ attempts += 1;
96
+ if (attempts < 6) frameId = window.requestAnimationFrame(applyLineTarget);
97
+ return;
98
+ }
99
+ const safeContent = String(content || "");
100
+ const charOffset = getCharOffsetForLine(safeContent, lineTarget);
101
+ textareaElement.setSelectionRange(charOffset, charOffset);
102
+ const startIndex = lineTarget - 1;
103
+ const endIndex = effectiveEnd - 1;
104
+ window.requestAnimationFrame(() => {
105
+ const nextTextareaElement = editorTextareaRef.current;
106
+ if (!nextTextareaElement) return;
107
+ scrollEditorToLine({
108
+ lineIndex: startIndex,
109
+ textareaElement: nextTextareaElement,
110
+ editorLineNumbersRef,
111
+ editorHighlightRef,
112
+ viewScrollRatioRef,
113
+ });
114
+ highlightLineRange(editorLineNumbersRef.current, startIndex, endIndex);
115
+ });
116
+ appliedLineTargetRef.current = lineKey;
117
+ restoredSelectionPathRef.current = normalizedPath;
118
+ };
119
+ frameId = window.requestAnimationFrame(applyLineTarget);
120
+ return () => {
121
+ if (frameId) window.cancelAnimationFrame(frameId);
122
+ };
123
+ }, [
124
+ canEditFile,
125
+ isEditBlocked,
126
+ loading,
127
+ hasSelectedPath,
128
+ normalizedPath,
129
+ content,
130
+ viewMode,
131
+ lineTarget,
132
+ lineEndTarget,
133
+ loadedFilePathRef,
134
+ restoredSelectionPathRef,
135
+ editorTextareaRef,
136
+ editorLineNumbersRef,
137
+ editorHighlightRef,
138
+ viewScrollRatioRef,
139
+ ]);
140
+
21
141
  useEffect(() => {
22
142
  if (isEditBlocked) {
23
143
  restoredSelectionPathRef.current = "";
@@ -27,6 +147,7 @@ export const useEditorSelectionRestore = ({
27
147
  if (loadedFilePathRef.current !== normalizedPath) return () => {};
28
148
  if (restoredSelectionPathRef.current === normalizedPath) return () => {};
29
149
  if (viewMode !== "edit") return () => {};
150
+ if (lineTarget && lineTarget >= 1) return () => {};
30
151
  const storedSelection = readStoredEditorSelection(normalizedPath);
31
152
  if (!storedSelection) {
32
153
  restoredSelectionPathRef.current = normalizedPath;
@@ -52,22 +173,13 @@ export const useEditorSelectionRestore = ({
52
173
  const safeContent = String(content || "");
53
174
  const safeStart = clampSelectionIndex(start, safeContent.length);
54
175
  const lineIndex = safeContent.slice(0, safeStart).split("\n").length - 1;
55
- const computedStyle = window.getComputedStyle(nextTextareaElement);
56
- const parsedLineHeight = Number.parseFloat(computedStyle.lineHeight || "");
57
- const lineHeight =
58
- Number.isFinite(parsedLineHeight) && parsedLineHeight > 0 ? parsedLineHeight : 20;
59
- const nextScrollTop = Math.max(
60
- 0,
61
- lineIndex * lineHeight - nextTextareaElement.clientHeight * 0.4,
62
- );
63
- nextTextareaElement.scrollTop = nextScrollTop;
64
- if (editorLineNumbersRef.current) {
65
- editorLineNumbersRef.current.scrollTop = nextScrollTop;
66
- }
67
- if (editorHighlightRef.current) {
68
- editorHighlightRef.current.scrollTop = nextScrollTop;
69
- }
70
- viewScrollRatioRef.current = getScrollRatio(nextTextareaElement);
176
+ scrollEditorToLine({
177
+ lineIndex,
178
+ textareaElement: nextTextareaElement,
179
+ editorLineNumbersRef,
180
+ editorHighlightRef,
181
+ viewScrollRatioRef,
182
+ });
71
183
  });
72
184
  restoredSelectionPathRef.current = normalizedPath;
73
185
  };
@@ -83,6 +195,7 @@ export const useEditorSelectionRestore = ({
83
195
  normalizedPath,
84
196
  content,
85
197
  viewMode,
198
+ lineTarget,
86
199
  loadedFilePathRef,
87
200
  restoredSelectionPathRef,
88
201
  editorTextareaRef,
@@ -32,6 +32,8 @@ export const useFileViewer = ({
32
32
  filePath = "",
33
33
  isPreviewOnly = false,
34
34
  browseView = "edit",
35
+ lineTarget = 0,
36
+ lineEndTarget = 0,
35
37
  onRequestClearSelection = () => {},
36
38
  onRequestEdit = () => {},
37
39
  }) => {
@@ -242,6 +244,8 @@ export const useFileViewer = ({
242
244
  restoredSelectionPathRef,
243
245
  viewMode,
244
246
  content,
247
+ lineTarget,
248
+ lineEndTarget,
245
249
  editorTextareaRef,
246
250
  editorLineNumbersRef,
247
251
  editorHighlightRef,
@@ -1,12 +1,13 @@
1
1
  import { h } from "https://esm.sh/preact";
2
- import { useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
2
+ import { useCallback, useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
3
3
  import htm from "https://esm.sh/htm";
4
4
  import { ModalShell } from "../modal-shell.js";
5
5
  import { PageHeader } from "../page-header.js";
6
6
  import { CloseIcon } from "../icons.js";
7
7
  import { ActionButton } from "../action-button.js";
8
- import { fetchAgentSessions, sendAgentMessage } from "../../lib/api.js";
8
+ import { sendAgentMessage } from "../../lib/api.js";
9
9
  import { showToast } from "../toast.js";
10
+ import { useAgentSessions } from "../../hooks/useAgentSessions.js";
10
11
 
11
12
  const html = htm.bind(h);
12
13
 
@@ -45,6 +46,11 @@ const kStepTitles = [
45
46
  const kTotalSteps = kStepTitles.length;
46
47
  const kNoSessionSelectedValue = "__none__";
47
48
 
49
+ const kDirectOrGroupFilter = (sessionRow) => {
50
+ const key = String(sessionRow?.key || "").toLowerCase();
51
+ return key.includes(":direct:") || key.includes(":group:");
52
+ };
53
+
48
54
  const renderCommandBlock = (command = "", onCopy = () => {}) => html`
49
55
  <div class="rounded-lg border border-border bg-black/30 p-3">
50
56
  <pre
@@ -80,10 +86,14 @@ export const GmailSetupWizard = ({
80
86
  const [watchEnabled, setWatchEnabled] = useState(false);
81
87
  const [sendingToAgent, setSendingToAgent] = useState(false);
82
88
  const [agentMessageSent, setAgentMessageSent] = useState(false);
83
- const [agentSessions, setAgentSessions] = useState([]);
84
- const [selectedSessionKey, setSelectedSessionKey] = useState("");
85
- const [loadingAgentSessions, setLoadingAgentSessions] = useState(false);
86
- const [agentSessionsError, setAgentSessionsError] = useState("");
89
+
90
+ const {
91
+ sessions: selectableAgentSessions,
92
+ selectedSessionKey,
93
+ setSelectedSessionKey,
94
+ loading: loadingAgentSessions,
95
+ error: agentSessionsError,
96
+ } = useAgentSessions({ enabled: visible, filter: kDirectOrGroupFilter });
87
97
 
88
98
  useEffect(() => {
89
99
  if (!visible) return;
@@ -94,41 +104,6 @@ export const GmailSetupWizard = ({
94
104
  setWatchEnabled(false);
95
105
  setSendingToAgent(false);
96
106
  setAgentMessageSent(false);
97
- setAgentSessions([]);
98
- setSelectedSessionKey("");
99
- setLoadingAgentSessions(false);
100
- setAgentSessionsError("");
101
- }, [visible, account?.id]);
102
-
103
- useEffect(() => {
104
- if (!visible) return;
105
- let active = true;
106
- const loadAgentSessions = async () => {
107
- try {
108
- setLoadingAgentSessions(true);
109
- setAgentSessionsError("");
110
- const data = await fetchAgentSessions();
111
- if (!active) return;
112
- const sessions = Array.isArray(data?.sessions) ? data.sessions : [];
113
- setAgentSessions(sessions);
114
- const defaultSession = sessions.find((sessionRow) => {
115
- const key = String(sessionRow?.key || "").toLowerCase();
116
- return key.includes(":direct:") || key.includes(":group:");
117
- });
118
- setSelectedSessionKey((currentKey) => currentKey || String(defaultSession?.key || ""));
119
- } catch (err) {
120
- if (!active) return;
121
- setAgentSessions([]);
122
- setSelectedSessionKey("");
123
- setAgentSessionsError(err.message || "Could not load sessions");
124
- } finally {
125
- if (active) setLoadingAgentSessions(false);
126
- }
127
- };
128
- loadAgentSessions();
129
- return () => {
130
- active = false;
131
- };
132
107
  }, [visible, account?.id]);
133
108
 
134
109
  const commands = clientConfig?.commands || null;
@@ -150,23 +125,15 @@ export const GmailSetupWizard = ({
150
125
  }
151
126
  return true;
152
127
  }, [needsProjectId, projectIdInput]);
153
- const selectableAgentSessions = useMemo(
154
- () =>
155
- agentSessions.filter((sessionRow) => {
156
- const key = String(sessionRow?.key || "").toLowerCase();
157
- return key.includes(":direct:") || key.includes(":group:");
158
- }),
159
- [agentSessions],
160
- );
161
128
 
162
- const handleCopy = async (value) => {
129
+ const handleCopy = useCallback(async (value) => {
163
130
  const ok = await copyText(value);
164
131
  if (ok) {
165
132
  showToast("Copied to clipboard", "success");
166
133
  return;
167
134
  }
168
135
  showToast("Could not copy text", "error");
169
- };
136
+ }, []);
170
137
 
171
138
  const handleFinish = async () => {
172
139
  try {
@@ -7,7 +7,10 @@ import { InfoTooltip } from "../info-tooltip.js";
7
7
  const html = htm.bind(h);
8
8
 
9
9
  const resolveWatchState = ({ watchStatus, busy = false }) => {
10
- if (busy) return { label: "Starting", tone: "warning" };
10
+ if (busy) {
11
+ const label = watchStatus?.enabled ? "Stopping" : "Starting";
12
+ return { label, tone: "warning" };
13
+ }
11
14
  if (!watchStatus?.enabled) return { label: "Stopped", tone: "neutral" };
12
15
  if (watchStatus.enabled && !watchStatus.running)
13
16
  return { label: "Error", tone: "danger" };
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useState } from "https://esm.sh/preact/hooks";
2
2
 
3
- export const kOnboardingStorageKey = "openclaw_setup";
3
+ import { kOnboardingStorageKey } from "../../lib/storage-keys.js";
4
+ export { kOnboardingStorageKey };
4
5
  export const kOnboardingStepKey = "_step";
5
6
  export const kPairingChannelKey = "_pairingChannel";
6
7
  export const kOnboardingSetupErrorKey = "_lastSetupError";
@@ -23,8 +23,11 @@ const kSteps = [
23
23
  { id: "summary", label: "Summary" },
24
24
  ];
25
25
 
26
- const kTelegramWorkspaceStorageKey = "telegram-workspace-state-v1";
27
- const kTelegramWorkspaceCacheKey = "telegram-workspace-cache-v1";
26
+ import {
27
+ kTelegramWorkspaceStorageKey,
28
+ kTelegramWorkspaceCacheKey,
29
+ } from "../../lib/storage-keys.js";
30
+
28
31
  const loadTelegramWorkspaceState = () => {
29
32
  try {
30
33
  const raw = window.localStorage.getItem(kTelegramWorkspaceStorageKey);
@@ -0,0 +1,128 @@
1
+ import { useState, useEffect, useMemo, useCallback } from "https://esm.sh/preact/hooks";
2
+ import { fetchAgentSessions } from "../lib/api.js";
3
+ import {
4
+ kAgentSessionsCacheKey,
5
+ kAgentLastSessionKey,
6
+ } from "../lib/storage-keys.js";
7
+
8
+ const readCachedSessions = () => {
9
+ try {
10
+ const raw = localStorage.getItem(kAgentSessionsCacheKey);
11
+ if (!raw) return [];
12
+ const parsed = JSON.parse(raw);
13
+ return Array.isArray(parsed) ? parsed : [];
14
+ } catch {
15
+ return [];
16
+ }
17
+ };
18
+
19
+ const writeCachedSessions = (sessions) => {
20
+ try {
21
+ localStorage.setItem(kAgentSessionsCacheKey, JSON.stringify(sessions));
22
+ } catch {}
23
+ };
24
+
25
+ const readLastSessionKey = () => {
26
+ try {
27
+ return localStorage.getItem(kAgentLastSessionKey) || "";
28
+ } catch {
29
+ return "";
30
+ }
31
+ };
32
+
33
+ const writeLastSessionKey = (key) => {
34
+ try {
35
+ localStorage.setItem(kAgentLastSessionKey, String(key || ""));
36
+ } catch {}
37
+ };
38
+
39
+ const pickPreferredSession = (sessions, lastKey) => {
40
+ if (lastKey) {
41
+ const lastMatch = sessions.find((row) => String(row?.key || "") === lastKey);
42
+ if (lastMatch) return lastMatch;
43
+ }
44
+ return (
45
+ sessions.find((row) => String(row?.key || "").toLowerCase() === "agent:main:main") ||
46
+ sessions.find((row) => {
47
+ const key = String(row?.key || "").toLowerCase();
48
+ return key.includes(":direct:") || key.includes(":group:");
49
+ }) ||
50
+ sessions[0] ||
51
+ null
52
+ );
53
+ };
54
+
55
+ /**
56
+ * Shared hook for agent session selection with localStorage caching.
57
+ *
58
+ * @param {object} options
59
+ * @param {boolean} options.enabled - Whether to load sessions (tie to modal visibility, etc.)
60
+ * @param {(sessions: Array) => Array} [options.filter] - Optional filter applied to the session list before exposing it.
61
+ * @returns {{ sessions, selectedSessionKey, setSelectedSessionKey, selectedSession, loading, error }}
62
+ */
63
+ export const useAgentSessions = ({ enabled = false, filter } = {}) => {
64
+ const [allSessions, setAllSessions] = useState([]);
65
+ const [selectedSessionKey, setSelectedSessionKeyState] = useState("");
66
+ const [loading, setLoading] = useState(false);
67
+ const [error, setError] = useState("");
68
+
69
+ const setSelectedSessionKey = useCallback((key) => {
70
+ const normalized = String(key || "");
71
+ setSelectedSessionKeyState(normalized);
72
+ writeLastSessionKey(normalized);
73
+ }, []);
74
+
75
+ useEffect(() => {
76
+ if (!enabled) return;
77
+ let active = true;
78
+
79
+ const cached = readCachedSessions();
80
+ const lastKey = readLastSessionKey();
81
+ if (cached.length > 0) {
82
+ setAllSessions(cached);
83
+ const preferred = pickPreferredSession(cached, lastKey);
84
+ setSelectedSessionKeyState(String(preferred?.key || ""));
85
+ }
86
+
87
+ const load = async () => {
88
+ try {
89
+ if (cached.length === 0) setLoading(true);
90
+ setError("");
91
+ const data = await fetchAgentSessions();
92
+ if (!active) return;
93
+ const nextSessions = Array.isArray(data?.sessions) ? data.sessions : [];
94
+ setAllSessions(nextSessions);
95
+ writeCachedSessions(nextSessions);
96
+ if (cached.length === 0 || !lastKey) {
97
+ const preferred = pickPreferredSession(nextSessions, lastKey);
98
+ setSelectedSessionKeyState(String(preferred?.key || ""));
99
+ }
100
+ } catch (err) {
101
+ if (!active) return;
102
+ if (cached.length === 0) {
103
+ setAllSessions([]);
104
+ setSelectedSessionKeyState("");
105
+ setError(err.message || "Could not load agent sessions");
106
+ }
107
+ } finally {
108
+ if (active) setLoading(false);
109
+ }
110
+ };
111
+ load();
112
+ return () => {
113
+ active = false;
114
+ };
115
+ }, [enabled]);
116
+
117
+ const sessions = useMemo(
118
+ () => (filter ? allSessions.filter(filter) : allSessions),
119
+ [allSessions, filter],
120
+ );
121
+
122
+ const selectedSession = useMemo(
123
+ () => sessions.find((row) => String(row?.key || "") === selectedSessionKey) || null,
124
+ [sessions, selectedSessionKey],
125
+ );
126
+
127
+ return { sessions, selectedSessionKey, setSelectedSessionKey, selectedSession, loading, error };
128
+ };
@@ -1,20 +1,21 @@
1
- export const kFileDraftStorageKeyPrefix = "alphaclaw.browse.draft.";
2
- export const kLegacyFileDraftStorageKeyPrefix = "alphaclawBrowseDraft:";
3
- export const kDraftIndexStorageKey = "alphaclaw.draftIndex";
1
+ import {
2
+ kFileDraftStorageKeyPrefix,
3
+ kDraftIndexStorageKey,
4
+ } from "./storage-keys.js";
5
+
6
+ export { kFileDraftStorageKeyPrefix, kDraftIndexStorageKey };
4
7
  export const kDraftIndexChangedEventName = "alphaclaw:browse-draft-index-changed";
5
8
 
6
9
  const getStorage = (storage) => storage || window.localStorage;
7
10
 
8
- export const getFileDraftStorageKey = (filePath, useLegacyPrefix = false) =>
9
- `${useLegacyPrefix ? kLegacyFileDraftStorageKeyPrefix : kFileDraftStorageKeyPrefix}${String(filePath || "").trim()}`;
11
+ export const getFileDraftStorageKey = (filePath) =>
12
+ `${kFileDraftStorageKeyPrefix}${String(filePath || "").trim()}`;
10
13
 
11
14
  export const readStoredFileDraft = (filePath, storage) => {
12
15
  try {
13
16
  if (!filePath) return "";
14
17
  const localStorage = getStorage(storage);
15
- const draft =
16
- localStorage.getItem(getFileDraftStorageKey(filePath)) ||
17
- localStorage.getItem(getFileDraftStorageKey(filePath, true));
18
+ const draft = localStorage.getItem(getFileDraftStorageKey(filePath));
18
19
  return typeof draft === "string" ? draft : "";
19
20
  } catch {
20
21
  return "";
@@ -34,7 +35,6 @@ export const clearStoredFileDraft = (filePath, storage) => {
34
35
  if (!filePath) return;
35
36
  const localStorage = getStorage(storage);
36
37
  localStorage.removeItem(getFileDraftStorageKey(filePath));
37
- localStorage.removeItem(getFileDraftStorageKey(filePath, true));
38
38
  } catch {}
39
39
  };
40
40
 
@@ -94,10 +94,6 @@ export const readStoredDraftPaths = (storage) => {
94
94
  const path = key.slice(kFileDraftStorageKeyPrefix.length).trim();
95
95
  if (path) nextDraftPaths.add(path);
96
96
  }
97
- if (key.startsWith(kLegacyFileDraftStorageKeyPrefix)) {
98
- const path = key.slice(kLegacyFileDraftStorageKeyPrefix.length).trim();
99
- if (path) nextDraftPaths.add(path);
100
- }
101
97
  }
102
98
  if (nextDraftPaths.size > 0) {
103
99
  writeDraftIndex(nextDraftPaths, { storage: localStorage });
@@ -0,0 +1,28 @@
1
+ // Centralized localStorage key registry.
2
+ // All standalone localStorage keys used by the Setup UI should be defined here
3
+ // so they stay discoverable, consistently prefixed, and free of collisions.
4
+ //
5
+ // Naming convention: "alphaclaw.<area>.<purpose>"
6
+
7
+ // --- UI settings (single JSON blob containing sub-keys) ---
8
+ export const kUiSettingsStorageKey = "alphaclaw.ui.settings";
9
+
10
+ // --- Browse / file viewer ---
11
+ export const kFileViewerModeStorageKey = "alphaclaw.browse.viewerMode";
12
+ export const kEditorSelectionStorageKey = "alphaclaw.browse.editorSelection";
13
+ export const kExpandedFoldersStorageKey = "alphaclaw.browse.expandedFolders";
14
+
15
+ // --- Browse / drafts ---
16
+ export const kFileDraftStorageKeyPrefix = "alphaclaw.browse.draft.";
17
+ export const kDraftIndexStorageKey = "alphaclaw.browse.draftIndex";
18
+
19
+ // --- Onboarding ---
20
+ export const kOnboardingStorageKey = "alphaclaw.onboarding.state";
21
+
22
+ // --- Telegram workspace ---
23
+ export const kTelegramWorkspaceStorageKey = "alphaclaw.telegram.workspaceState";
24
+ export const kTelegramWorkspaceCacheKey = "alphaclaw.telegram.workspaceCache";
25
+
26
+ // --- Agent sessions (shared across session pickers) ---
27
+ export const kAgentSessionsCacheKey = "alphaclaw.agent.sessionsCache";
28
+ export const kAgentLastSessionKey = "alphaclaw.agent.lastSessionKey";
@@ -1,4 +1,6 @@
1
- export const kUiSettingsStorageKey = "alphaclaw.uiSettings";
1
+ import { kUiSettingsStorageKey } from "./storage-keys.js";
2
+
3
+ export { kUiSettingsStorageKey };
2
4
 
3
5
  const parseSettings = (rawValue) => {
4
6
  if (!rawValue) return {};