@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.
@@ -1,16 +1,16 @@
1
1
  import { h } from "https://esm.sh/preact";
2
- import { useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
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 [sessions, setSessions] = useState([]);
24
- const [selectedSessionKey, setSelectedSessionKey] = useState("");
25
- const [loadingSessions, setLoadingSessions] = useState(false);
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
- const hasMeaningfulChanges = !!changeSummary?.hasMeaningfulChanges;
87
- if (changedFilesCount === 0) {
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
- 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
  `
@@ -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;
@@ -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 { 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 {};
@@ -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
- manifest[normalizeRelativePath(normalizedRootDir, filePath)] = hashFile(filePath);
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, fileHash] of entries) {
70
+ for (const [relativePath, entry] of entries) {
50
71
  hash.update(relativePath);
51
72
  hash.update("\0");
52
- hash.update(fileHash);
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 previousHash = previousManifest[relativePath] || "";
104
- const currentHash = currentManifest[relativePath] || "";
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.4.6-beta.1",
3
+ "version": "0.4.6-beta.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },