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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/lib/public/css/explorer.css +4 -7
  2. package/lib/public/css/shell.css +14 -2
  3. package/lib/public/css/theme.css +4 -0
  4. package/lib/public/js/app.js +62 -38
  5. package/lib/public/js/components/doctor/findings-list.js +190 -12
  6. package/lib/public/js/components/doctor/fix-card-modal.js +20 -73
  7. package/lib/public/js/components/doctor/helpers.js +7 -27
  8. package/lib/public/js/components/doctor/index.js +5 -5
  9. package/lib/public/js/components/file-tree.js +1 -1
  10. package/lib/public/js/components/file-viewer/constants.js +4 -3
  11. package/lib/public/js/components/file-viewer/editor-surface.js +1 -0
  12. package/lib/public/js/components/file-viewer/index.js +4 -0
  13. package/lib/public/js/components/file-viewer/storage.js +1 -4
  14. package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +130 -17
  15. package/lib/public/js/components/file-viewer/use-file-viewer.js +4 -0
  16. package/lib/public/js/components/google/gmail-setup-wizard.js +18 -51
  17. package/lib/public/js/components/google/gmail-watch-toggle.js +4 -1
  18. package/lib/public/js/components/onboarding/use-welcome-storage.js +2 -1
  19. package/lib/public/js/components/telegram-workspace/index.js +5 -2
  20. package/lib/public/js/hooks/useAgentSessions.js +128 -0
  21. package/lib/public/js/lib/browse-draft-state.js +9 -13
  22. package/lib/public/js/lib/storage-keys.js +28 -0
  23. package/lib/public/js/lib/ui-settings.js +3 -1
  24. package/lib/server/doctor/normalize.js +57 -23
  25. package/lib/server/doctor/prompt.js +30 -4
  26. package/lib/server/doctor/service.js +46 -0
  27. package/lib/server/doctor/workspace-fingerprint.js +46 -6
  28. package/package.json +1 -1
@@ -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
  }
@@ -47,12 +47,22 @@
47
47
  .app-content {
48
48
  grid-column: 3;
49
49
  grid-row: 2;
50
- overflow-y: auto;
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
 
@@ -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);
@@ -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
- if (view === "diff" && encodedPath) return `${baseRoute}?view=diff`;
713
- return baseRoute;
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 handleAppContentScroll = (e) => {
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
- class=${`app-content ${isBrowseRoute ? "browse-mode" : ""}`}
1022
- onscroll=${handleAppContentScroll}
1023
- >
1024
- <div class=${`mobile-topbar ${mobileTopbarScrolled ? "is-scrolled" : ""}`}>
1025
- <button
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
- <div style=${{ display: isBrowseRoute ? "none" : "block" }}>
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
- ${!isBrowseRoute && location !== "/general"
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-50 space-y-2 pointer-events-none"
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 renderEvidenceLine = (item = {}) => {
14
- if (item?.path) return item.path;
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
- (targetPath) => html`
103
- <span
104
- class="text-[11px] px-2 py-1 rounded-md bg-black/30 border border-border font-mono text-gray-300"
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
- ${targetPath}
107
- </span>
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-1">
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(item)}
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, 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
- import { Badge } from "../badge.js";
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 { getDoctorPriorityTone } from "./helpers.js";
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 [sessions, setSessions] = useState([]);
24
- const [selectedSessionKey, setSelectedSessionKey] = useState("");
25
- const [loadingSessions, setLoadingSessions] = useState(false);
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("Doctor fix request sent and finding marked fixed", "success");
48
+ showToast(
49
+ "Doctor fix request sent and finding marked fixed",
50
+ "success",
51
+ );
95
52
  } catch (statusError) {
96
53
  showToast(
97
- statusError.message || "Doctor fix request sent, but could not mark the finding fixed",
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-sm text-gray-400">
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
- 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) => {
@@ -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 markers = [];
109
+ const highPriority = [];
116
110
  if (Number(run?.priorityCounts?.P0 || 0) > 0) {
117
- markers.push({
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
- markers.push({
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 markers;
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 = () => [