@chrysb/alphaclaw 0.4.6-beta.5 → 0.4.6-beta.7
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/envars.js +146 -29
- package/lib/public/js/components/features.js +1 -1
- 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/icons.js +52 -0
- package/lib/public/js/components/info-tooltip.js +4 -7
- package/lib/public/js/components/models-tab/provider-auth-card.js +2 -2
- package/lib/public/js/components/models.js +1 -1
- package/lib/public/js/components/providers.js +1 -1
- package/lib/public/js/components/tooltip.js +106 -0
- package/lib/public/js/components/webhooks.js +103 -2
- package/lib/public/js/components/welcome.js +1 -1
- package/lib/public/js/lib/model-config.js +1 -0
- package/lib/server/auth-profiles.js +67 -0
- package/lib/server/constants.js +30 -8
- package/lib/server/doctor/service.js +0 -3
- package/lib/server/gateway.js +68 -29
- package/lib/server/gmail-watch.js +6 -3
- package/lib/server/onboarding/index.js +16 -1
- package/lib/server/onboarding/validation.js +2 -2
- package/lib/server/routes/models.js +44 -0
- package/lib/server/routes/onboarding.js +2 -0
- package/lib/server/routes/system.js +66 -11
- package/lib/server/watchdog.js +2 -2
- package/lib/server.js +5 -1
- package/lib/setup/env.template +1 -0
- 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
|
};
|
|
@@ -6,16 +6,44 @@ import { showToast } from "./toast.js";
|
|
|
6
6
|
import { SecretInput } from "./secret-input.js";
|
|
7
7
|
import { PageHeader } from "./page-header.js";
|
|
8
8
|
import { ActionButton } from "./action-button.js";
|
|
9
|
+
import {
|
|
10
|
+
Brain2LineIcon,
|
|
11
|
+
ChatVoiceLineIcon,
|
|
12
|
+
ChevronDownIcon,
|
|
13
|
+
ImageAiLineIcon,
|
|
14
|
+
TextToSpeechLineIcon,
|
|
15
|
+
} from "./icons.js";
|
|
16
|
+
import { Tooltip } from "./tooltip.js";
|
|
9
17
|
const html = htm.bind(h);
|
|
10
18
|
|
|
11
19
|
const kGroupLabels = {
|
|
20
|
+
ai: "AI Provider Keys",
|
|
12
21
|
github: "GitHub",
|
|
13
22
|
channels: "Channels",
|
|
14
23
|
tools: "Tools",
|
|
15
24
|
custom: "Custom",
|
|
16
25
|
};
|
|
17
26
|
|
|
18
|
-
const kGroupOrder = ["github", "channels", "tools", "custom"];
|
|
27
|
+
const kGroupOrder = ["ai", "github", "channels", "tools", "custom"];
|
|
28
|
+
const kDefaultVisibleAiKeys = new Set(["OPENAI_API_KEY", "GEMINI_API_KEY"]);
|
|
29
|
+
const kFeatureIconByName = {
|
|
30
|
+
Embeddings: {
|
|
31
|
+
Icon: Brain2LineIcon,
|
|
32
|
+
label: "Memory embeddings",
|
|
33
|
+
},
|
|
34
|
+
Image: {
|
|
35
|
+
Icon: ImageAiLineIcon,
|
|
36
|
+
label: "Image generation",
|
|
37
|
+
},
|
|
38
|
+
TTS: {
|
|
39
|
+
Icon: TextToSpeechLineIcon,
|
|
40
|
+
label: "Text to speech",
|
|
41
|
+
},
|
|
42
|
+
STT: {
|
|
43
|
+
Icon: ChatVoiceLineIcon,
|
|
44
|
+
label: "Speech to text",
|
|
45
|
+
},
|
|
46
|
+
};
|
|
19
47
|
const normalizeEnvVarKey = (raw) => raw.trim().toUpperCase().replace(/[^A-Z0-9_]/g, "_");
|
|
20
48
|
const stripSurroundingQuotes = (raw) => {
|
|
21
49
|
const value = String(raw || "").trim();
|
|
@@ -57,27 +85,80 @@ const kHintByKey = {
|
|
|
57
85
|
ANTHROPIC_TOKEN: html`from <code class="text-xs bg-black/30 px-1 rounded">claude setup-token</code>`,
|
|
58
86
|
OPENAI_API_KEY: html`from <a href="https://platform.openai.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">platform.openai.com</a>`,
|
|
59
87
|
GEMINI_API_KEY: html`from <a href="https://aistudio.google.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">aistudio.google.com</a>`,
|
|
88
|
+
ELEVENLABS_API_KEY: html`from <a href="https://elevenlabs.io" target="_blank" class="hover:underline" style="color: var(--accent-link)">elevenlabs.io</a> · <code class="text-xs bg-black/30 px-1 rounded">XI_API_KEY</code> also supported`,
|
|
60
89
|
GITHUB_TOKEN: html`classic PAT · <code class="text-xs bg-black/30 px-1 rounded">repo</code> scope · <a href="https://github.com/settings/tokens" target="_blank" class="hover:underline" style="color: var(--accent-link)">github settings</a>`,
|
|
61
90
|
GITHUB_WORKSPACE_REPO: html`use <code class="text-xs bg-black/30 px-1 rounded">owner/repo</code> or <code class="text-xs bg-black/30 px-1 rounded">https://github.com/owner/repo</code>`,
|
|
62
91
|
TELEGRAM_BOT_TOKEN: html`from <a href="https://t.me/BotFather" target="_blank" class="hover:underline" style="color: var(--accent-link)">@BotFather</a> · <a href="https://docs.openclaw.ai/channels/telegram" target="_blank" class="hover:underline" style="color: var(--accent-link)">full guide</a>`,
|
|
63
92
|
DISCORD_BOT_TOKEN: html`from <a href="https://discord.com/developers/applications" target="_blank" class="hover:underline" style="color: var(--accent-link)">developer portal</a> · <a href="https://docs.openclaw.ai/channels/discord" target="_blank" class="hover:underline" style="color: var(--accent-link)">full guide</a>`,
|
|
93
|
+
MISTRAL_API_KEY: html`from <a href="https://console.mistral.ai" target="_blank" class="hover:underline" style="color: var(--accent-link)">console.mistral.ai</a>`,
|
|
94
|
+
VOYAGE_API_KEY: html`from <a href="https://dash.voyageai.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">dash.voyageai.com</a>`,
|
|
95
|
+
GROQ_API_KEY: html`from <a href="https://console.groq.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">console.groq.com</a>`,
|
|
96
|
+
DEEPGRAM_API_KEY: html`from <a href="https://console.deepgram.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">console.deepgram.com</a>`,
|
|
64
97
|
BRAVE_API_KEY: html`from <a href="https://brave.com/search/api/" target="_blank" class="hover:underline" style="color: var(--accent-link)">brave.com/search/api</a> — free tier available`,
|
|
65
98
|
};
|
|
66
99
|
|
|
67
100
|
const getHintContent = (envVar) => kHintByKey[envVar.key] || envVar.hint || "";
|
|
68
101
|
|
|
102
|
+
const getVisibleFeatureIcons = (envVar) =>
|
|
103
|
+
(Array.isArray(envVar?.features) ? envVar.features : []).filter(
|
|
104
|
+
(feature) => !!kFeatureIconByName[feature],
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const splitAiVars = (items) => {
|
|
108
|
+
const visible = [];
|
|
109
|
+
const hidden = [];
|
|
110
|
+
(items || []).forEach((item) => {
|
|
111
|
+
const hasValue = !!String(item?.value || "").trim();
|
|
112
|
+
if (kDefaultVisibleAiKeys.has(item?.key) || hasValue) {
|
|
113
|
+
visible.push(item);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
hidden.push(item);
|
|
117
|
+
});
|
|
118
|
+
return { visible, hidden };
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const FeatureIcon = ({ feature }) => {
|
|
122
|
+
const entry = kFeatureIconByName[feature];
|
|
123
|
+
if (!entry) return null;
|
|
124
|
+
const { Icon, label } = entry;
|
|
125
|
+
return html`
|
|
126
|
+
<${Tooltip} text=${label} widthClass="w-auto" tooltipClassName="whitespace-nowrap">
|
|
127
|
+
<span
|
|
128
|
+
class="inline-flex items-center justify-center text-gray-500 hover:text-gray-300 focus-within:text-gray-300"
|
|
129
|
+
tabindex="0"
|
|
130
|
+
aria-label=${label}
|
|
131
|
+
>
|
|
132
|
+
<${Icon} className="w-3.5 h-3.5" />
|
|
133
|
+
</span>
|
|
134
|
+
</${Tooltip}>
|
|
135
|
+
`;
|
|
136
|
+
};
|
|
137
|
+
|
|
69
138
|
const EnvRow = ({ envVar, onChange, onDelete, disabled }) => {
|
|
70
139
|
const hint = getHintContent(envVar);
|
|
140
|
+
const featureIcons = getVisibleFeatureIcons(envVar);
|
|
71
141
|
|
|
72
142
|
return html`
|
|
73
143
|
<div class="flex items-start gap-4 px-4 py-3">
|
|
74
|
-
<div class="shrink-0
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
144
|
+
<div class="shrink-0" style="width: 200px">
|
|
145
|
+
<div class="flex items-center gap-2 pt-1.5">
|
|
146
|
+
<span
|
|
147
|
+
class="inline-block w-1.5 h-1.5 rounded-full shrink-0 ${envVar.value
|
|
148
|
+
? "bg-green-500"
|
|
149
|
+
: "bg-gray-600"}"
|
|
150
|
+
/>
|
|
151
|
+
<code class="text-sm truncate">${envVar.key}</code>
|
|
152
|
+
</div>
|
|
153
|
+
${featureIcons.length > 0
|
|
154
|
+
? html`
|
|
155
|
+
<div class="flex items-center gap-2 mt-1 pl-3.5">
|
|
156
|
+
${featureIcons.map(
|
|
157
|
+
(feature) => html`<${FeatureIcon} key=${feature} feature=${feature} />`,
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
`
|
|
161
|
+
: null}
|
|
81
162
|
</div>
|
|
82
163
|
<div class="flex-1 min-w-0">
|
|
83
164
|
<div class="flex items-center gap-1">
|
|
@@ -114,6 +195,7 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
114
195
|
const [secretMaskEpoch, setSecretMaskEpoch] = useState(0);
|
|
115
196
|
const [dirty, setDirty] = useState(false);
|
|
116
197
|
const [saving, setSaving] = useState(false);
|
|
198
|
+
const [showAllAiKeys, setShowAllAiKeys] = useState(false);
|
|
117
199
|
const [newKey, setNewKey] = useState("");
|
|
118
200
|
const baselineSignatureRef = useRef("[]");
|
|
119
201
|
|
|
@@ -310,6 +392,61 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
310
392
|
const pendingAtBottom = grouped.custom.filter((item) => pending.has(item.key));
|
|
311
393
|
grouped.custom = [...nonPending, ...pendingAtBottom];
|
|
312
394
|
}
|
|
395
|
+
const aiSplit = splitAiVars(grouped.ai || []);
|
|
396
|
+
const renderEnvRows = (items) =>
|
|
397
|
+
items.map(
|
|
398
|
+
(v) =>
|
|
399
|
+
html`<${EnvRow}
|
|
400
|
+
key=${`${secretMaskEpoch}:${v.key}`}
|
|
401
|
+
envVar=${v}
|
|
402
|
+
onChange=${handleChange}
|
|
403
|
+
onDelete=${handleDelete}
|
|
404
|
+
disabled=${saving}
|
|
405
|
+
/>`,
|
|
406
|
+
);
|
|
407
|
+
const renderGroupCard = (groupKey) => {
|
|
408
|
+
const items = grouped[groupKey] || [];
|
|
409
|
+
if (!items.length) return null;
|
|
410
|
+
if (groupKey === "ai") {
|
|
411
|
+
const { visible, hidden } = aiSplit;
|
|
412
|
+
const expanded = showAllAiKeys && hidden.length > 0;
|
|
413
|
+
return html`
|
|
414
|
+
<div class="bg-surface border border-border rounded-xl overflow-hidden">
|
|
415
|
+
<h3 class="card-label text-xs px-4 pt-3 pb-2">
|
|
416
|
+
${kGroupLabels[groupKey] || groupKey}
|
|
417
|
+
</h3>
|
|
418
|
+
<div class="divide-y divide-border">${renderEnvRows(visible)}</div>
|
|
419
|
+
${hidden.length > 0
|
|
420
|
+
? html`
|
|
421
|
+
<div class="border-t border-border px-4 py-2">
|
|
422
|
+
<button
|
|
423
|
+
type="button"
|
|
424
|
+
onclick=${() => setShowAllAiKeys((prev) => !prev)}
|
|
425
|
+
class="inline-flex items-center gap-1.5 text-xs text-gray-500 hover:text-gray-300"
|
|
426
|
+
>
|
|
427
|
+
<${ChevronDownIcon}
|
|
428
|
+
className=${`transition-transform ${expanded ? "rotate-180" : ""}`}
|
|
429
|
+
/>
|
|
430
|
+
${expanded ? "Show fewer" : `Show more (${hidden.length})`}
|
|
431
|
+
</button>
|
|
432
|
+
</div>
|
|
433
|
+
`
|
|
434
|
+
: null}
|
|
435
|
+
${expanded
|
|
436
|
+
? html`<div class="divide-y divide-border border-t border-border">${renderEnvRows(hidden)}</div>`
|
|
437
|
+
: null}
|
|
438
|
+
</div>
|
|
439
|
+
`;
|
|
440
|
+
}
|
|
441
|
+
return html`
|
|
442
|
+
<div class="bg-surface border border-border rounded-xl overflow-hidden">
|
|
443
|
+
<h3 class="card-label text-xs px-4 pt-3 pb-2">
|
|
444
|
+
${kGroupLabels[groupKey] || groupKey}
|
|
445
|
+
</h3>
|
|
446
|
+
<div class="divide-y divide-border">${renderEnvRows(items)}</div>
|
|
447
|
+
</div>
|
|
448
|
+
`;
|
|
449
|
+
};
|
|
313
450
|
|
|
314
451
|
return html`
|
|
315
452
|
<div class="space-y-4">
|
|
@@ -331,27 +468,7 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
331
468
|
|
|
332
469
|
${kGroupOrder
|
|
333
470
|
.filter((g) => grouped[g]?.length)
|
|
334
|
-
.map(
|
|
335
|
-
(g) => html`
|
|
336
|
-
<div class="bg-surface border border-border rounded-xl overflow-hidden">
|
|
337
|
-
<h3 class="card-label text-xs px-4 pt-3 pb-2">
|
|
338
|
-
${kGroupLabels[g] || g}
|
|
339
|
-
</h3>
|
|
340
|
-
<div class="divide-y divide-border">
|
|
341
|
-
${grouped[g].map(
|
|
342
|
-
(v) =>
|
|
343
|
-
html`<${EnvRow}
|
|
344
|
-
key=${`${secretMaskEpoch}:${v.key}`}
|
|
345
|
-
envVar=${v}
|
|
346
|
-
onChange=${handleChange}
|
|
347
|
-
onDelete=${handleDelete}
|
|
348
|
-
disabled=${saving}
|
|
349
|
-
/>`,
|
|
350
|
-
)}
|
|
351
|
-
</div>
|
|
352
|
-
</div>
|
|
353
|
-
`,
|
|
354
|
-
)}
|
|
471
|
+
.map((g) => renderGroupCard(g))}
|
|
355
472
|
|
|
356
473
|
<div class="bg-surface border border-border rounded-xl overflow-hidden">
|
|
357
474
|
<div class="flex items-center justify-between px-4 pt-3 pb-2">
|
|
@@ -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}
|