@chrysb/alphaclaw 0.6.2-beta.5 → 0.7.0-beta.0
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/agents.css +37 -13
- package/lib/public/css/cron.css +124 -41
- package/lib/public/css/shell.css +61 -2
- package/lib/public/css/theme.css +2 -1
- package/lib/public/js/app.js +41 -33
- package/lib/public/js/components/agents-tab/agent-detail-panel.js +61 -49
- package/lib/public/js/components/agents-tab/agent-overview/index.js +9 -0
- package/lib/public/js/components/agents-tab/agent-overview/tools-card.js +54 -0
- package/lib/public/js/components/cron-tab/cron-calendar.js +297 -203
- package/lib/public/js/components/cron-tab/cron-helpers.js +48 -0
- package/lib/public/js/components/cron-tab/cron-insights-panel.js +294 -0
- package/lib/public/js/components/cron-tab/cron-job-detail.js +38 -363
- package/lib/public/js/components/cron-tab/cron-job-settings-card.js +233 -0
- package/lib/public/js/components/cron-tab/cron-overview.js +40 -19
- package/lib/public/js/components/cron-tab/cron-prompt-editor.js +173 -0
- package/lib/public/js/components/cron-tab/cron-run-history-panel.js +74 -62
- package/lib/public/js/components/cron-tab/cron-runs-trend-card.js +24 -24
- package/lib/public/js/components/cron-tab/index.js +170 -78
- package/lib/public/js/components/envars.js +187 -46
- package/lib/public/js/components/file-viewer/editor-surface.js +5 -1
- package/lib/public/js/components/file-viewer/use-editor-line-number-sync.js +36 -0
- package/lib/public/js/components/file-viewer/use-file-viewer.js +7 -23
- package/lib/public/js/components/file-viewer/utils.js +1 -5
- package/lib/public/js/components/models-tab/index.js +137 -133
- package/lib/public/js/components/models-tab/provider-auth-card.js +8 -1
- package/lib/public/js/components/models-tab/use-models.js +35 -8
- package/lib/public/js/components/onboarding/welcome-pairing-step.js +88 -59
- package/lib/public/js/components/pane-shell.js +27 -0
- package/lib/public/js/components/routes/envars-route.js +1 -3
- package/lib/public/js/components/routes/models-route.js +1 -3
- package/lib/public/js/lib/app-navigation.js +1 -1
- package/lib/server/cost-utils.js +2 -2
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "https://esm.sh/preact/hooks";
|
|
2
3
|
import htm from "https://esm.sh/htm";
|
|
3
4
|
import { ActionButton } from "../action-button.js";
|
|
4
5
|
import { PageHeader } from "../page-header.js";
|
|
@@ -11,94 +12,185 @@ import { useCronTab } from "./use-cron-tab.js";
|
|
|
11
12
|
const html = htm.bind(h);
|
|
12
13
|
|
|
13
14
|
export const CronTab = ({ jobId = "", onSetLocation = () => {} }) => {
|
|
14
|
-
const {
|
|
15
|
+
const { state, actions } = useCronTab({ jobId, onSetLocation });
|
|
16
|
+
const [showJobSelector, setShowJobSelector] = useState(false);
|
|
17
|
+
const selectorShellRef = useRef(null);
|
|
15
18
|
const isAllJobsSelected = state.selectedRouteKey === kAllCronJobsRouteKey;
|
|
16
19
|
const noJobs = state.jobs.length === 0;
|
|
20
|
+
const selectedJob = state.selectedJob;
|
|
21
|
+
const selectedJobLabel = useMemo(() => {
|
|
22
|
+
if (isAllJobsSelected) return "All jobs";
|
|
23
|
+
const selectedJob = state.jobs.find(
|
|
24
|
+
(job) => String(job?.id || "") === String(state.selectedRouteKey || ""),
|
|
25
|
+
);
|
|
26
|
+
return String(selectedJob?.name || selectedJob?.id || "All jobs");
|
|
27
|
+
}, [isAllJobsSelected, state.jobs, state.selectedRouteKey]);
|
|
28
|
+
const hasUnsavedDetailChanges = useMemo(() => {
|
|
29
|
+
if (isAllJobsSelected || !selectedJob) return false;
|
|
30
|
+
const sessionTarget = String(
|
|
31
|
+
state.routingDraft?.sessionTarget || selectedJob?.sessionTarget || "main",
|
|
32
|
+
);
|
|
33
|
+
const wakeMode = String(
|
|
34
|
+
state.routingDraft?.wakeMode || selectedJob?.wakeMode || "now",
|
|
35
|
+
);
|
|
36
|
+
const deliveryMode = String(
|
|
37
|
+
state.routingDraft?.deliveryMode || selectedJob?.delivery?.mode || "none",
|
|
38
|
+
);
|
|
39
|
+
const currentSessionTarget = String(selectedJob?.sessionTarget || "main");
|
|
40
|
+
const currentWakeMode = String(selectedJob?.wakeMode || "now");
|
|
41
|
+
const currentDeliveryMode = String(selectedJob?.delivery?.mode || "none");
|
|
42
|
+
const isRoutingDirty =
|
|
43
|
+
sessionTarget !== currentSessionTarget ||
|
|
44
|
+
wakeMode !== currentWakeMode ||
|
|
45
|
+
deliveryMode !== currentDeliveryMode;
|
|
46
|
+
const isPromptDirty = state.promptValue !== state.savedPromptValue;
|
|
47
|
+
return isRoutingDirty || isPromptDirty;
|
|
48
|
+
}, [
|
|
49
|
+
isAllJobsSelected,
|
|
50
|
+
selectedJob,
|
|
51
|
+
state.promptValue,
|
|
52
|
+
state.routingDraft?.deliveryMode,
|
|
53
|
+
state.routingDraft?.sessionTarget,
|
|
54
|
+
state.routingDraft?.wakeMode,
|
|
55
|
+
state.savedPromptValue,
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!showJobSelector) return () => {};
|
|
60
|
+
const handlePointerDown = (event) => {
|
|
61
|
+
if (selectorShellRef.current?.contains(event.target)) return;
|
|
62
|
+
setShowJobSelector(false);
|
|
63
|
+
};
|
|
64
|
+
const handleKeyDown = (event) => {
|
|
65
|
+
if (event.key !== "Escape") return;
|
|
66
|
+
setShowJobSelector(false);
|
|
67
|
+
};
|
|
68
|
+
window.addEventListener("pointerdown", handlePointerDown);
|
|
69
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
70
|
+
return () => {
|
|
71
|
+
window.removeEventListener("pointerdown", handlePointerDown);
|
|
72
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
73
|
+
};
|
|
74
|
+
}, [showJobSelector]);
|
|
75
|
+
|
|
76
|
+
const handleSelectAllJobs = () => {
|
|
77
|
+
actions.selectAllJobs();
|
|
78
|
+
setShowJobSelector(false);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const handleSelectJob = (nextJobId) => {
|
|
82
|
+
actions.selectJob(nextJobId);
|
|
83
|
+
setShowJobSelector(false);
|
|
84
|
+
};
|
|
17
85
|
|
|
18
86
|
return html`
|
|
19
87
|
<div class="cron-tab-shell">
|
|
20
88
|
<div class="cron-tab-header">
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
89
|
+
<div class="cron-tab-header-content">
|
|
90
|
+
<${PageHeader}
|
|
91
|
+
leading=${html`
|
|
92
|
+
<div class="cron-tab-selector-shell" ref=${selectorShellRef}>
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
class=${`cron-tab-selector-toggle ${showJobSelector ? "is-open" : ""}`}
|
|
96
|
+
onClick=${() => setShowJobSelector((value) => !value)}
|
|
97
|
+
aria-expanded=${showJobSelector}
|
|
98
|
+
aria-haspopup="listbox"
|
|
99
|
+
>
|
|
100
|
+
<span class="cron-tab-selector-title">${selectedJobLabel}</span>
|
|
101
|
+
<span class="cron-tab-selector-caret">▾</span>
|
|
102
|
+
</button>
|
|
103
|
+
${showJobSelector
|
|
104
|
+
? html`
|
|
105
|
+
<div class="cron-tab-selector-dropdown">
|
|
106
|
+
<${CronJobList}
|
|
107
|
+
jobs=${state.jobs}
|
|
108
|
+
selectedRouteKey=${state.selectedRouteKey}
|
|
109
|
+
onSelectAllJobs=${handleSelectAllJobs}
|
|
110
|
+
onSelectJob=${handleSelectJob}
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
`
|
|
114
|
+
: null}
|
|
115
|
+
</div>
|
|
116
|
+
`}
|
|
117
|
+
actions=${html`
|
|
118
|
+
${isAllJobsSelected || noJobs
|
|
119
|
+
? html`
|
|
120
|
+
<${ActionButton}
|
|
121
|
+
onClick=${actions.refreshAll}
|
|
122
|
+
tone="secondary"
|
|
123
|
+
size="sm"
|
|
124
|
+
idleLabel="Refresh"
|
|
125
|
+
/>
|
|
126
|
+
`
|
|
127
|
+
: html`
|
|
128
|
+
<${ActionButton}
|
|
129
|
+
onClick=${actions.saveChanges}
|
|
130
|
+
loading=${state.savingChanges}
|
|
131
|
+
disabled=${!hasUnsavedDetailChanges}
|
|
132
|
+
tone="primary"
|
|
133
|
+
size="sm"
|
|
134
|
+
idleLabel="Save changes"
|
|
135
|
+
loadingLabel="Saving..."
|
|
136
|
+
/>
|
|
137
|
+
`}
|
|
138
|
+
`}
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
32
141
|
</div>
|
|
33
142
|
<div class="cron-tab-main">
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
style=${{ width: `${state.listPanelWidthPx}px` }}
|
|
38
|
-
>
|
|
39
|
-
<${CronJobList}
|
|
40
|
-
jobs=${state.jobs}
|
|
41
|
-
selectedRouteKey=${state.selectedRouteKey}
|
|
42
|
-
onSelectAllJobs=${actions.selectAllJobs}
|
|
43
|
-
onSelectJob=${actions.selectJob}
|
|
44
|
-
/>
|
|
45
|
-
</aside>
|
|
46
|
-
<div
|
|
47
|
-
class=${`cron-list-resizer ${state.isResizingListPanel ? "is-resizing" : ""}`}
|
|
48
|
-
onpointerdown=${actions.onListResizerPointerDown}
|
|
49
|
-
role="separator"
|
|
50
|
-
aria-orientation="vertical"
|
|
51
|
-
aria-label="Resize cron jobs list"
|
|
52
|
-
></div>
|
|
53
|
-
<main class="cron-detail-panel">
|
|
54
|
-
${noJobs
|
|
55
|
-
? html`
|
|
56
|
-
<div class="h-full flex items-center justify-center text-sm text-gray-500">
|
|
57
|
-
No cron jobs configured. Cron jobs are managed via the OpenClaw CLI.
|
|
58
|
-
</div>
|
|
59
|
-
`
|
|
60
|
-
: isAllJobsSelected
|
|
143
|
+
<div class="cron-tab-main-content">
|
|
144
|
+
<main class="cron-detail-panel">
|
|
145
|
+
${noJobs
|
|
61
146
|
? html`
|
|
62
|
-
|
|
63
|
-
jobs
|
|
64
|
-
|
|
65
|
-
bulkUsageByJobId=${state.bulkUsageByJobId}
|
|
66
|
-
bulkRunsByJobId=${state.bulkRunsByJobId}
|
|
67
|
-
onSelectJob=${actions.selectJob}
|
|
68
|
-
/>
|
|
147
|
+
<div class="h-full flex items-center justify-center text-sm text-gray-500">
|
|
148
|
+
No cron jobs configured. Cron jobs are managed via the OpenClaw CLI.
|
|
149
|
+
</div>
|
|
69
150
|
`
|
|
70
|
-
:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
151
|
+
: isAllJobsSelected
|
|
152
|
+
? html`
|
|
153
|
+
<${CronOverview}
|
|
154
|
+
jobs=${state.jobs}
|
|
155
|
+
status=${state.status}
|
|
156
|
+
bulkUsageByJobId=${state.bulkUsageByJobId}
|
|
157
|
+
bulkRunsByJobId=${state.bulkRunsByJobId}
|
|
158
|
+
onSelectJob=${handleSelectJob}
|
|
159
|
+
/>
|
|
160
|
+
`
|
|
161
|
+
: html`
|
|
162
|
+
<${CronJobDetail}
|
|
163
|
+
job=${state.selectedJob}
|
|
164
|
+
runEntries=${state.runEntries}
|
|
165
|
+
runTotal=${state.runTotal}
|
|
166
|
+
runHasMore=${state.runHasMore}
|
|
167
|
+
loadingMoreRuns=${state.loadingMoreRuns}
|
|
168
|
+
runStatusFilter=${state.runStatusFilter}
|
|
169
|
+
onSetRunStatusFilter=${actions.setRunStatusFilter}
|
|
170
|
+
onLoadMoreRuns=${actions.loadMoreRuns}
|
|
171
|
+
onRunNow=${actions.runSelectedJobNow}
|
|
172
|
+
runningJob=${state.runningJob}
|
|
173
|
+
onToggleEnabled=${actions.setSelectedJobEnabled}
|
|
174
|
+
togglingJobEnabled=${state.togglingJobEnabled}
|
|
175
|
+
usage=${state.usage}
|
|
176
|
+
usageDays=${state.usageDays}
|
|
177
|
+
onSetUsageDays=${actions.setUsageDays}
|
|
178
|
+
promptValue=${state.promptValue}
|
|
179
|
+
savedPromptValue=${state.savedPromptValue}
|
|
180
|
+
onChangePrompt=${actions.setPromptValue}
|
|
181
|
+
onSaveChanges=${actions.saveChanges}
|
|
182
|
+
savingChanges=${state.savingChanges}
|
|
183
|
+
routingDraft=${state.routingDraft}
|
|
184
|
+
onChangeRoutingDraft=${actions.setRoutingDraft}
|
|
185
|
+
deliverySessions=${state.deliverySessions}
|
|
186
|
+
loadingDeliverySessions=${state.loadingDeliverySessions}
|
|
187
|
+
deliverySessionsError=${state.deliverySessionsError}
|
|
188
|
+
destinationSessionKey=${state.destinationSessionKey}
|
|
189
|
+
onChangeDestinationSessionKey=${actions.setDestinationSessionKey}
|
|
190
|
+
/>
|
|
191
|
+
`}
|
|
192
|
+
</main>
|
|
193
|
+
</div>
|
|
102
194
|
</div>
|
|
103
195
|
</div>
|
|
104
196
|
`;
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
useState,
|
|
4
|
+
useEffect,
|
|
5
|
+
useCallback,
|
|
6
|
+
useRef,
|
|
7
|
+
} from "https://esm.sh/preact/hooks";
|
|
3
8
|
import htm from "https://esm.sh/htm";
|
|
4
9
|
import { fetchEnvVars, saveEnvVars } from "../lib/api.js";
|
|
5
10
|
import { showToast } from "./toast.js";
|
|
6
11
|
import { SecretInput } from "./secret-input.js";
|
|
7
12
|
import { PageHeader } from "./page-header.js";
|
|
8
13
|
import { ActionButton } from "./action-button.js";
|
|
14
|
+
import { PopActions } from "./pop-actions.js";
|
|
15
|
+
import { PaneShell } from "./pane-shell.js";
|
|
9
16
|
import {
|
|
10
17
|
Brain2LineIcon,
|
|
11
18
|
ChatVoiceLineIcon,
|
|
@@ -44,7 +51,11 @@ const kFeatureIconByName = {
|
|
|
44
51
|
label: "Speech to text",
|
|
45
52
|
},
|
|
46
53
|
};
|
|
47
|
-
const normalizeEnvVarKey = (raw) =>
|
|
54
|
+
const normalizeEnvVarKey = (raw) =>
|
|
55
|
+
raw
|
|
56
|
+
.trim()
|
|
57
|
+
.toUpperCase()
|
|
58
|
+
.replace(/[^A-Z0-9_]/g, "_");
|
|
48
59
|
const kManagedChannelTokenPattern =
|
|
49
60
|
/^(?:TELEGRAM_BOT_TOKEN|DISCORD_BOT_TOKEN|SLACK_BOT_TOKEN|SLACK_APP_TOKEN)(?:_[A-Z0-9_]+)?$/;
|
|
50
61
|
const stripSurroundingQuotes = (raw) => {
|
|
@@ -59,7 +70,11 @@ const stripSurroundingQuotes = (raw) => {
|
|
|
59
70
|
return value;
|
|
60
71
|
};
|
|
61
72
|
const isManagedChannelTokenKey = (key = "") =>
|
|
62
|
-
kManagedChannelTokenPattern.test(
|
|
73
|
+
kManagedChannelTokenPattern.test(
|
|
74
|
+
String(key || "")
|
|
75
|
+
.trim()
|
|
76
|
+
.toUpperCase(),
|
|
77
|
+
);
|
|
63
78
|
const getVarsSignature = (items) =>
|
|
64
79
|
JSON.stringify(
|
|
65
80
|
(items || [])
|
|
@@ -85,19 +100,119 @@ const sortCustomVarsAlphabetically = (items) => {
|
|
|
85
100
|
};
|
|
86
101
|
|
|
87
102
|
const kHintByKey = {
|
|
88
|
-
ANTHROPIC_API_KEY: html`from
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
ANTHROPIC_API_KEY: html`from${" "}
|
|
104
|
+
<a
|
|
105
|
+
href="https://console.anthropic.com"
|
|
106
|
+
target="_blank"
|
|
107
|
+
class="hover:underline"
|
|
108
|
+
style="color: var(--accent-link)"
|
|
109
|
+
>console.anthropic.com</a
|
|
110
|
+
>`,
|
|
111
|
+
ANTHROPIC_TOKEN: html`from
|
|
112
|
+
<code class="text-xs bg-black/30 px-1 rounded">claude setup-token</code>`,
|
|
113
|
+
OPENAI_API_KEY: html`from${" "}
|
|
114
|
+
<a
|
|
115
|
+
href="https://platform.openai.com"
|
|
116
|
+
target="_blank"
|
|
117
|
+
class="hover:underline"
|
|
118
|
+
style="color: var(--accent-link)"
|
|
119
|
+
>platform.openai.com</a
|
|
120
|
+
>`,
|
|
121
|
+
GEMINI_API_KEY: html`from${" "}
|
|
122
|
+
<a
|
|
123
|
+
href="https://aistudio.google.com"
|
|
124
|
+
target="_blank"
|
|
125
|
+
class="hover:underline"
|
|
126
|
+
style="color: var(--accent-link)"
|
|
127
|
+
>aistudio.google.com</a
|
|
128
|
+
>`,
|
|
129
|
+
ELEVENLABS_API_KEY: html`from${" "}
|
|
130
|
+
<a
|
|
131
|
+
href="https://elevenlabs.io"
|
|
132
|
+
target="_blank"
|
|
133
|
+
class="hover:underline"
|
|
134
|
+
style="color: var(--accent-link)"
|
|
135
|
+
>elevenlabs.io</a
|
|
136
|
+
>${" "} · ${" "}
|
|
137
|
+
<code class="text-xs bg-black/30 px-1 rounded">XI_API_KEY</code> also
|
|
138
|
+
supported`,
|
|
139
|
+
GITHUB_WORKSPACE_REPO: html`use
|
|
140
|
+
<code class="text-xs bg-black/30 px-1 rounded">owner/repo</code> or
|
|
141
|
+
<code class="text-xs bg-black/30 px-1 rounded"
|
|
142
|
+
>https://github.com/owner/repo</code
|
|
143
|
+
>`,
|
|
144
|
+
TELEGRAM_BOT_TOKEN: html`from${" "}
|
|
145
|
+
<a
|
|
146
|
+
href="https://t.me/BotFather"
|
|
147
|
+
target="_blank"
|
|
148
|
+
class="hover:underline"
|
|
149
|
+
style="color: var(--accent-link)"
|
|
150
|
+
>@BotFather</a
|
|
151
|
+
>
|
|
152
|
+
·
|
|
153
|
+
<a
|
|
154
|
+
href="https://docs.openclaw.ai/channels/telegram"
|
|
155
|
+
target="_blank"
|
|
156
|
+
class="hover:underline"
|
|
157
|
+
style="color: var(--accent-link)"
|
|
158
|
+
>full guide</a
|
|
159
|
+
>`,
|
|
160
|
+
DISCORD_BOT_TOKEN: html`from${" "}
|
|
161
|
+
<a
|
|
162
|
+
href="https://discord.com/developers/applications"
|
|
163
|
+
target="_blank"
|
|
164
|
+
class="hover:underline"
|
|
165
|
+
style="color: var(--accent-link)"
|
|
166
|
+
>developer portal</a
|
|
167
|
+
>
|
|
168
|
+
·
|
|
169
|
+
<a
|
|
170
|
+
href="https://docs.openclaw.ai/channels/discord"
|
|
171
|
+
target="_blank"
|
|
172
|
+
class="hover:underline"
|
|
173
|
+
style="color: var(--accent-link)"
|
|
174
|
+
>full guide</a
|
|
175
|
+
>`,
|
|
176
|
+
MISTRAL_API_KEY: html`from${" "}
|
|
177
|
+
<a
|
|
178
|
+
href="https://console.mistral.ai"
|
|
179
|
+
target="_blank"
|
|
180
|
+
class="hover:underline"
|
|
181
|
+
style="color: var(--accent-link)"
|
|
182
|
+
>console.mistral.ai</a
|
|
183
|
+
>`,
|
|
184
|
+
VOYAGE_API_KEY: html`from${" "}
|
|
185
|
+
<a
|
|
186
|
+
href="https://dash.voyageai.com"
|
|
187
|
+
target="_blank"
|
|
188
|
+
class="hover:underline"
|
|
189
|
+
style="color: var(--accent-link)"
|
|
190
|
+
>dash.voyageai.com</a
|
|
191
|
+
>`,
|
|
192
|
+
GROQ_API_KEY: html`from${" "}
|
|
193
|
+
<a
|
|
194
|
+
href="https://console.groq.com"
|
|
195
|
+
target="_blank"
|
|
196
|
+
class="hover:underline"
|
|
197
|
+
style="color: var(--accent-link)"
|
|
198
|
+
>console.groq.com</a
|
|
199
|
+
>`,
|
|
200
|
+
DEEPGRAM_API_KEY: html`from${" "}
|
|
201
|
+
<a
|
|
202
|
+
href="https://console.deepgram.com"
|
|
203
|
+
target="_blank"
|
|
204
|
+
class="hover:underline"
|
|
205
|
+
style="color: var(--accent-link)"
|
|
206
|
+
>console.deepgram.com</a
|
|
207
|
+
>`,
|
|
208
|
+
BRAVE_API_KEY: html`from${" "}
|
|
209
|
+
<a
|
|
210
|
+
href="https://brave.com/search/api/"
|
|
211
|
+
target="_blank"
|
|
212
|
+
class="hover:underline"
|
|
213
|
+
style="color: var(--accent-link)"
|
|
214
|
+
>brave.com/search/api</a
|
|
215
|
+
>${" "} — free tier available`,
|
|
101
216
|
};
|
|
102
217
|
|
|
103
218
|
const getHintContent = (envVar) => kHintByKey[envVar.key] || envVar.hint || "";
|
|
@@ -157,7 +272,8 @@ const EnvRow = ({ envVar, onChange, onDelete, disabled }) => {
|
|
|
157
272
|
? html`
|
|
158
273
|
<div class="flex items-center gap-2 mt-1 pl-3.5">
|
|
159
274
|
${featureIcons.map(
|
|
160
|
-
(feature) =>
|
|
275
|
+
(feature) =>
|
|
276
|
+
html`<${FeatureIcon} key=${feature} feature=${feature} />`,
|
|
161
277
|
)}
|
|
162
278
|
</div>
|
|
163
279
|
`
|
|
@@ -183,9 +299,7 @@ const EnvRow = ({ envVar, onChange, onDelete, disabled }) => {
|
|
|
183
299
|
</button>`
|
|
184
300
|
: null}
|
|
185
301
|
</div>
|
|
186
|
-
${hint
|
|
187
|
-
? html`<p class="text-xs text-gray-600 mt-1">${hint}</p>`
|
|
188
|
-
: null}
|
|
302
|
+
${hint ? html`<p class="text-xs text-gray-600 mt-1">${hint}</p>` : null}
|
|
189
303
|
</div>
|
|
190
304
|
</div>
|
|
191
305
|
`;
|
|
@@ -230,7 +344,9 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
230
344
|
|
|
231
345
|
const handleDelete = (key) => {
|
|
232
346
|
setVars((prev) => prev.filter((v) => v.key !== key));
|
|
233
|
-
setPendingCustomKeys((prev) =>
|
|
347
|
+
setPendingCustomKeys((prev) =>
|
|
348
|
+
prev.filter((pendingKey) => pendingKey !== key),
|
|
349
|
+
);
|
|
234
350
|
};
|
|
235
351
|
|
|
236
352
|
const handleSave = async () => {
|
|
@@ -348,7 +464,10 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
348
464
|
);
|
|
349
465
|
}
|
|
350
466
|
if (added) {
|
|
351
|
-
showToast(
|
|
467
|
+
showToast(
|
|
468
|
+
`Added ${added} variable${added !== 1 ? "s" : ""}`,
|
|
469
|
+
"success",
|
|
470
|
+
);
|
|
352
471
|
}
|
|
353
472
|
return;
|
|
354
473
|
}
|
|
@@ -410,7 +529,9 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
410
529
|
const nonPending = grouped.custom
|
|
411
530
|
.filter((item) => !pending.has(item.key))
|
|
412
531
|
.sort((a, b) => String(a?.key || "").localeCompare(String(b?.key || "")));
|
|
413
|
-
const pendingAtBottom = grouped.custom.filter((item) =>
|
|
532
|
+
const pendingAtBottom = grouped.custom.filter((item) =>
|
|
533
|
+
pending.has(item.key),
|
|
534
|
+
);
|
|
414
535
|
grouped.custom = [...nonPending, ...pendingAtBottom];
|
|
415
536
|
}
|
|
416
537
|
const aiSplit = splitAiVars(grouped.ai || []);
|
|
@@ -454,7 +575,9 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
454
575
|
`
|
|
455
576
|
: null}
|
|
456
577
|
${expanded
|
|
457
|
-
? html`<div class="divide-y divide-border border-t border-border"
|
|
578
|
+
? html`<div class="divide-y divide-border border-t border-border">
|
|
579
|
+
${renderEnvRows(hidden)}
|
|
580
|
+
</div>`
|
|
458
581
|
: null}
|
|
459
582
|
</div>
|
|
460
583
|
`;
|
|
@@ -470,33 +593,52 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
470
593
|
};
|
|
471
594
|
|
|
472
595
|
return html`
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
596
|
+
<${PaneShell}
|
|
597
|
+
header=${html`
|
|
598
|
+
<${PageHeader}
|
|
599
|
+
title="Envars"
|
|
600
|
+
actions=${html`
|
|
601
|
+
<${PopActions} visible=${dirty}>
|
|
602
|
+
<${ActionButton}
|
|
603
|
+
onClick=${load}
|
|
604
|
+
disabled=${saving}
|
|
605
|
+
tone="secondary"
|
|
606
|
+
size="sm"
|
|
607
|
+
idleLabel="Cancel"
|
|
608
|
+
className="text-xs"
|
|
609
|
+
/>
|
|
610
|
+
<${ActionButton}
|
|
611
|
+
onClick=${handleSave}
|
|
612
|
+
disabled=${saving}
|
|
613
|
+
loading=${saving}
|
|
614
|
+
loadingMode="inline"
|
|
615
|
+
tone="primary"
|
|
616
|
+
size="sm"
|
|
617
|
+
idleLabel="Save changes"
|
|
618
|
+
loadingLabel="Saving…"
|
|
619
|
+
className="text-xs"
|
|
620
|
+
/>
|
|
621
|
+
</${PopActions}>
|
|
622
|
+
`}
|
|
623
|
+
/>
|
|
624
|
+
`}
|
|
625
|
+
>
|
|
490
626
|
${kGroupOrder
|
|
491
627
|
.filter((g) => grouped[g]?.length)
|
|
492
628
|
.map((g) => renderGroupCard(g))}
|
|
493
629
|
|
|
494
|
-
<div
|
|
630
|
+
<div
|
|
631
|
+
class="bg-surface border border-border rounded-xl overflow-hidden"
|
|
632
|
+
>
|
|
495
633
|
<div class="flex items-center justify-between px-4 pt-3 pb-2">
|
|
496
634
|
<h3 class="card-label text-xs">Add Variable</h3>
|
|
497
|
-
<span class="text-xs" style="color: var(--text-dim)"
|
|
635
|
+
<span class="text-xs" style="color: var(--text-dim)"
|
|
636
|
+
>Paste KEY=VALUE or multiple lines</span
|
|
637
|
+
>
|
|
498
638
|
</div>
|
|
499
|
-
<div
|
|
639
|
+
<div
|
|
640
|
+
class="flex items-start gap-4 px-4 py-3 border-t border-border"
|
|
641
|
+
>
|
|
500
642
|
<div class="shrink-0" style="width: 200px">
|
|
501
643
|
<input
|
|
502
644
|
type="text"
|
|
@@ -527,7 +669,6 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
527
669
|
</div>
|
|
528
670
|
</div>
|
|
529
671
|
</div>
|
|
530
|
-
|
|
531
|
-
</div>
|
|
672
|
+
</${PaneShell}>
|
|
532
673
|
`;
|
|
533
674
|
};
|
|
@@ -13,6 +13,7 @@ const EditorTextarea = ({
|
|
|
13
13
|
handleEditorSelectionChange,
|
|
14
14
|
isEditBlocked,
|
|
15
15
|
isPreviewOnly,
|
|
16
|
+
textareaWrap = "soft",
|
|
16
17
|
}) => html`
|
|
17
18
|
<textarea
|
|
18
19
|
class=${overlay ? "file-viewer-editor file-viewer-editor-overlay" : "file-viewer-editor"}
|
|
@@ -33,7 +34,7 @@ const EditorTextarea = ({
|
|
|
33
34
|
data-gramm="false"
|
|
34
35
|
data-gramm_editor="false"
|
|
35
36
|
data-enable-grammarly="false"
|
|
36
|
-
wrap
|
|
37
|
+
wrap=${textareaWrap}
|
|
37
38
|
></textarea>
|
|
38
39
|
`;
|
|
39
40
|
|
|
@@ -55,6 +56,7 @@ export const EditorSurface = ({
|
|
|
55
56
|
handleEditorSelectionChange,
|
|
56
57
|
isEditBlocked,
|
|
57
58
|
isPreviewOnly,
|
|
59
|
+
textareaWrap = "soft",
|
|
58
60
|
}) => html`
|
|
59
61
|
<div class=${editorShellClassName} aria-hidden=${editorShellAriaHidden}>
|
|
60
62
|
<div class="file-viewer-editor-line-num-col" ref=${editorLineNumbersRef}>
|
|
@@ -106,6 +108,7 @@ export const EditorSurface = ({
|
|
|
106
108
|
handleEditorSelectionChange=${handleEditorSelectionChange}
|
|
107
109
|
isEditBlocked=${isEditBlocked}
|
|
108
110
|
isPreviewOnly=${isPreviewOnly}
|
|
111
|
+
textareaWrap=${textareaWrap}
|
|
109
112
|
/>
|
|
110
113
|
</div>
|
|
111
114
|
`
|
|
@@ -120,6 +123,7 @@ export const EditorSurface = ({
|
|
|
120
123
|
handleEditorSelectionChange=${handleEditorSelectionChange}
|
|
121
124
|
isEditBlocked=${isEditBlocked}
|
|
122
125
|
isPreviewOnly=${isPreviewOnly}
|
|
126
|
+
textareaWrap=${textareaWrap}
|
|
123
127
|
/>
|
|
124
128
|
`}
|
|
125
129
|
</div>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useCallback, useEffect } from "https://esm.sh/preact/hooks";
|
|
2
|
+
|
|
3
|
+
export const useEditorLineNumberSync = ({
|
|
4
|
+
enabled = false,
|
|
5
|
+
syncKey = "",
|
|
6
|
+
editorLineNumberRowRefs,
|
|
7
|
+
editorHighlightLineRefs,
|
|
8
|
+
}) => {
|
|
9
|
+
const syncEditorLineNumberHeights = useCallback(() => {
|
|
10
|
+
if (!enabled) return;
|
|
11
|
+
const numberRows = editorLineNumberRowRefs?.current || [];
|
|
12
|
+
const highlightRows = editorHighlightLineRefs?.current || [];
|
|
13
|
+
const rowCount = Math.min(numberRows.length, highlightRows.length);
|
|
14
|
+
for (let index = 0; index < rowCount; index += 1) {
|
|
15
|
+
const numberRow = numberRows[index];
|
|
16
|
+
const highlightRow = highlightRows[index];
|
|
17
|
+
if (!numberRow || !highlightRow) continue;
|
|
18
|
+
numberRow.style.height = `${highlightRow.offsetHeight}px`;
|
|
19
|
+
}
|
|
20
|
+
}, [editorHighlightLineRefs, editorLineNumberRowRefs, enabled]);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
syncEditorLineNumberHeights();
|
|
24
|
+
}, [syncEditorLineNumberHeights, syncKey]);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!enabled) return () => {};
|
|
28
|
+
const onResize = () => syncEditorLineNumberHeights();
|
|
29
|
+
window.addEventListener("resize", onResize);
|
|
30
|
+
return () => window.removeEventListener("resize", onResize);
|
|
31
|
+
}, [enabled, syncEditorLineNumberHeights]);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
syncEditorLineNumberHeights,
|
|
35
|
+
};
|
|
36
|
+
};
|