@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.
- package/lib/public/css/explorer.css +4 -7
- package/lib/public/css/shell.css +14 -2
- package/lib/public/css/theme.css +4 -0
- package/lib/public/js/app.js +62 -38
- package/lib/public/js/components/doctor/findings-list.js +190 -12
- package/lib/public/js/components/doctor/fix-card-modal.js +20 -73
- package/lib/public/js/components/doctor/helpers.js +7 -27
- package/lib/public/js/components/doctor/index.js +5 -5
- package/lib/public/js/components/file-tree.js +1 -1
- package/lib/public/js/components/file-viewer/constants.js +4 -3
- package/lib/public/js/components/file-viewer/editor-surface.js +1 -0
- package/lib/public/js/components/file-viewer/index.js +4 -0
- package/lib/public/js/components/file-viewer/storage.js +1 -4
- package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +130 -17
- package/lib/public/js/components/file-viewer/use-file-viewer.js +4 -0
- package/lib/public/js/components/google/gmail-setup-wizard.js +18 -51
- package/lib/public/js/components/google/gmail-watch-toggle.js +4 -1
- package/lib/public/js/components/onboarding/use-welcome-storage.js +2 -1
- package/lib/public/js/components/telegram-workspace/index.js +5 -2
- package/lib/public/js/hooks/useAgentSessions.js +128 -0
- package/lib/public/js/lib/browse-draft-state.js +9 -13
- package/lib/public/js/lib/storage-keys.js +28 -0
- package/lib/public/js/lib/ui-settings.js +3 -1
- package/lib/server/doctor/normalize.js +57 -23
- package/lib/server/doctor/prompt.js +30 -4
- package/lib/server/doctor/service.js +46 -0
- package/lib/server/doctor/workspace-fingerprint.js +46 -6
- 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
|
-
|
|
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
|
-
|
|
55
|
+
import { kExpandedFoldersStorageKey } from "../lib/storage-keys.js";
|
|
56
56
|
|
|
57
57
|
const readStoredExpandedPaths = () => {
|
|
58
58
|
try {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
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;
|
|
@@ -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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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 {
|
|
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
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
9
|
-
`${
|
|
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";
|