@chrysb/alphaclaw 0.4.6-beta.6 → 0.4.6-beta.8
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/agent-send-modal.js +142 -0
- package/lib/public/js/components/doctor/fix-card-modal.js +15 -89
- package/lib/public/js/components/doctor/helpers.js +71 -5
- package/lib/public/js/components/doctor/index.js +89 -28
- package/lib/public/js/components/gateway.js +1 -0
- package/lib/public/js/components/google/gmail-setup-wizard.js +28 -4
- package/lib/public/js/components/webhooks.js +103 -2
- package/lib/server/constants.js +6 -0
- package/lib/server/doctor/bootstrap-context.js +191 -0
- package/lib/server/doctor/prompt.js +20 -4
- package/lib/server/doctor/service.js +18 -4
- package/lib/server/gateway.js +41 -1
- package/lib/server/gmail-watch.js +6 -3
- package/lib/server/onboarding/index.js +14 -1
- package/lib/server/routes/system.js +26 -12
- package/lib/server/watchdog.js +2 -2
- package/lib/server.js +1 -2
- package/package.json +1 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useEffect, useState } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
import { ModalShell } from "./modal-shell.js";
|
|
5
|
+
import { ActionButton } from "./action-button.js";
|
|
6
|
+
import { PageHeader } from "./page-header.js";
|
|
7
|
+
import { CloseIcon } from "./icons.js";
|
|
8
|
+
import { useAgentSessions } from "../hooks/useAgentSessions.js";
|
|
9
|
+
|
|
10
|
+
const html = htm.bind(h);
|
|
11
|
+
|
|
12
|
+
export const AgentSendModal = ({
|
|
13
|
+
visible = false,
|
|
14
|
+
title = "Send to agent",
|
|
15
|
+
messageLabel = "Message",
|
|
16
|
+
messageRows = 8,
|
|
17
|
+
initialMessage = "",
|
|
18
|
+
resetKey = "",
|
|
19
|
+
submitLabel = "Send message",
|
|
20
|
+
loadingLabel = "Sending...",
|
|
21
|
+
cancelLabel = "Cancel",
|
|
22
|
+
onClose = () => {},
|
|
23
|
+
onSubmit = async () => true,
|
|
24
|
+
sessionFilter = undefined,
|
|
25
|
+
}) => {
|
|
26
|
+
const {
|
|
27
|
+
sessions,
|
|
28
|
+
selectedSessionKey,
|
|
29
|
+
setSelectedSessionKey,
|
|
30
|
+
selectedSession,
|
|
31
|
+
loading: loadingSessions,
|
|
32
|
+
error: loadError,
|
|
33
|
+
} = useAgentSessions({ enabled: visible, filter: sessionFilter });
|
|
34
|
+
const [messageText, setMessageText] = useState("");
|
|
35
|
+
const [sending, setSending] = useState(false);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!visible) return;
|
|
39
|
+
setMessageText(String(initialMessage || ""));
|
|
40
|
+
}, [visible, initialMessage, resetKey]);
|
|
41
|
+
|
|
42
|
+
const handleSend = async () => {
|
|
43
|
+
if (!selectedSession || sending) return;
|
|
44
|
+
const trimmedMessage = String(messageText || "").trim();
|
|
45
|
+
if (!trimmedMessage) return;
|
|
46
|
+
setSending(true);
|
|
47
|
+
try {
|
|
48
|
+
const shouldClose = await onSubmit({
|
|
49
|
+
selectedSession,
|
|
50
|
+
selectedSessionKey,
|
|
51
|
+
message: trimmedMessage,
|
|
52
|
+
});
|
|
53
|
+
if (shouldClose !== false) {
|
|
54
|
+
onClose();
|
|
55
|
+
}
|
|
56
|
+
} finally {
|
|
57
|
+
setSending(false);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return html`
|
|
62
|
+
<${ModalShell}
|
|
63
|
+
visible=${visible}
|
|
64
|
+
onClose=${() => {
|
|
65
|
+
if (sending) return;
|
|
66
|
+
onClose();
|
|
67
|
+
}}
|
|
68
|
+
panelClassName="bg-modal border border-border rounded-xl p-5 max-w-lg w-full space-y-4"
|
|
69
|
+
>
|
|
70
|
+
<${PageHeader}
|
|
71
|
+
title=${title}
|
|
72
|
+
actions=${html`
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
onclick=${() => {
|
|
76
|
+
if (sending) return;
|
|
77
|
+
onClose();
|
|
78
|
+
}}
|
|
79
|
+
class="h-8 w-8 inline-flex items-center justify-center rounded-lg ac-btn-secondary"
|
|
80
|
+
aria-label="Close modal"
|
|
81
|
+
>
|
|
82
|
+
<${CloseIcon} className="w-3.5 h-3.5 text-gray-300" />
|
|
83
|
+
</button>
|
|
84
|
+
`}
|
|
85
|
+
/>
|
|
86
|
+
<div class="space-y-2">
|
|
87
|
+
<label class="text-xs text-gray-500">Send to session</label>
|
|
88
|
+
<select
|
|
89
|
+
value=${selectedSessionKey}
|
|
90
|
+
onChange=${(event) =>
|
|
91
|
+
setSelectedSessionKey(String(event.currentTarget?.value || ""))}
|
|
92
|
+
disabled=${loadingSessions || sending}
|
|
93
|
+
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-xs text-gray-200 focus:border-gray-500"
|
|
94
|
+
>
|
|
95
|
+
${sessions.length === 0
|
|
96
|
+
? html`<option value="">No sessions available</option>`
|
|
97
|
+
: null}
|
|
98
|
+
${sessions.map(
|
|
99
|
+
(sessionRow) => html`
|
|
100
|
+
<option value=${String(sessionRow?.key || "")}>
|
|
101
|
+
${String(sessionRow?.label || sessionRow?.key || "Session")}
|
|
102
|
+
</option>
|
|
103
|
+
`,
|
|
104
|
+
)}
|
|
105
|
+
</select>
|
|
106
|
+
${loadingSessions
|
|
107
|
+
? html`<div class="text-xs text-gray-500">Loading sessions...</div>`
|
|
108
|
+
: null}
|
|
109
|
+
${loadError ? html`<div class="text-xs text-red-400">${loadError}</div>` : null}
|
|
110
|
+
</div>
|
|
111
|
+
<div class="space-y-2">
|
|
112
|
+
<label class="text-xs text-gray-500">${messageLabel}</label>
|
|
113
|
+
<textarea
|
|
114
|
+
value=${messageText}
|
|
115
|
+
onInput=${(event) =>
|
|
116
|
+
setMessageText(String(event.currentTarget?.value || ""))}
|
|
117
|
+
disabled=${sending}
|
|
118
|
+
rows=${messageRows}
|
|
119
|
+
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-xs text-gray-200 focus:border-gray-500 font-mono leading-5"
|
|
120
|
+
></textarea>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="flex items-center justify-end gap-2">
|
|
123
|
+
<${ActionButton}
|
|
124
|
+
onClick=${onClose}
|
|
125
|
+
disabled=${sending}
|
|
126
|
+
tone="secondary"
|
|
127
|
+
size="md"
|
|
128
|
+
idleLabel=${cancelLabel}
|
|
129
|
+
/>
|
|
130
|
+
<${ActionButton}
|
|
131
|
+
onClick=${handleSend}
|
|
132
|
+
disabled=${!selectedSession || loadingSessions || !!loadError || !String(messageText || "").trim()}
|
|
133
|
+
loading=${sending}
|
|
134
|
+
tone="primary"
|
|
135
|
+
size="md"
|
|
136
|
+
idleLabel=${submitLabel}
|
|
137
|
+
loadingLabel=${loadingLabel}
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
</${ModalShell}>
|
|
141
|
+
`;
|
|
142
|
+
};
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
|
-
import { useEffect, useState } from "https://esm.sh/preact/hooks";
|
|
3
2
|
import htm from "https://esm.sh/htm";
|
|
4
|
-
import { ModalShell } from "../modal-shell.js";
|
|
5
|
-
import { ActionButton } from "../action-button.js";
|
|
6
3
|
import { sendDoctorCardFix, updateDoctorCardStatus } from "../../lib/api.js";
|
|
7
4
|
import { showToast } from "../toast.js";
|
|
8
|
-
import {
|
|
5
|
+
import { AgentSendModal } from "../agent-send-modal.js";
|
|
9
6
|
|
|
10
7
|
const html = htm.bind(h);
|
|
11
8
|
|
|
@@ -15,33 +12,15 @@ export const DoctorFixCardModal = ({
|
|
|
15
12
|
onClose = () => {},
|
|
16
13
|
onComplete = () => {},
|
|
17
14
|
}) => {
|
|
18
|
-
const {
|
|
19
|
-
|
|
20
|
-
selectedSessionKey,
|
|
21
|
-
setSelectedSessionKey,
|
|
22
|
-
selectedSession,
|
|
23
|
-
loading: loadingSessions,
|
|
24
|
-
error: loadError,
|
|
25
|
-
} = useAgentSessions({ enabled: visible });
|
|
26
|
-
|
|
27
|
-
const [sending, setSending] = useState(false);
|
|
28
|
-
const [promptText, setPromptText] = useState("");
|
|
29
|
-
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
if (!visible) return;
|
|
32
|
-
setPromptText(String(card?.fixPrompt || ""));
|
|
33
|
-
}, [visible, card?.fixPrompt, card?.id]);
|
|
34
|
-
|
|
35
|
-
const handleSend = async () => {
|
|
36
|
-
if (!card?.id || sending) return;
|
|
15
|
+
const handleSend = async ({ selectedSession, message }) => {
|
|
16
|
+
if (!card?.id) return false;
|
|
37
17
|
try {
|
|
38
|
-
setSending(true);
|
|
39
18
|
await sendDoctorCardFix({
|
|
40
19
|
cardId: card.id,
|
|
41
20
|
sessionId: selectedSession?.sessionId || "",
|
|
42
21
|
replyChannel: selectedSession?.replyChannel || "",
|
|
43
22
|
replyTo: selectedSession?.replyTo || "",
|
|
44
|
-
prompt:
|
|
23
|
+
prompt: message,
|
|
45
24
|
});
|
|
46
25
|
try {
|
|
47
26
|
await updateDoctorCardStatus({ cardId: card.id, status: "fixed" });
|
|
@@ -57,77 +36,24 @@ export const DoctorFixCardModal = ({
|
|
|
57
36
|
);
|
|
58
37
|
}
|
|
59
38
|
await onComplete();
|
|
60
|
-
|
|
39
|
+
return true;
|
|
61
40
|
} catch (error) {
|
|
62
41
|
showToast(error.message || "Could not send Doctor fix request", "error");
|
|
63
|
-
|
|
64
|
-
setSending(false);
|
|
42
|
+
return false;
|
|
65
43
|
}
|
|
66
44
|
};
|
|
67
45
|
|
|
68
46
|
return html`
|
|
69
|
-
<${
|
|
47
|
+
<${AgentSendModal}
|
|
70
48
|
visible=${visible}
|
|
49
|
+
title="Ask agent to fix"
|
|
50
|
+
messageLabel="Instructions"
|
|
51
|
+
initialMessage=${String(card?.fixPrompt || "")}
|
|
52
|
+
resetKey=${String(card?.id || "")}
|
|
53
|
+
submitLabel="Send fix request"
|
|
54
|
+
loadingLabel="Sending..."
|
|
71
55
|
onClose=${onClose}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
<div class="space-y-1">
|
|
75
|
-
<h2 class="text-base font-semibold">Ask agent to fix</h2>
|
|
76
|
-
<p class="text-xs text-gray-400">
|
|
77
|
-
Send this Doctor finding to one of your agent sessions as a focused fix request.
|
|
78
|
-
</p>
|
|
79
|
-
</div>
|
|
80
|
-
<div class="space-y-2">
|
|
81
|
-
<label class="text-xs text-gray-500">Send to session</label>
|
|
82
|
-
<select
|
|
83
|
-
value=${selectedSessionKey}
|
|
84
|
-
onChange=${(event) => setSelectedSessionKey(String(event.currentTarget?.value || ""))}
|
|
85
|
-
disabled=${loadingSessions || sending}
|
|
86
|
-
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-xs text-gray-200 focus:border-gray-500"
|
|
87
|
-
>
|
|
88
|
-
${sessions.map(
|
|
89
|
-
(sessionRow) => html`
|
|
90
|
-
<option value=${String(sessionRow?.key || "")}>
|
|
91
|
-
${String(sessionRow?.label || sessionRow?.key || "Session")}
|
|
92
|
-
</option>
|
|
93
|
-
`,
|
|
94
|
-
)}
|
|
95
|
-
</select>
|
|
96
|
-
${
|
|
97
|
-
loadingSessions
|
|
98
|
-
? html`<div class="text-xs text-gray-500">Loading sessions...</div>`
|
|
99
|
-
: null
|
|
100
|
-
}
|
|
101
|
-
${loadError ? html`<div class="text-xs text-red-400">${loadError}</div>` : null}
|
|
102
|
-
</div>
|
|
103
|
-
<div class="space-y-2">
|
|
104
|
-
<label class="text-xs text-gray-500">Instructions</label>
|
|
105
|
-
<textarea
|
|
106
|
-
value=${promptText}
|
|
107
|
-
onInput=${(event) => setPromptText(String(event.currentTarget?.value || ""))}
|
|
108
|
-
disabled=${sending}
|
|
109
|
-
rows="8"
|
|
110
|
-
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-xs text-gray-200 focus:border-gray-500 font-mono leading-5"
|
|
111
|
-
></textarea>
|
|
112
|
-
</div>
|
|
113
|
-
<div class="flex items-center justify-end gap-2">
|
|
114
|
-
<${ActionButton}
|
|
115
|
-
onClick=${onClose}
|
|
116
|
-
disabled=${sending}
|
|
117
|
-
tone="secondary"
|
|
118
|
-
size="md"
|
|
119
|
-
idleLabel="Cancel"
|
|
120
|
-
/>
|
|
121
|
-
<${ActionButton}
|
|
122
|
-
onClick=${handleSend}
|
|
123
|
-
disabled=${!selectedSession || loadingSessions || !!loadError || !String(promptText || "").trim()}
|
|
124
|
-
loading=${sending}
|
|
125
|
-
tone="primary"
|
|
126
|
-
size="md"
|
|
127
|
-
idleLabel="Send fix request"
|
|
128
|
-
loadingLabel="Sending..."
|
|
129
|
-
/>
|
|
130
|
-
</div>
|
|
131
|
-
</${ModalShell}>
|
|
56
|
+
onSubmit=${handleSend}
|
|
57
|
+
/>
|
|
132
58
|
`;
|
|
133
59
|
};
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
export const getDoctorPriorityTone = (priority = "") => {
|
|
2
|
-
const normalized = String(priority || "")
|
|
2
|
+
const normalized = String(priority || "")
|
|
3
|
+
.trim()
|
|
4
|
+
.toUpperCase();
|
|
3
5
|
if (normalized === "P0") return "danger";
|
|
4
6
|
if (normalized === "P1") return "warning";
|
|
5
7
|
return "neutral";
|
|
6
8
|
};
|
|
7
9
|
|
|
8
10
|
export const getDoctorStatusTone = (status = "") => {
|
|
9
|
-
const normalized = String(status || "")
|
|
11
|
+
const normalized = String(status || "")
|
|
12
|
+
.trim()
|
|
13
|
+
.toLowerCase();
|
|
10
14
|
if (normalized === "fixed") return "success";
|
|
11
15
|
if (normalized === "dismissed") return "neutral";
|
|
12
16
|
return "warning";
|
|
@@ -35,7 +39,9 @@ export const formatDoctorCategory = (category = "") => {
|
|
|
35
39
|
export const buildDoctorPriorityCounts = (cards = []) =>
|
|
36
40
|
cards.reduce(
|
|
37
41
|
(totals, card) => {
|
|
38
|
-
const priority = String(card?.priority || "")
|
|
42
|
+
const priority = String(card?.priority || "")
|
|
43
|
+
.trim()
|
|
44
|
+
.toUpperCase();
|
|
39
45
|
if (priority === "P0" || priority === "P1" || priority === "P2") {
|
|
40
46
|
totals[priority] += 1;
|
|
41
47
|
}
|
|
@@ -47,7 +53,9 @@ export const buildDoctorPriorityCounts = (cards = []) =>
|
|
|
47
53
|
export const groupDoctorCardsByStatus = (cards = []) =>
|
|
48
54
|
cards.reduce(
|
|
49
55
|
(groups, card) => {
|
|
50
|
-
const status = String(card?.status || "open")
|
|
56
|
+
const status = String(card?.status || "open")
|
|
57
|
+
.trim()
|
|
58
|
+
.toLowerCase();
|
|
51
59
|
if (status === "fixed") {
|
|
52
60
|
groups.fixed.push(card);
|
|
53
61
|
return groups;
|
|
@@ -74,13 +82,71 @@ export const shouldShowDoctorWarning = (
|
|
|
74
82
|
|
|
75
83
|
export const getDoctorWarningMessage = (doctorStatus = null) => {
|
|
76
84
|
if (!doctorStatus) return "";
|
|
77
|
-
const changedFilesCount = Number(
|
|
85
|
+
const changedFilesCount = Number(
|
|
86
|
+
doctorStatus.changeSummary?.changedFilesCount || 0,
|
|
87
|
+
);
|
|
78
88
|
if (changedFilesCount > 0) {
|
|
79
89
|
return `Drift Doctor has not been run in the last week and ${changedFilesCount} file${changedFilesCount === 1 ? "" : "s"} changed since the last review.`;
|
|
80
90
|
}
|
|
81
91
|
return "Doctor has not been run in the last week.";
|
|
82
92
|
};
|
|
83
93
|
|
|
94
|
+
export const formatDoctorCharCount = (value = 0) =>
|
|
95
|
+
`${Number(value || 0).toLocaleString()} chars`;
|
|
96
|
+
|
|
97
|
+
const isManagedBootstrapContextPath = (filePath = "") =>
|
|
98
|
+
String(filePath || "").startsWith("hooks/bootstrap/");
|
|
99
|
+
|
|
100
|
+
export const getDoctorBootstrapTruncationItems = (doctorStatus = null) => {
|
|
101
|
+
const bootstrapContext = doctorStatus?.bootstrapContext;
|
|
102
|
+
const truncatedFiles = (bootstrapContext?.activeTruncatedFiles || []).filter(
|
|
103
|
+
(file) => !isManagedBootstrapContextPath(file?.path),
|
|
104
|
+
);
|
|
105
|
+
const nearLimitFiles = (bootstrapContext?.activeNearLimitFiles || []).filter(
|
|
106
|
+
(file) => !isManagedBootstrapContextPath(file?.path),
|
|
107
|
+
);
|
|
108
|
+
return [
|
|
109
|
+
...truncatedFiles.map((file) => ({
|
|
110
|
+
path: file.path,
|
|
111
|
+
size: formatDoctorCharCount(file.rawChars),
|
|
112
|
+
statusText: `-${Number(
|
|
113
|
+
Math.max(
|
|
114
|
+
0,
|
|
115
|
+
Number(file.rawChars || 0) - Number(file.injectedChars || 0),
|
|
116
|
+
),
|
|
117
|
+
).toLocaleString()} cut`,
|
|
118
|
+
statusTone: "danger",
|
|
119
|
+
})),
|
|
120
|
+
...nearLimitFiles.map((file) => ({
|
|
121
|
+
path: file.path,
|
|
122
|
+
size: formatDoctorCharCount(file.rawChars),
|
|
123
|
+
statusText: "Near limit",
|
|
124
|
+
statusTone: "warning",
|
|
125
|
+
})),
|
|
126
|
+
];
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const hasDoctorBootstrapWarnings = (doctorStatus = null) =>
|
|
130
|
+
getDoctorBootstrapTruncationItems(doctorStatus).length > 0;
|
|
131
|
+
|
|
132
|
+
export const getDoctorBootstrapWarningTitle = (doctorStatus = null) => {
|
|
133
|
+
const items = getDoctorBootstrapTruncationItems(doctorStatus);
|
|
134
|
+
if (!items.length) return "";
|
|
135
|
+
const hasTruncatedItems = items.some((item) => item.statusTone === "danger");
|
|
136
|
+
const hasNearLimitItems = items.some((item) => item.statusTone === "warning");
|
|
137
|
+
if (hasTruncatedItems && hasNearLimitItems) {
|
|
138
|
+
return "Some of your main files are being truncated or nearing the limit:";
|
|
139
|
+
}
|
|
140
|
+
if (hasNearLimitItems) {
|
|
141
|
+
return items.length === 1
|
|
142
|
+
? "One of your main files is nearing the limit:"
|
|
143
|
+
: "Some of your main files are nearing the limit:";
|
|
144
|
+
}
|
|
145
|
+
return items.length === 1
|
|
146
|
+
? "One of your main files is being truncated:"
|
|
147
|
+
: "Some of your main files are being truncated:";
|
|
148
|
+
};
|
|
149
|
+
|
|
84
150
|
export const getDoctorChangeLabel = (changeSummary = null) => {
|
|
85
151
|
const changedFilesCount = Number(changeSummary?.changedFilesCount || 0);
|
|
86
152
|
if (changedFilesCount === 0) return "No changes since last run";
|
|
@@ -20,8 +20,11 @@ import { DoctorFixCardModal } from "./fix-card-modal.js";
|
|
|
20
20
|
import {
|
|
21
21
|
buildDoctorRunMarkers,
|
|
22
22
|
buildDoctorStatusFilterOptions,
|
|
23
|
+
getDoctorBootstrapTruncationItems,
|
|
24
|
+
getDoctorBootstrapWarningTitle,
|
|
23
25
|
getDoctorChangeLabel,
|
|
24
26
|
getDoctorRunPillDetail,
|
|
27
|
+
hasDoctorBootstrapWarnings,
|
|
25
28
|
shouldShowDoctorWarning,
|
|
26
29
|
} from "./helpers.js";
|
|
27
30
|
|
|
@@ -182,6 +185,18 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
|
|
|
182
185
|
() => shouldShowDoctorWarning(doctorStatus, 0),
|
|
183
186
|
[doctorStatus],
|
|
184
187
|
);
|
|
188
|
+
const showBootstrapTruncationBanner = useMemo(
|
|
189
|
+
() => hasDoctorBootstrapWarnings(doctorStatus),
|
|
190
|
+
[doctorStatus],
|
|
191
|
+
);
|
|
192
|
+
const bootstrapTruncationMessage = useMemo(
|
|
193
|
+
() => getDoctorBootstrapWarningTitle(doctorStatus),
|
|
194
|
+
[doctorStatus],
|
|
195
|
+
);
|
|
196
|
+
const bootstrapTruncationItems = useMemo(
|
|
197
|
+
() => getDoctorBootstrapTruncationItems(doctorStatus),
|
|
198
|
+
[doctorStatus],
|
|
199
|
+
);
|
|
185
200
|
const hasCompletedDoctorRun = !!doctorStatus?.lastRunAt;
|
|
186
201
|
const hasRuns = runs.length > 0;
|
|
187
202
|
const hasLoadedRuns = runsPoll.data !== null || runsPoll.error !== null;
|
|
@@ -312,29 +327,76 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
|
|
|
312
327
|
: null}
|
|
313
328
|
${!showInitialLoadingState && hasRuns
|
|
314
329
|
? html`
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
330
|
+
<div class="space-y-3">
|
|
331
|
+
<${DoctorSummaryCards} cards=${openCards} />
|
|
332
|
+
<div class="space-y-3">
|
|
333
|
+
${hasCompletedDoctorRun
|
|
334
|
+
? html`
|
|
335
|
+
<div
|
|
336
|
+
class="bg-surface border border-border rounded-xl p-4 flex flex-wrap items-center justify-between gap-3"
|
|
337
|
+
>
|
|
338
|
+
<span class="text-xs text-gray-500">
|
|
339
|
+
Last run ·${" "}
|
|
340
|
+
<span class="text-gray-300">
|
|
341
|
+
${formatLocaleDateTime(doctorStatus?.lastRunAt, {
|
|
342
|
+
fallback: "Never",
|
|
343
|
+
})}
|
|
344
|
+
</span>
|
|
328
345
|
</span>
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
</
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
346
|
+
<span class="text-xs text-gray-500">
|
|
347
|
+
${changeLabel}
|
|
348
|
+
</span>
|
|
349
|
+
</div>
|
|
350
|
+
${showBootstrapTruncationBanner
|
|
351
|
+
? html`
|
|
352
|
+
<div
|
|
353
|
+
class="bg-surface border border-border rounded-xl p-4 space-y-3"
|
|
354
|
+
>
|
|
355
|
+
<div class="text-xs text-gray-400">
|
|
356
|
+
⚠️ ${bootstrapTruncationMessage}
|
|
357
|
+
</div>
|
|
358
|
+
<div class="space-y-2">
|
|
359
|
+
${bootstrapTruncationItems.map(
|
|
360
|
+
(item) => html`
|
|
361
|
+
<div
|
|
362
|
+
class="flex items-center justify-between gap-3 text-xs"
|
|
363
|
+
>
|
|
364
|
+
<button
|
|
365
|
+
type="button"
|
|
366
|
+
class="font-mono text-gray-200 ac-tip-link hover:underline text-left cursor-pointer"
|
|
367
|
+
onClick=${() => onOpenFile(String(item.path || ""))}
|
|
368
|
+
>
|
|
369
|
+
${item.path}
|
|
370
|
+
</button>
|
|
371
|
+
<span
|
|
372
|
+
class="flex items-center gap-3 whitespace-nowrap"
|
|
373
|
+
>
|
|
374
|
+
<span class="text-gray-500">
|
|
375
|
+
${item.size}
|
|
376
|
+
</span>
|
|
377
|
+
<span
|
|
378
|
+
class=${item.statusTone === "warning"
|
|
379
|
+
? "text-yellow-300"
|
|
380
|
+
: "text-red-300"}
|
|
381
|
+
>
|
|
382
|
+
${item.statusText}
|
|
383
|
+
</span>
|
|
384
|
+
</span>
|
|
385
|
+
</div>
|
|
386
|
+
`,
|
|
387
|
+
)}
|
|
388
|
+
</div>
|
|
389
|
+
<div class="border-t border-border"></div>
|
|
390
|
+
<p class="text-xs text-gray-500 leading-5">
|
|
391
|
+
Truncated files become partially hidden from
|
|
392
|
+
your agent and could cause drift.
|
|
393
|
+
</p>
|
|
394
|
+
</div>
|
|
395
|
+
`
|
|
396
|
+
: null}
|
|
397
|
+
`
|
|
398
|
+
: null}
|
|
399
|
+
${showDoctorStaleBanner
|
|
338
400
|
? html`
|
|
339
401
|
<div
|
|
340
402
|
class="text-xs text-yellow-300 bg-yellow-500/10 border border-yellow-500/35 rounded-lg px-3 py-2"
|
|
@@ -344,8 +406,8 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
|
|
|
344
406
|
changed.
|
|
345
407
|
</div>
|
|
346
408
|
`
|
|
347
|
-
: null
|
|
348
|
-
|
|
409
|
+
: null}
|
|
410
|
+
</div>
|
|
349
411
|
</div>
|
|
350
412
|
`
|
|
351
413
|
: null}
|
|
@@ -461,9 +523,7 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
|
|
|
461
523
|
${selectedRunIsInProgress
|
|
462
524
|
? html`
|
|
463
525
|
<div class="ac-surface-inset rounded-xl p-4">
|
|
464
|
-
<div
|
|
465
|
-
class="text-xs leading-5 text-gray-400"
|
|
466
|
-
>
|
|
526
|
+
<div class="text-xs leading-5 text-gray-400">
|
|
467
527
|
<span
|
|
468
528
|
>Run in progress. Findings will appear when analysis
|
|
469
529
|
completes.</span
|
|
@@ -479,7 +539,8 @@ export const DoctorTab = ({ isActive = false, onOpenFile = () => {} }) => {
|
|
|
479
539
|
onAskAgentFix=${setFixCard}
|
|
480
540
|
onUpdateStatus=${handleUpdateStatus}
|
|
481
541
|
onOpenFile=${onOpenFile}
|
|
482
|
-
changedPaths=${doctorStatus?.changeSummary?.changedPaths ||
|
|
542
|
+
changedPaths=${doctorStatus?.changeSummary?.changedPaths ||
|
|
543
|
+
[]}
|
|
483
544
|
showRunMeta=${selectedRunFilter === "all"}
|
|
484
545
|
hideEmptyState=${selectedRunIsInProgress}
|
|
485
546
|
/>
|
|
@@ -326,6 +326,7 @@ export const Gateway = ({
|
|
|
326
326
|
const showInspectButton = watchdogHealth === "degraded" && !!onOpenWatchdog;
|
|
327
327
|
const showRepairButton =
|
|
328
328
|
isRepairInProgress ||
|
|
329
|
+
(watchdogStatus?.health === "degraded" && !onOpenWatchdog) ||
|
|
329
330
|
watchdogStatus?.lifecycle === "crash_loop" ||
|
|
330
331
|
watchdogStatus?.health === "unhealthy" ||
|
|
331
332
|
watchdogStatus?.health === "crashed";
|
|
@@ -81,6 +81,7 @@ export const GmailSetupWizard = ({
|
|
|
81
81
|
}) => {
|
|
82
82
|
const [step, setStep] = useState(0);
|
|
83
83
|
const [projectIdInput, setProjectIdInput] = useState("");
|
|
84
|
+
const [editingProjectId, setEditingProjectId] = useState(false);
|
|
84
85
|
const [localError, setLocalError] = useState("");
|
|
85
86
|
const [projectIdResolved, setProjectIdResolved] = useState(false);
|
|
86
87
|
const [watchEnabled, setWatchEnabled] = useState(false);
|
|
@@ -100,6 +101,7 @@ export const GmailSetupWizard = ({
|
|
|
100
101
|
setStep(0);
|
|
101
102
|
setLocalError("");
|
|
102
103
|
setProjectIdInput("");
|
|
104
|
+
setEditingProjectId(false);
|
|
103
105
|
setProjectIdResolved(false);
|
|
104
106
|
setWatchEnabled(false);
|
|
105
107
|
setSendingToAgent(false);
|
|
@@ -110,10 +112,11 @@ export const GmailSetupWizard = ({
|
|
|
110
112
|
const hasProjectIdFromConfig = Boolean(
|
|
111
113
|
String(clientConfig?.projectId || "").trim() || commands,
|
|
112
114
|
);
|
|
113
|
-
const needsProjectId =
|
|
115
|
+
const needsProjectId =
|
|
116
|
+
editingProjectId || (!hasProjectIdFromConfig && !projectIdResolved);
|
|
114
117
|
const detectedProjectId =
|
|
115
|
-
String(clientConfig?.projectId || "").trim() ||
|
|
116
118
|
String(projectIdInput || "").trim() ||
|
|
119
|
+
String(clientConfig?.projectId || "").trim() ||
|
|
117
120
|
"<project-id>";
|
|
118
121
|
const client =
|
|
119
122
|
String(account?.client || clientConfig?.client || "default").trim() ||
|
|
@@ -135,6 +138,13 @@ export const GmailSetupWizard = ({
|
|
|
135
138
|
showToast("Could not copy text", "error");
|
|
136
139
|
}, []);
|
|
137
140
|
|
|
141
|
+
const handleChangeProjectId = useCallback(() => {
|
|
142
|
+
setLocalError("");
|
|
143
|
+
setProjectIdInput(String(clientConfig?.projectId || "").trim());
|
|
144
|
+
setProjectIdResolved(false);
|
|
145
|
+
setEditingProjectId(true);
|
|
146
|
+
}, [clientConfig?.projectId]);
|
|
147
|
+
|
|
138
148
|
const handleFinish = async () => {
|
|
139
149
|
try {
|
|
140
150
|
setLocalError("");
|
|
@@ -159,6 +169,7 @@ export const GmailSetupWizard = ({
|
|
|
159
169
|
client,
|
|
160
170
|
projectId: String(projectIdInput || "").trim(),
|
|
161
171
|
});
|
|
172
|
+
setEditingProjectId(false);
|
|
162
173
|
setProjectIdResolved(true);
|
|
163
174
|
} catch (err) {
|
|
164
175
|
setLocalError(err.message || "Could not save project id");
|
|
@@ -230,7 +241,9 @@ export const GmailSetupWizard = ({
|
|
|
230
241
|
<div
|
|
231
242
|
class="rounded-lg border border-border bg-black/20 p-3 space-y-2"
|
|
232
243
|
>
|
|
233
|
-
<div class="text-sm">
|
|
244
|
+
<div class="text-sm">
|
|
245
|
+
${editingProjectId ? "Change project ID" : "Project ID required"}
|
|
246
|
+
</div>
|
|
234
247
|
<div class="text-xs text-gray-500">
|
|
235
248
|
Find it in the${" "}
|
|
236
249
|
<a
|
|
@@ -257,6 +270,9 @@ export const GmailSetupWizard = ({
|
|
|
257
270
|
!needsProjectId && step === 0
|
|
258
271
|
? html`
|
|
259
272
|
<div class="space-y-1">
|
|
273
|
+
<div class="text-xs text-gray-500">
|
|
274
|
+
Using project <code>${detectedProjectId}</code>.
|
|
275
|
+
</div>
|
|
260
276
|
<div class="text-xs text-gray-500">
|
|
261
277
|
If <code>gcloud</code> is not installed on your computer,
|
|
262
278
|
follow the official install guide:${" "}
|
|
@@ -372,7 +388,15 @@ export const GmailSetupWizard = ({
|
|
|
372
388
|
}
|
|
373
389
|
<div class="grid grid-cols-2 gap-2 pt-2">
|
|
374
390
|
${step === 0
|
|
375
|
-
? html
|
|
391
|
+
? html`${!needsProjectId
|
|
392
|
+
? html`<button
|
|
393
|
+
type="button"
|
|
394
|
+
onclick=${handleChangeProjectId}
|
|
395
|
+
class="justify-self-start text-xs px-2 py-1 rounded-lg ac-btn-ghost"
|
|
396
|
+
>
|
|
397
|
+
Change project ID
|
|
398
|
+
</button>`
|
|
399
|
+
: html`<div></div>`}`
|
|
376
400
|
: html`<${ActionButton}
|
|
377
401
|
onClick=${() => setStep((prev) => Math.max(prev - 1, 0))}
|
|
378
402
|
disabled=${saving}
|