@chrysb/alphaclaw 0.4.6-beta.1 → 0.4.6-beta.2
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/js/components/doctor/fix-card-modal.js +12 -53
- package/lib/public/js/components/doctor/helpers.js +2 -8
- package/lib/public/js/components/doctor/index.js +2 -4
- 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/storage.js +1 -4
- 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/prompt.js +17 -1
- package/lib/server/doctor/service.js +9 -0
- package/lib/server/doctor/workspace-fingerprint.js +46 -6
- package/package.json +1 -1
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
|
-
import { useEffect,
|
|
2
|
+
import { useEffect, 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 { ActionButton } from "../action-button.js";
|
|
6
6
|
import { Badge } from "../badge.js";
|
|
7
7
|
import {
|
|
8
|
-
fetchAgentSessions,
|
|
9
8
|
sendDoctorCardFix,
|
|
10
9
|
updateDoctorCardStatus,
|
|
11
10
|
} from "../../lib/api.js";
|
|
12
11
|
import { showToast } from "../toast.js";
|
|
13
12
|
import { getDoctorPriorityTone } from "./helpers.js";
|
|
13
|
+
import { useAgentSessions } from "../../hooks/useAgentSessions.js";
|
|
14
14
|
|
|
15
15
|
const html = htm.bind(h);
|
|
16
16
|
|
|
@@ -20,11 +20,16 @@ export const DoctorFixCardModal = ({
|
|
|
20
20
|
onClose = () => {},
|
|
21
21
|
onComplete = () => {},
|
|
22
22
|
}) => {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const {
|
|
24
|
+
sessions,
|
|
25
|
+
selectedSessionKey,
|
|
26
|
+
setSelectedSessionKey,
|
|
27
|
+
selectedSession,
|
|
28
|
+
loading: loadingSessions,
|
|
29
|
+
error: loadError,
|
|
30
|
+
} = useAgentSessions({ enabled: visible });
|
|
31
|
+
|
|
26
32
|
const [sending, setSending] = useState(false);
|
|
27
|
-
const [loadError, setLoadError] = useState("");
|
|
28
33
|
const [promptText, setPromptText] = useState("");
|
|
29
34
|
|
|
30
35
|
useEffect(() => {
|
|
@@ -32,52 +37,6 @@ export const DoctorFixCardModal = ({
|
|
|
32
37
|
setPromptText(String(card?.fixPrompt || ""));
|
|
33
38
|
}, [visible, card?.fixPrompt, card?.id]);
|
|
34
39
|
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
if (!visible) return;
|
|
37
|
-
let active = true;
|
|
38
|
-
const loadSessions = async () => {
|
|
39
|
-
try {
|
|
40
|
-
setLoadingSessions(true);
|
|
41
|
-
setLoadError("");
|
|
42
|
-
const data = await fetchAgentSessions();
|
|
43
|
-
if (!active) return;
|
|
44
|
-
const nextSessions = Array.isArray(data?.sessions) ? data.sessions : [];
|
|
45
|
-
setSessions(nextSessions);
|
|
46
|
-
const preferredSession =
|
|
47
|
-
nextSessions.find((sessionRow) => {
|
|
48
|
-
const key = String(sessionRow?.key || "").toLowerCase();
|
|
49
|
-
return key === "agent:main:main";
|
|
50
|
-
}) ||
|
|
51
|
-
nextSessions.find((sessionRow) => {
|
|
52
|
-
const key = String(sessionRow?.key || "").toLowerCase();
|
|
53
|
-
return key.includes(":direct:") || key.includes(":group:");
|
|
54
|
-
}) ||
|
|
55
|
-
nextSessions[0] ||
|
|
56
|
-
null;
|
|
57
|
-
setSelectedSessionKey(String(preferredSession?.key || ""));
|
|
58
|
-
} catch (error) {
|
|
59
|
-
if (!active) return;
|
|
60
|
-
setSessions([]);
|
|
61
|
-
setSelectedSessionKey("");
|
|
62
|
-
setLoadError(error.message || "Could not load agent sessions");
|
|
63
|
-
} finally {
|
|
64
|
-
if (active) setLoadingSessions(false);
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
loadSessions();
|
|
68
|
-
return () => {
|
|
69
|
-
active = false;
|
|
70
|
-
};
|
|
71
|
-
}, [visible, card?.id]);
|
|
72
|
-
|
|
73
|
-
const selectedSession = useMemo(
|
|
74
|
-
() =>
|
|
75
|
-
sessions.find(
|
|
76
|
-
(sessionRow) => String(sessionRow?.key || "") === selectedSessionKey,
|
|
77
|
-
) || null,
|
|
78
|
-
[sessions, selectedSessionKey],
|
|
79
|
-
);
|
|
80
|
-
|
|
81
40
|
const handleSend = async () => {
|
|
82
41
|
if (!card?.id || sending) return;
|
|
83
42
|
try {
|
|
@@ -98,7 +57,7 @@ export const DoctorFixCardModal = ({
|
|
|
98
57
|
"warning",
|
|
99
58
|
);
|
|
100
59
|
}
|
|
101
|
-
onComplete();
|
|
60
|
+
await onComplete();
|
|
102
61
|
onClose();
|
|
103
62
|
} catch (error) {
|
|
104
63
|
showToast(error.message || "Could not send Doctor fix request", "error");
|
|
@@ -83,14 +83,8 @@ export const getDoctorWarningMessage = (doctorStatus = null) => {
|
|
|
83
83
|
|
|
84
84
|
export const getDoctorChangeLabel = (changeSummary = null) => {
|
|
85
85
|
const changedFilesCount = Number(changeSummary?.changedFilesCount || 0);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return { text: "No changes since last run", meaningful: false };
|
|
89
|
-
}
|
|
90
|
-
return {
|
|
91
|
-
text: `${changedFilesCount} change${changedFilesCount === 1 ? "" : "s"} since last run`,
|
|
92
|
-
meaningful: hasMeaningfulChanges,
|
|
93
|
-
};
|
|
86
|
+
if (changedFilesCount === 0) return "No changes since last run";
|
|
87
|
+
return `${changedFilesCount} change${changedFilesCount === 1 ? "" : "s"} since last run`;
|
|
94
88
|
};
|
|
95
89
|
|
|
96
90
|
export const getDoctorRunPillDetail = (run = null) => {
|
|
@@ -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
|
`
|
|
@@ -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;
|
|
@@ -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,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";
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
const renderList = (items = []) =>
|
|
2
2
|
items.length ? items.map((item) => `- ${item}`).join("\n") : "- (none)";
|
|
3
3
|
|
|
4
|
+
const renderResolvedCards = (cards = []) => {
|
|
5
|
+
if (!cards.length) return "";
|
|
6
|
+
const lines = cards.map(
|
|
7
|
+
(card) =>
|
|
8
|
+
`- [${card.status}] ${card.title}` +
|
|
9
|
+
(card.category ? ` (${card.category})` : ""),
|
|
10
|
+
);
|
|
11
|
+
return `
|
|
12
|
+
|
|
13
|
+
Previously resolved findings (do not re-suggest these):
|
|
14
|
+
${lines.join("\n")}
|
|
15
|
+
`;
|
|
16
|
+
};
|
|
17
|
+
|
|
4
18
|
const buildDoctorPrompt = ({
|
|
5
19
|
workspaceRoot = "",
|
|
6
20
|
managedRoot = "",
|
|
7
21
|
protectedPaths = [],
|
|
8
22
|
lockedPaths = [],
|
|
23
|
+
resolvedCards = [],
|
|
9
24
|
promptVersion = "doctor-v1",
|
|
10
25
|
}) => `
|
|
11
26
|
You are AlphaClaw Doctor. Analyze this OpenClaw workspace for guidance drift, redundancy, misplacement, and cleanup opportunities.
|
|
@@ -74,10 +89,11 @@ Return exactly this JSON shape:
|
|
|
74
89
|
]
|
|
75
90
|
}
|
|
76
91
|
|
|
77
|
-
Constraints:
|
|
92
|
+
${renderResolvedCards(resolvedCards)}Constraints:
|
|
78
93
|
- Maximum 12 cards
|
|
79
94
|
- Use relative paths in evidence and targetPaths
|
|
80
95
|
- Do not include duplicate cards
|
|
96
|
+
- Do not re-suggest findings that appear in the "Previously resolved" list above
|
|
81
97
|
- Do not create cards for healthy default-template behavior
|
|
82
98
|
- Do not create cards whose primary recommendation is to refactor AlphaClaw-managed file structure
|
|
83
99
|
- If there are no meaningful findings, return an empty cards array
|
|
@@ -156,11 +156,20 @@ const createDoctorService = ({
|
|
|
156
156
|
|
|
157
157
|
const executeDoctorRun = async (runId) => {
|
|
158
158
|
try {
|
|
159
|
+
const allCards = listDoctorCards();
|
|
160
|
+
const resolvedCards = allCards
|
|
161
|
+
.filter((card) => card.status === "dismissed" || card.status === "fixed")
|
|
162
|
+
.map((card) => ({
|
|
163
|
+
status: card.status,
|
|
164
|
+
title: card.title || "",
|
|
165
|
+
category: card.category || "",
|
|
166
|
+
}));
|
|
159
167
|
const prompt = buildDoctorPrompt({
|
|
160
168
|
workspaceRoot,
|
|
161
169
|
managedRoot,
|
|
162
170
|
protectedPaths,
|
|
163
171
|
lockedPaths,
|
|
172
|
+
resolvedCards,
|
|
164
173
|
promptVersion: kDoctorPromptVersion,
|
|
165
174
|
});
|
|
166
175
|
const gatewayTimeoutMs = kDoctorRunTimeoutMs + 30000;
|
|
@@ -4,6 +4,17 @@ const crypto = require("crypto");
|
|
|
4
4
|
|
|
5
5
|
const kIgnoredDirectoryNames = new Set([".git", "node_modules"]);
|
|
6
6
|
|
|
7
|
+
const kContentFileExtensions = new Set([
|
|
8
|
+
".md", ".json", ".js", ".ts", ".jsx", ".tsx", ".yaml", ".yml",
|
|
9
|
+
".txt", ".sh", ".css", ".html", ".xml", ".toml", ".ini", ".cfg",
|
|
10
|
+
".py", ".rb", ".go", ".rs", ".java", ".c", ".cpp", ".h",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const isContentFile = (relativePath = "") => {
|
|
14
|
+
const ext = path.extname(String(relativePath || "")).toLowerCase();
|
|
15
|
+
return kContentFileExtensions.has(ext);
|
|
16
|
+
};
|
|
17
|
+
|
|
7
18
|
const hashFile = (filePath) => {
|
|
8
19
|
const buffer = fs.readFileSync(filePath);
|
|
9
20
|
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
@@ -34,11 +45,21 @@ const buildWorkspaceManifest = (rootDir) => {
|
|
|
34
45
|
const normalizedRootDir = path.resolve(String(rootDir || ""));
|
|
35
46
|
const files = walkFiles(normalizedRootDir);
|
|
36
47
|
return files.reduce((manifest, filePath) => {
|
|
37
|
-
|
|
48
|
+
const stat = fs.statSync(filePath);
|
|
49
|
+
manifest[normalizeRelativePath(normalizedRootDir, filePath)] = {
|
|
50
|
+
hash: hashFile(filePath),
|
|
51
|
+
size: stat.size,
|
|
52
|
+
};
|
|
38
53
|
return manifest;
|
|
39
54
|
}, {});
|
|
40
55
|
};
|
|
41
56
|
|
|
57
|
+
const getManifestEntryHash = (entry) =>
|
|
58
|
+
typeof entry === "object" && entry !== null ? String(entry.hash || "") : String(entry || "");
|
|
59
|
+
|
|
60
|
+
const getManifestEntrySize = (entry) =>
|
|
61
|
+
typeof entry === "object" && entry !== null ? Number(entry.size || 0) : 0;
|
|
62
|
+
|
|
42
63
|
const computeWorkspaceFingerprintFromManifest = (manifest = {}) => {
|
|
43
64
|
const hash = crypto.createHash("sha256");
|
|
44
65
|
const entries = Object.entries(manifest).sort(([leftPath], [rightPath]) =>
|
|
@@ -46,10 +67,10 @@ const computeWorkspaceFingerprintFromManifest = (manifest = {}) => {
|
|
|
46
67
|
);
|
|
47
68
|
|
|
48
69
|
hash.update("workspace-fingerprint-v1");
|
|
49
|
-
for (const [relativePath,
|
|
70
|
+
for (const [relativePath, entry] of entries) {
|
|
50
71
|
hash.update(relativePath);
|
|
51
72
|
hash.update("\0");
|
|
52
|
-
hash.update(
|
|
73
|
+
hash.update(getManifestEntryHash(entry));
|
|
53
74
|
hash.update("\0");
|
|
54
75
|
}
|
|
55
76
|
|
|
@@ -84,6 +105,20 @@ const getPathChangeWeight = (relativePath = "") => {
|
|
|
84
105
|
return 1;
|
|
85
106
|
};
|
|
86
107
|
|
|
108
|
+
const kByteDeltaSmallThreshold = 100;
|
|
109
|
+
const kByteDeltaSignificantThreshold = 500;
|
|
110
|
+
|
|
111
|
+
const getModifiedFileScore = (relativePath, previousEntry, currentEntry) => {
|
|
112
|
+
if (!isContentFile(relativePath)) return 1;
|
|
113
|
+
const previousSize = getManifestEntrySize(previousEntry);
|
|
114
|
+
const currentSize = getManifestEntrySize(currentEntry);
|
|
115
|
+
if (!previousSize && !currentSize) return getPathChangeWeight(relativePath);
|
|
116
|
+
const byteDelta = Math.abs(currentSize - previousSize);
|
|
117
|
+
if (byteDelta < kByteDeltaSmallThreshold) return 1;
|
|
118
|
+
if (byteDelta < kByteDeltaSignificantThreshold) return 2;
|
|
119
|
+
return getPathChangeWeight(relativePath);
|
|
120
|
+
};
|
|
121
|
+
|
|
87
122
|
const calculateWorkspaceDelta = ({ previousManifest = {}, currentManifest = {} } = {}) => {
|
|
88
123
|
const previousPaths = Object.keys(previousManifest);
|
|
89
124
|
const currentPaths = Object.keys(currentManifest);
|
|
@@ -100,19 +135,23 @@ const calculateWorkspaceDelta = ({ previousManifest = {}, currentManifest = {} }
|
|
|
100
135
|
};
|
|
101
136
|
|
|
102
137
|
for (const relativePath of allPaths) {
|
|
103
|
-
const
|
|
104
|
-
const
|
|
138
|
+
const previousEntry = previousManifest[relativePath];
|
|
139
|
+
const currentEntry = currentManifest[relativePath];
|
|
140
|
+
const previousHash = getManifestEntryHash(previousEntry);
|
|
141
|
+
const currentHash = getManifestEntryHash(currentEntry);
|
|
105
142
|
if (!previousHash && currentHash) {
|
|
106
143
|
changeSummary.addedFilesCount += 1;
|
|
144
|
+
changeSummary.deltaScore += getPathChangeWeight(relativePath);
|
|
107
145
|
} else if (previousHash && !currentHash) {
|
|
108
146
|
changeSummary.removedFilesCount += 1;
|
|
147
|
+
changeSummary.deltaScore += getPathChangeWeight(relativePath);
|
|
109
148
|
} else if (previousHash !== currentHash) {
|
|
110
149
|
changeSummary.modifiedFilesCount += 1;
|
|
150
|
+
changeSummary.deltaScore += getModifiedFileScore(relativePath, previousEntry, currentEntry);
|
|
111
151
|
} else {
|
|
112
152
|
continue;
|
|
113
153
|
}
|
|
114
154
|
changeSummary.changedFilesCount += 1;
|
|
115
|
-
changeSummary.deltaScore += getPathChangeWeight(relativePath);
|
|
116
155
|
changeSummary.changedPaths.push(relativePath);
|
|
117
156
|
}
|
|
118
157
|
|
|
@@ -123,4 +162,5 @@ module.exports = {
|
|
|
123
162
|
calculateWorkspaceDelta,
|
|
124
163
|
computeWorkspaceFingerprintFromManifest,
|
|
125
164
|
computeWorkspaceSnapshot,
|
|
165
|
+
isContentFile,
|
|
126
166
|
};
|