@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
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
/* ── Browse/Explorer mode ─────────────────────── */
|
|
2
2
|
|
|
3
|
-
.app-content.browse-mode {
|
|
4
|
-
padding: 0;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
3
|
.sidebar-tabs {
|
|
8
4
|
display: flex;
|
|
9
5
|
align-items: center;
|
|
@@ -875,6 +871,10 @@
|
|
|
875
871
|
align-items: flex-start;
|
|
876
872
|
}
|
|
877
873
|
|
|
874
|
+
.line-highlight-flash {
|
|
875
|
+
color: #4ade80 !important;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
878
|
.file-viewer-editor {
|
|
879
879
|
width: 100%;
|
|
880
880
|
min-height: 0;
|
|
@@ -1608,7 +1608,4 @@
|
|
|
1608
1608
|
display: none;
|
|
1609
1609
|
}
|
|
1610
1610
|
|
|
1611
|
-
.app-content.browse-mode {
|
|
1612
|
-
padding: 0;
|
|
1613
|
-
}
|
|
1614
1611
|
}
|
package/lib/public/css/shell.css
CHANGED
|
@@ -47,12 +47,22 @@
|
|
|
47
47
|
.app-content {
|
|
48
48
|
grid-column: 3;
|
|
49
49
|
grid-row: 2;
|
|
50
|
-
overflow
|
|
51
|
-
padding: 24px 32px;
|
|
50
|
+
overflow: hidden;
|
|
52
51
|
position: relative;
|
|
53
52
|
z-index: 1;
|
|
54
53
|
}
|
|
55
54
|
|
|
55
|
+
.app-content-pane {
|
|
56
|
+
position: absolute;
|
|
57
|
+
inset: 0;
|
|
58
|
+
overflow-y: auto;
|
|
59
|
+
padding: 24px 32px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.app-content-pane.browse-pane {
|
|
63
|
+
padding: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
56
66
|
/* ── Sidebar ───────────────────────────────────── */
|
|
57
67
|
|
|
58
68
|
.app-sidebar {
|
|
@@ -312,6 +322,8 @@
|
|
|
312
322
|
.app-content {
|
|
313
323
|
grid-column: 1;
|
|
314
324
|
grid-row: 2;
|
|
325
|
+
}
|
|
326
|
+
.app-content-pane {
|
|
315
327
|
padding: 0 14px 12px;
|
|
316
328
|
}
|
|
317
329
|
|
package/lib/public/css/theme.css
CHANGED
|
@@ -74,6 +74,10 @@ body::before {
|
|
|
74
74
|
background: rgba(0, 0, 0, 0.12);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
.snippet-collapse-fade {
|
|
78
|
+
background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.75) 70%);
|
|
79
|
+
}
|
|
80
|
+
|
|
77
81
|
/* Shared inset panel for "surface on surface" layouts. */
|
|
78
82
|
.ac-surface-inset {
|
|
79
83
|
border: 1px solid var(--panel-border-contrast);
|
package/lib/public/js/app.js
CHANGED
|
@@ -477,6 +477,7 @@ const App = () => {
|
|
|
477
477
|
const [openclawUpdateInProgress, setOpenclawUpdateInProgress] = useState(false);
|
|
478
478
|
const menuRef = useRef(null);
|
|
479
479
|
const routeHistoryRef = useRef([]);
|
|
480
|
+
const menuPaneRef = useRef(null);
|
|
480
481
|
const sharedStatusPoll = usePolling(fetchStatus, statusPollCadenceMs, {
|
|
481
482
|
enabled: onboarded === true,
|
|
482
483
|
});
|
|
@@ -709,8 +710,12 @@ const App = () => {
|
|
|
709
710
|
.map((segment) => encodeURIComponent(segment))
|
|
710
711
|
.join("/");
|
|
711
712
|
const baseRoute = encodedPath ? `/browse/${encodedPath}` : "/browse";
|
|
712
|
-
|
|
713
|
-
|
|
713
|
+
const params = new URLSearchParams();
|
|
714
|
+
if (view === "diff" && encodedPath) params.set("view", "diff");
|
|
715
|
+
if (options.line) params.set("line", String(options.line));
|
|
716
|
+
if (options.lineEnd) params.set("lineEnd", String(options.lineEnd));
|
|
717
|
+
const query = params.toString();
|
|
718
|
+
return query ? `${baseRoute}?${query}` : baseRoute;
|
|
714
719
|
};
|
|
715
720
|
const navigateToSubScreen = (screen) => {
|
|
716
721
|
setLocation(`/${screen}`);
|
|
@@ -767,7 +772,7 @@ const App = () => {
|
|
|
767
772
|
setLocation(`/${kDefaultUiTab}`);
|
|
768
773
|
setMobileSidebarOpen(false);
|
|
769
774
|
};
|
|
770
|
-
const
|
|
775
|
+
const handlePaneScroll = (e) => {
|
|
771
776
|
const nextScrolled = e.currentTarget.scrollTop > 0;
|
|
772
777
|
setMobileTopbarScrolled((currentScrolled) =>
|
|
773
778
|
currentScrolled === nextScrolled ? currentScrolled : nextScrolled,
|
|
@@ -820,11 +825,13 @@ const App = () => {
|
|
|
820
825
|
.join("/")
|
|
821
826
|
: "";
|
|
822
827
|
const activeBrowsePath = browsePreviewPath || selectedBrowsePath;
|
|
828
|
+
const browseQueryParams = isBrowseRoute ? new URLSearchParams(browseRouteQuery) : null;
|
|
823
829
|
const browseViewerMode =
|
|
824
|
-
!browsePreviewPath &&
|
|
825
|
-
new URLSearchParams(browseRouteQuery).get("view") === "diff"
|
|
830
|
+
!browsePreviewPath && browseQueryParams?.get("view") === "diff"
|
|
826
831
|
? "diff"
|
|
827
832
|
: "edit";
|
|
833
|
+
const browseLineTarget = Number.parseInt(browseQueryParams?.get("line") || "", 10) || 0;
|
|
834
|
+
const browseLineEndTarget = Number.parseInt(browseQueryParams?.get("lineEnd") || "", 10) || 0;
|
|
828
835
|
const selectedNavId = isBrowseRoute
|
|
829
836
|
? "browse"
|
|
830
837
|
: location === "/telegram"
|
|
@@ -1017,33 +1024,18 @@ const App = () => {
|
|
|
1017
1024
|
onclick=${() => setMobileSidebarOpen(false)}
|
|
1018
1025
|
/>
|
|
1019
1026
|
|
|
1020
|
-
<div
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
<
|
|
1026
|
-
class="mobile-topbar-menu"
|
|
1027
|
-
onclick=${() => setMobileSidebarOpen((open) => !open)}
|
|
1028
|
-
aria-label="Open menu"
|
|
1029
|
-
aria-expanded=${mobileSidebarOpen ? "true" : "false"}
|
|
1030
|
-
>
|
|
1031
|
-
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor">
|
|
1032
|
-
<path
|
|
1033
|
-
d="M2 3.75a.75.75 0 01.75-.75h10.5a.75.75 0 010 1.5H2.75A.75.75 0 012 3.75zm0 4.25a.75.75 0 01.75-.75h10.5a.75.75 0 010 1.5H2.75A.75.75 0 012 8zm0 4.25a.75.75 0 01.75-.75h10.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75z"
|
|
1034
|
-
/>
|
|
1035
|
-
</svg>
|
|
1036
|
-
</button>
|
|
1037
|
-
<span class="mobile-topbar-title">
|
|
1038
|
-
<span style="color: var(--accent)">alpha</span>claw
|
|
1039
|
-
</span>
|
|
1040
|
-
</div>
|
|
1041
|
-
<div class=${isBrowseRoute ? "w-full" : "max-w-2xl w-full mx-auto"}>
|
|
1042
|
-
<div style=${{ display: isBrowseRoute ? "block" : "none" }}>
|
|
1027
|
+
<div class="app-content">
|
|
1028
|
+
<div
|
|
1029
|
+
class="app-content-pane browse-pane"
|
|
1030
|
+
style=${{ display: isBrowseRoute ? "block" : "none" }}
|
|
1031
|
+
>
|
|
1032
|
+
<div class="w-full">
|
|
1043
1033
|
<${FileViewer}
|
|
1044
1034
|
filePath=${activeBrowsePath}
|
|
1045
1035
|
isPreviewOnly=${false}
|
|
1046
1036
|
browseView=${browseViewerMode}
|
|
1037
|
+
lineTarget=${browseLineTarget}
|
|
1038
|
+
lineEndTarget=${browseLineEndTarget}
|
|
1047
1039
|
onRequestEdit=${(targetPath) => {
|
|
1048
1040
|
const normalizedTargetPath = String(targetPath || "");
|
|
1049
1041
|
if (
|
|
@@ -1061,7 +1053,31 @@ const App = () => {
|
|
|
1061
1053
|
}}
|
|
1062
1054
|
/>
|
|
1063
1055
|
</div>
|
|
1064
|
-
|
|
1056
|
+
</div>
|
|
1057
|
+
<div
|
|
1058
|
+
class="app-content-pane"
|
|
1059
|
+
ref=${menuPaneRef}
|
|
1060
|
+
onscroll=${handlePaneScroll}
|
|
1061
|
+
style=${{ display: isBrowseRoute ? "none" : "block" }}
|
|
1062
|
+
>
|
|
1063
|
+
<div class=${`mobile-topbar ${mobileTopbarScrolled ? "is-scrolled" : ""}`}>
|
|
1064
|
+
<button
|
|
1065
|
+
class="mobile-topbar-menu"
|
|
1066
|
+
onclick=${() => setMobileSidebarOpen((open) => !open)}
|
|
1067
|
+
aria-label="Open menu"
|
|
1068
|
+
aria-expanded=${mobileSidebarOpen ? "true" : "false"}
|
|
1069
|
+
>
|
|
1070
|
+
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor">
|
|
1071
|
+
<path
|
|
1072
|
+
d="M2 3.75a.75.75 0 01.75-.75h10.5a.75.75 0 010 1.5H2.75A.75.75 0 012 3.75zm0 4.25a.75.75 0 01.75-.75h10.5a.75.75 0 010 1.5H2.75A.75.75 0 012 8zm0 4.25a.75.75 0 01.75-.75h10.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75z"
|
|
1073
|
+
/>
|
|
1074
|
+
</svg>
|
|
1075
|
+
</button>
|
|
1076
|
+
<span class="mobile-topbar-title">
|
|
1077
|
+
<span style="color: var(--accent)">alpha</span>claw
|
|
1078
|
+
</span>
|
|
1079
|
+
</div>
|
|
1080
|
+
<div class="max-w-2xl w-full mx-auto">
|
|
1065
1081
|
<div style=${{ display: location === "/general" ? "block" : "none" }}>
|
|
1066
1082
|
<div class="pt-4">
|
|
1067
1083
|
<${GeneralTab}
|
|
@@ -1086,7 +1102,22 @@ const App = () => {
|
|
|
1086
1102
|
/>
|
|
1087
1103
|
</div>
|
|
1088
1104
|
</div>
|
|
1089
|
-
|
|
1105
|
+
<div style=${{ display: location === "/doctor" ? "block" : "none" }}>
|
|
1106
|
+
<div class="pt-4">
|
|
1107
|
+
<${DoctorTab}
|
|
1108
|
+
isActive=${location === "/doctor"}
|
|
1109
|
+
onOpenFile=${(relativePath, options = {}) => {
|
|
1110
|
+
const browsePath = `workspace/${String(relativePath || "").trim().replace(/^workspace\//, "")}`;
|
|
1111
|
+
navigateToBrowseFile(browsePath, {
|
|
1112
|
+
view: "edit",
|
|
1113
|
+
...(options.line ? { line: options.line } : {}),
|
|
1114
|
+
...(options.lineEnd ? { lineEnd: options.lineEnd } : {}),
|
|
1115
|
+
});
|
|
1116
|
+
}}
|
|
1117
|
+
/>
|
|
1118
|
+
</div>
|
|
1119
|
+
</div>
|
|
1120
|
+
${!isBrowseRoute && location !== "/general" && location !== "/doctor"
|
|
1090
1121
|
? html`
|
|
1091
1122
|
<${Switch}>
|
|
1092
1123
|
<${Route} path="/telegram">
|
|
@@ -1136,13 +1167,6 @@ const App = () => {
|
|
|
1136
1167
|
/>
|
|
1137
1168
|
</div>
|
|
1138
1169
|
</${Route}>
|
|
1139
|
-
<${Route} path="/doctor">
|
|
1140
|
-
<div class="pt-4">
|
|
1141
|
-
<${DoctorTab}
|
|
1142
|
-
isActive=${location === "/doctor"}
|
|
1143
|
-
/>
|
|
1144
|
-
</div>
|
|
1145
|
-
</${Route}>
|
|
1146
1170
|
<${Route} path="/envars">
|
|
1147
1171
|
<div class="pt-4">
|
|
1148
1172
|
<${Envars} onRestartRequired=${setRestartRequired} />
|
|
@@ -1164,7 +1188,7 @@ const App = () => {
|
|
|
1164
1188
|
</div>
|
|
1165
1189
|
</div>
|
|
1166
1190
|
<${ToastContainer}
|
|
1167
|
-
className="fixed top-4 right-4 z-
|
|
1191
|
+
className="fixed top-4 right-4 z-[60] space-y-2 pointer-events-none"
|
|
1168
1192
|
/>
|
|
1169
1193
|
</div>
|
|
1170
1194
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useState } from "https://esm.sh/preact/hooks";
|
|
2
3
|
import htm from "https://esm.sh/htm";
|
|
3
4
|
import { Badge } from "../badge.js";
|
|
4
5
|
import { ActionButton } from "../action-button.js";
|
|
@@ -10,8 +11,159 @@ import {
|
|
|
10
11
|
|
|
11
12
|
const html = htm.bind(h);
|
|
12
13
|
|
|
13
|
-
const
|
|
14
|
-
if (item
|
|
14
|
+
const resolveTargetPath = (item) => {
|
|
15
|
+
if (!item) return null;
|
|
16
|
+
if (typeof item === "string") return { path: item };
|
|
17
|
+
if (typeof item === "object" && item.path) return item;
|
|
18
|
+
return null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const formatPathLabel = (filePath, startLine, endLine) => {
|
|
22
|
+
if (startLine && endLine && endLine > startLine)
|
|
23
|
+
return `${filePath}:${startLine}-${endLine}`;
|
|
24
|
+
if (startLine) return `${filePath}:${startLine}`;
|
|
25
|
+
return filePath;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const buildLineOptions = (startLine, endLine) => {
|
|
29
|
+
const options = {};
|
|
30
|
+
if (startLine) options.line = startLine;
|
|
31
|
+
if (endLine && endLine > startLine) options.lineEnd = endLine;
|
|
32
|
+
return options;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const renderPathLink = (filePath, onOpenFile, { startLine, endLine } = {}) => {
|
|
36
|
+
const label = formatPathLabel(filePath, startLine, endLine);
|
|
37
|
+
return html`
|
|
38
|
+
<button
|
|
39
|
+
type="button"
|
|
40
|
+
class="text-left font-mono ac-tip-link hover:underline cursor-pointer"
|
|
41
|
+
onClick=${(e) => {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
onOpenFile(
|
|
44
|
+
String(filePath || ""),
|
|
45
|
+
buildLineOptions(startLine, endLine),
|
|
46
|
+
);
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
${label}
|
|
50
|
+
</button>
|
|
51
|
+
`;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const kSnippetCollapseThreshold = 7;
|
|
55
|
+
|
|
56
|
+
const SnippetBlock = ({ item, onOpenFile, isOutdated }) => {
|
|
57
|
+
const snippet = item.snippet;
|
|
58
|
+
const allLines = String(snippet.text || "").split("\n");
|
|
59
|
+
const isCollapsible = allLines.length > kSnippetCollapseThreshold;
|
|
60
|
+
const [expanded, setExpanded] = useState(!isCollapsible);
|
|
61
|
+
const visibleLines = expanded
|
|
62
|
+
? allLines
|
|
63
|
+
: allLines.slice(0, kSnippetCollapseThreshold);
|
|
64
|
+
const gutterWidth = String(snippet.endLine || snippet.startLine || 1).length;
|
|
65
|
+
return html`
|
|
66
|
+
<div class="mt-1.5 rounded-lg border border-border overflow-hidden">
|
|
67
|
+
<div
|
|
68
|
+
class="flex items-center justify-between px-3 py-1.5 bg-black/30 border-b border-border"
|
|
69
|
+
>
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
class="text-[11px] font-mono ac-tip-link hover:underline cursor-pointer"
|
|
73
|
+
onClick=${(e) => {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
onOpenFile(
|
|
76
|
+
String(item.path || ""),
|
|
77
|
+
buildLineOptions(item.startLine, item.endLine),
|
|
78
|
+
);
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
${formatPathLabel(item.path, item.startLine, item.endLine)}
|
|
82
|
+
</button>
|
|
83
|
+
${isOutdated
|
|
84
|
+
? html`<span class="text-[10px] text-yellow-500/80"
|
|
85
|
+
>file changed since scan</span
|
|
86
|
+
>`
|
|
87
|
+
: html`<span class="text-[10px] text-gray-600">snapshot</span>`}
|
|
88
|
+
</div>
|
|
89
|
+
<div class="relative">
|
|
90
|
+
<div
|
|
91
|
+
class="px-3 py-2 text-[11px] leading-[18px] font-mono text-gray-300 bg-black/20"
|
|
92
|
+
style="white-space:pre-wrap;word-break:break-word"
|
|
93
|
+
>
|
|
94
|
+
${visibleLines.map(
|
|
95
|
+
(line, index) => html`
|
|
96
|
+
<div class="flex">
|
|
97
|
+
<span
|
|
98
|
+
class="text-gray-600 select-none shrink-0"
|
|
99
|
+
style="width:${gutterWidth +
|
|
100
|
+
1}ch;text-align:right;margin-right:1ch"
|
|
101
|
+
>${snippet.startLine + index}</span
|
|
102
|
+
><span>${line || " "}</span>
|
|
103
|
+
</div>
|
|
104
|
+
`,
|
|
105
|
+
)}
|
|
106
|
+
${expanded && snippet.truncated
|
|
107
|
+
? html`<div class="text-gray-600 italic pl-1">... truncated</div>`
|
|
108
|
+
: ""}
|
|
109
|
+
</div>
|
|
110
|
+
${isCollapsible && !expanded
|
|
111
|
+
? html`
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
class="absolute inset-x-0 bottom-0 flex items-end justify-center pb-2 pt-10 cursor-pointer snippet-collapse-fade"
|
|
115
|
+
onClick=${() => setExpanded(true)}
|
|
116
|
+
>
|
|
117
|
+
<span
|
|
118
|
+
class="text-[10px] text-gray-500 hover:text-white flex items-center gap-1 transition-colors"
|
|
119
|
+
>
|
|
120
|
+
<span
|
|
121
|
+
class="inline-block text-xs transition-transform"
|
|
122
|
+
aria-hidden="true"
|
|
123
|
+
>▾</span
|
|
124
|
+
>
|
|
125
|
+
${allLines.length} lines
|
|
126
|
+
</span>
|
|
127
|
+
</button>
|
|
128
|
+
`
|
|
129
|
+
: null}
|
|
130
|
+
${isCollapsible && expanded
|
|
131
|
+
? html`
|
|
132
|
+
<button
|
|
133
|
+
type="button"
|
|
134
|
+
class="w-full flex items-center justify-center py-1 cursor-pointer bg-black/20 border-t border-border"
|
|
135
|
+
onClick=${() => setExpanded(false)}
|
|
136
|
+
>
|
|
137
|
+
<span class="text-[10px] text-gray-500 flex items-center gap-1">
|
|
138
|
+
<span
|
|
139
|
+
class="inline-block transition-transform"
|
|
140
|
+
aria-hidden="true"
|
|
141
|
+
>▴</span
|
|
142
|
+
>
|
|
143
|
+
collapse
|
|
144
|
+
</span>
|
|
145
|
+
</button>
|
|
146
|
+
`
|
|
147
|
+
: null}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
`;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const renderEvidenceLine = (item = {}, onOpenFile, changedPathsSet) => {
|
|
154
|
+
if (item?.path && item?.snippet) {
|
|
155
|
+
const isOutdated = changedPathsSet.has(item.path);
|
|
156
|
+
return html`<${SnippetBlock}
|
|
157
|
+
item=${item}
|
|
158
|
+
onOpenFile=${onOpenFile}
|
|
159
|
+
isOutdated=${isOutdated}
|
|
160
|
+
/>`;
|
|
161
|
+
}
|
|
162
|
+
if (item?.path)
|
|
163
|
+
return renderPathLink(item.path, onOpenFile, {
|
|
164
|
+
startLine: item.startLine,
|
|
165
|
+
endLine: item.endLine,
|
|
166
|
+
});
|
|
15
167
|
if (item?.text) return item.text;
|
|
16
168
|
return JSON.stringify(item);
|
|
17
169
|
};
|
|
@@ -21,9 +173,12 @@ export const DoctorFindingsList = ({
|
|
|
21
173
|
busyCardId = 0,
|
|
22
174
|
onAskAgentFix = () => {},
|
|
23
175
|
onUpdateStatus = () => {},
|
|
176
|
+
onOpenFile = () => {},
|
|
177
|
+
changedPaths = [],
|
|
24
178
|
showRunMeta = false,
|
|
25
179
|
hideEmptyState = false,
|
|
26
180
|
}) => {
|
|
181
|
+
const changedPathsSet = new Set(changedPaths);
|
|
27
182
|
return html`
|
|
28
183
|
<div class="space-y-4">
|
|
29
184
|
${cards.length
|
|
@@ -98,15 +253,34 @@ export const DoctorFindingsList = ({
|
|
|
98
253
|
Target paths
|
|
99
254
|
</div>
|
|
100
255
|
<div class="mt-1 flex flex-wrap gap-1.5">
|
|
101
|
-
${card.targetPaths.map(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
256
|
+
${card.targetPaths.map((rawItem) => {
|
|
257
|
+
const resolved =
|
|
258
|
+
resolveTargetPath(rawItem);
|
|
259
|
+
if (!resolved) return null;
|
|
260
|
+
const label = formatPathLabel(
|
|
261
|
+
resolved.path,
|
|
262
|
+
resolved.startLine,
|
|
263
|
+
resolved.endLine,
|
|
264
|
+
);
|
|
265
|
+
return html`
|
|
266
|
+
<button
|
|
267
|
+
type="button"
|
|
268
|
+
class="text-[11px] px-2 py-1 rounded-md bg-black/30 border border-border font-mono text-gray-200 hover:text-white hover:border-gray-500 cursor-pointer transition-colors"
|
|
269
|
+
onClick=${(e) => {
|
|
270
|
+
e.preventDefault();
|
|
271
|
+
onOpenFile(
|
|
272
|
+
String(resolved.path || ""),
|
|
273
|
+
buildLineOptions(
|
|
274
|
+
resolved.startLine,
|
|
275
|
+
resolved.endLine,
|
|
276
|
+
),
|
|
277
|
+
);
|
|
278
|
+
}}
|
|
105
279
|
>
|
|
106
|
-
${
|
|
107
|
-
</
|
|
108
|
-
|
|
109
|
-
)}
|
|
280
|
+
${label}
|
|
281
|
+
</button>
|
|
282
|
+
`;
|
|
283
|
+
})}
|
|
110
284
|
</div>
|
|
111
285
|
</div>
|
|
112
286
|
`
|
|
@@ -117,11 +291,15 @@ export const DoctorFindingsList = ({
|
|
|
117
291
|
? html`
|
|
118
292
|
<div>
|
|
119
293
|
<div class="ac-small-heading">Evidence</div>
|
|
120
|
-
<div class="mt-1 space-y-
|
|
294
|
+
<div class="mt-1 space-y-2">
|
|
121
295
|
${card.evidence.map(
|
|
122
296
|
(item) => html`
|
|
123
297
|
<div class="text-xs text-gray-400">
|
|
124
|
-
${renderEvidenceLine(
|
|
298
|
+
${renderEvidenceLine(
|
|
299
|
+
item,
|
|
300
|
+
onOpenFile,
|
|
301
|
+
changedPathsSet,
|
|
302
|
+
)}
|
|
125
303
|
</div>
|
|
126
304
|
`,
|
|
127
305
|
)}
|
|
@@ -1,16 +1,11 @@
|
|
|
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
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
fetchAgentSessions,
|
|
9
|
-
sendDoctorCardFix,
|
|
10
|
-
updateDoctorCardStatus,
|
|
11
|
-
} from "../../lib/api.js";
|
|
6
|
+
import { sendDoctorCardFix, updateDoctorCardStatus } from "../../lib/api.js";
|
|
12
7
|
import { showToast } from "../toast.js";
|
|
13
|
-
import {
|
|
8
|
+
import { useAgentSessions } from "../../hooks/useAgentSessions.js";
|
|
14
9
|
|
|
15
10
|
const html = htm.bind(h);
|
|
16
11
|
|
|
@@ -20,11 +15,16 @@ export const DoctorFixCardModal = ({
|
|
|
20
15
|
onClose = () => {},
|
|
21
16
|
onComplete = () => {},
|
|
22
17
|
}) => {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
const {
|
|
19
|
+
sessions,
|
|
20
|
+
selectedSessionKey,
|
|
21
|
+
setSelectedSessionKey,
|
|
22
|
+
selectedSession,
|
|
23
|
+
loading: loadingSessions,
|
|
24
|
+
error: loadError,
|
|
25
|
+
} = useAgentSessions({ enabled: visible });
|
|
26
|
+
|
|
26
27
|
const [sending, setSending] = useState(false);
|
|
27
|
-
const [loadError, setLoadError] = useState("");
|
|
28
28
|
const [promptText, setPromptText] = useState("");
|
|
29
29
|
|
|
30
30
|
useEffect(() => {
|
|
@@ -32,52 +32,6 @@ export const DoctorFixCardModal = ({
|
|
|
32
32
|
setPromptText(String(card?.fixPrompt || ""));
|
|
33
33
|
}, [visible, card?.fixPrompt, card?.id]);
|
|
34
34
|
|
|
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
35
|
const handleSend = async () => {
|
|
82
36
|
if (!card?.id || sending) return;
|
|
83
37
|
try {
|
|
@@ -91,14 +45,18 @@ export const DoctorFixCardModal = ({
|
|
|
91
45
|
});
|
|
92
46
|
try {
|
|
93
47
|
await updateDoctorCardStatus({ cardId: card.id, status: "fixed" });
|
|
94
|
-
showToast(
|
|
48
|
+
showToast(
|
|
49
|
+
"Doctor fix request sent and finding marked fixed",
|
|
50
|
+
"success",
|
|
51
|
+
);
|
|
95
52
|
} catch (statusError) {
|
|
96
53
|
showToast(
|
|
97
|
-
statusError.message ||
|
|
54
|
+
statusError.message ||
|
|
55
|
+
"Doctor fix request sent, but could not mark the finding fixed",
|
|
98
56
|
"warning",
|
|
99
57
|
);
|
|
100
58
|
}
|
|
101
|
-
onComplete();
|
|
59
|
+
await onComplete();
|
|
102
60
|
onClose();
|
|
103
61
|
} catch (error) {
|
|
104
62
|
showToast(error.message || "Could not send Doctor fix request", "error");
|
|
@@ -115,21 +73,10 @@ export const DoctorFixCardModal = ({
|
|
|
115
73
|
>
|
|
116
74
|
<div class="space-y-1">
|
|
117
75
|
<h2 class="text-base font-semibold">Ask agent to fix</h2>
|
|
118
|
-
<p class="text-
|
|
76
|
+
<p class="text-xs text-gray-400">
|
|
119
77
|
Send this Doctor finding to one of your agent sessions as a focused fix request.
|
|
120
78
|
</p>
|
|
121
79
|
</div>
|
|
122
|
-
<div class="ac-surface-inset p-3 space-y-1">
|
|
123
|
-
<div class="flex items-center gap-2 min-w-0">
|
|
124
|
-
<${Badge} tone=${getDoctorPriorityTone(card?.priority || "P2")}>
|
|
125
|
-
${card?.priority || "P2"}
|
|
126
|
-
</${Badge}>
|
|
127
|
-
<div class="text-sm font-semibold text-gray-200 leading-5 min-w-0">
|
|
128
|
-
${card?.title || "Doctor finding"}
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
<div class="text-xs text-gray-400">${card?.recommendation || ""}</div>
|
|
132
|
-
</div>
|
|
133
80
|
<div class="space-y-2">
|
|
134
81
|
<label class="text-xs text-gray-500">Send to session</label>
|
|
135
82
|
<select
|
|
@@ -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) => {
|
|
@@ -112,29 +106,15 @@ export const buildDoctorRunMarkers = (run = null) => {
|
|
|
112
106
|
if ((run.cardCount || 0) === 0) {
|
|
113
107
|
return [{ tone: "success", count: 0, label: "No findings" }];
|
|
114
108
|
}
|
|
115
|
-
const
|
|
109
|
+
const highPriority = [];
|
|
116
110
|
if (Number(run?.priorityCounts?.P0 || 0) > 0) {
|
|
117
|
-
|
|
118
|
-
tone: "danger",
|
|
119
|
-
count: Number(run.priorityCounts.P0 || 0),
|
|
120
|
-
label: "P0",
|
|
121
|
-
});
|
|
111
|
+
highPriority.push({ tone: "danger", count: 0, label: "P0" });
|
|
122
112
|
}
|
|
123
113
|
if (Number(run?.priorityCounts?.P1 || 0) > 0) {
|
|
124
|
-
|
|
125
|
-
tone: "warning",
|
|
126
|
-
count: Number(run.priorityCounts.P1 || 0),
|
|
127
|
-
label: "P1",
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
if (Number(run?.priorityCounts?.P2 || 0) > 0) {
|
|
131
|
-
markers.push({
|
|
132
|
-
tone: "neutral",
|
|
133
|
-
count: Number(run.priorityCounts.P2 || 0),
|
|
134
|
-
label: "P2",
|
|
135
|
-
});
|
|
114
|
+
highPriority.push({ tone: "warning", count: 0, label: "P1" });
|
|
136
115
|
}
|
|
137
|
-
return
|
|
116
|
+
if (highPriority.length > 0) return highPriority.slice(0, 2);
|
|
117
|
+
return [{ tone: "neutral", count: 0, label: "P2" }];
|
|
138
118
|
};
|
|
139
119
|
|
|
140
120
|
export const buildDoctorStatusFilterOptions = () => [
|