@chrysb/alphaclaw 0.4.2 → 0.4.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.
|
@@ -31,6 +31,11 @@ const kSizeClassBySize = {
|
|
|
31
31
|
md: "h-9 text-sm font-medium leading-none px-4 rounded-xl",
|
|
32
32
|
lg: "h-10 text-sm font-medium leading-none px-5 rounded-lg",
|
|
33
33
|
};
|
|
34
|
+
const kIconOnlySizeClassBySize = {
|
|
35
|
+
sm: "h-7 w-7 p-0 rounded-lg",
|
|
36
|
+
md: "h-9 w-9 p-0 rounded-xl",
|
|
37
|
+
lg: "h-10 w-10 p-0 rounded-lg",
|
|
38
|
+
};
|
|
34
39
|
|
|
35
40
|
export const ActionButton = ({
|
|
36
41
|
onClick,
|
|
@@ -45,11 +50,16 @@ export const ActionButton = ({
|
|
|
45
50
|
className = "",
|
|
46
51
|
idleIcon = null,
|
|
47
52
|
idleIconClassName = "h-3 w-3",
|
|
53
|
+
iconOnly = false,
|
|
54
|
+
title = "",
|
|
55
|
+
ariaLabel = "",
|
|
48
56
|
}) => {
|
|
49
57
|
const isDisabled = disabled || loading;
|
|
50
58
|
const isInteractive = !isDisabled;
|
|
51
59
|
const toneClass = getToneClass(tone, isInteractive);
|
|
52
|
-
const sizeClass =
|
|
60
|
+
const sizeClass = iconOnly
|
|
61
|
+
? kIconOnlySizeClassBySize[size] || kIconOnlySizeClassBySize.sm
|
|
62
|
+
: kSizeClassBySize[size] || kSizeClassBySize.sm;
|
|
53
63
|
const loadingClass = loading
|
|
54
64
|
? `cursor-not-allowed ${
|
|
55
65
|
tone === "warning"
|
|
@@ -60,7 +70,9 @@ export const ActionButton = ({
|
|
|
60
70
|
const spinnerSizeClass = size === "md" || size === "lg" ? "h-4 w-4" : "h-3 w-3";
|
|
61
71
|
const isInlineLoading = loadingMode === "inline";
|
|
62
72
|
const IdleIcon = idleIcon;
|
|
63
|
-
const idleContent = IdleIcon
|
|
73
|
+
const idleContent = iconOnly && IdleIcon
|
|
74
|
+
? html`<${IdleIcon} className=${idleIconClassName} />`
|
|
75
|
+
: IdleIcon
|
|
64
76
|
? html`
|
|
65
77
|
<span class="inline-flex items-center gap-1.5 leading-none">
|
|
66
78
|
<${IdleIcon} className=${idleIconClassName} />
|
|
@@ -75,6 +87,8 @@ export const ActionButton = ({
|
|
|
75
87
|
type=${type}
|
|
76
88
|
onclick=${onClick}
|
|
77
89
|
disabled=${isDisabled}
|
|
90
|
+
title=${title}
|
|
91
|
+
aria-label=${ariaLabel || null}
|
|
78
92
|
class="inline-flex items-center justify-center transition-colors whitespace-nowrap ${sizeClass} ${toneClass} ${loadingClass} ${className}"
|
|
79
93
|
>
|
|
80
94
|
${isInlineLoading
|
|
@@ -17,6 +17,17 @@ const kGroupLabels = {
|
|
|
17
17
|
|
|
18
18
|
const kGroupOrder = ["github", "channels", "tools", "custom"];
|
|
19
19
|
const normalizeEnvVarKey = (raw) => raw.trim().toUpperCase().replace(/[^A-Z0-9_]/g, "_");
|
|
20
|
+
const stripSurroundingQuotes = (raw) => {
|
|
21
|
+
const value = String(raw || "").trim();
|
|
22
|
+
if (value.length < 2) return value;
|
|
23
|
+
const startsWithDouble = value.startsWith('"');
|
|
24
|
+
const endsWithDouble = value.endsWith('"');
|
|
25
|
+
if (startsWithDouble && endsWithDouble) return value.slice(1, -1);
|
|
26
|
+
const startsWithSingle = value.startsWith("'");
|
|
27
|
+
const endsWithSingle = value.endsWith("'");
|
|
28
|
+
if (startsWithSingle && endsWithSingle) return value.slice(1, -1);
|
|
29
|
+
return value;
|
|
30
|
+
};
|
|
20
31
|
const getVarsSignature = (items) =>
|
|
21
32
|
JSON.stringify(
|
|
22
33
|
(items || [])
|
|
@@ -27,6 +38,20 @@ const getVarsSignature = (items) =>
|
|
|
27
38
|
.sort((a, b) => a.key.localeCompare(b.key)),
|
|
28
39
|
);
|
|
29
40
|
|
|
41
|
+
const sortCustomVarsAlphabetically = (items) => {
|
|
42
|
+
const list = Array.isArray(items) ? [...items] : [];
|
|
43
|
+
const customSorted = list
|
|
44
|
+
.filter((item) => (item?.group || "custom") === "custom")
|
|
45
|
+
.sort((a, b) => String(a?.key || "").localeCompare(String(b?.key || "")));
|
|
46
|
+
let customIdx = 0;
|
|
47
|
+
return list.map((item) => {
|
|
48
|
+
if ((item?.group || "custom") !== "custom") return item;
|
|
49
|
+
const next = customSorted[customIdx];
|
|
50
|
+
customIdx += 1;
|
|
51
|
+
return next;
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
|
|
30
55
|
const kHintByKey = {
|
|
31
56
|
ANTHROPIC_API_KEY: html`from <a href="https://console.anthropic.com" target="_blank" class="hover:underline" style="color: var(--accent-link)">console.anthropic.com</a>`,
|
|
32
57
|
ANTHROPIC_TOKEN: html`from <code class="text-xs bg-black/30 px-1 rounded">claude setup-token</code>`,
|
|
@@ -85,6 +110,8 @@ const EnvRow = ({ envVar, onChange, onDelete, disabled }) => {
|
|
|
85
110
|
export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
86
111
|
const [vars, setVars] = useState([]);
|
|
87
112
|
const [reservedKeys, setReservedKeys] = useState(() => new Set());
|
|
113
|
+
const [pendingCustomKeys, setPendingCustomKeys] = useState([]);
|
|
114
|
+
const [secretMaskEpoch, setSecretMaskEpoch] = useState(0);
|
|
88
115
|
const [dirty, setDirty] = useState(false);
|
|
89
116
|
const [saving, setSaving] = useState(false);
|
|
90
117
|
const [newKey, setNewKey] = useState("");
|
|
@@ -93,9 +120,10 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
93
120
|
const load = useCallback(async () => {
|
|
94
121
|
try {
|
|
95
122
|
const data = await fetchEnvVars();
|
|
96
|
-
const nextVars = data.vars || [];
|
|
123
|
+
const nextVars = sortCustomVarsAlphabetically(data.vars || []);
|
|
97
124
|
baselineSignatureRef.current = getVarsSignature(nextVars);
|
|
98
125
|
setVars(nextVars);
|
|
126
|
+
setPendingCustomKeys([]);
|
|
99
127
|
setReservedKeys(new Set(data.reservedKeys || []));
|
|
100
128
|
onRestartRequired(!!data.restartRequired);
|
|
101
129
|
} catch (err) {
|
|
@@ -117,6 +145,7 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
117
145
|
|
|
118
146
|
const handleDelete = (key) => {
|
|
119
147
|
setVars((prev) => prev.filter((v) => v.key !== key));
|
|
148
|
+
setPendingCustomKeys((prev) => prev.filter((pendingKey) => pendingKey !== key));
|
|
120
149
|
};
|
|
121
150
|
|
|
122
151
|
const handleSave = async () => {
|
|
@@ -135,7 +164,11 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
135
164
|
: "Environment variables saved",
|
|
136
165
|
"success",
|
|
137
166
|
);
|
|
138
|
-
|
|
167
|
+
const sortedVars = sortCustomVarsAlphabetically(vars);
|
|
168
|
+
setVars(sortedVars);
|
|
169
|
+
setPendingCustomKeys([]);
|
|
170
|
+
setSecretMaskEpoch((prev) => prev + 1);
|
|
171
|
+
baselineSignatureRef.current = getVarsSignature(sortedVars);
|
|
139
172
|
setDirty(false);
|
|
140
173
|
} catch (err) {
|
|
141
174
|
showToast("Failed to save: " + err.message, "error");
|
|
@@ -158,7 +191,7 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
158
191
|
if (eqIdx > 0)
|
|
159
192
|
pairs.push({
|
|
160
193
|
key: line.slice(0, eqIdx).trim(),
|
|
161
|
-
value: line.slice(eqIdx + 1)
|
|
194
|
+
value: stripSurroundingQuotes(line.slice(eqIdx + 1)),
|
|
162
195
|
});
|
|
163
196
|
}
|
|
164
197
|
return pairs;
|
|
@@ -167,6 +200,7 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
167
200
|
const addVars = (pairs) => {
|
|
168
201
|
let added = 0;
|
|
169
202
|
const blocked = [];
|
|
203
|
+
const addedCustomKeys = [];
|
|
170
204
|
setVars((prev) => {
|
|
171
205
|
const next = [...prev];
|
|
172
206
|
for (const { key: rawKey, value } of pairs) {
|
|
@@ -189,11 +223,15 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
189
223
|
source: "env_file",
|
|
190
224
|
editable: true,
|
|
191
225
|
});
|
|
226
|
+
addedCustomKeys.push(key);
|
|
192
227
|
}
|
|
193
228
|
added++;
|
|
194
229
|
}
|
|
195
230
|
return next;
|
|
196
231
|
});
|
|
232
|
+
if (addedCustomKeys.length) {
|
|
233
|
+
setPendingCustomKeys((prev) => [...prev, ...addedCustomKeys]);
|
|
234
|
+
}
|
|
197
235
|
return { added, blocked };
|
|
198
236
|
};
|
|
199
237
|
|
|
@@ -264,6 +302,14 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
264
302
|
if (!grouped[g]) grouped[g] = [];
|
|
265
303
|
grouped[g].push(v);
|
|
266
304
|
}
|
|
305
|
+
if (grouped.custom?.length) {
|
|
306
|
+
const pending = new Set(pendingCustomKeys);
|
|
307
|
+
const nonPending = grouped.custom
|
|
308
|
+
.filter((item) => !pending.has(item.key))
|
|
309
|
+
.sort((a, b) => String(a?.key || "").localeCompare(String(b?.key || "")));
|
|
310
|
+
const pendingAtBottom = grouped.custom.filter((item) => pending.has(item.key));
|
|
311
|
+
grouped.custom = [...nonPending, ...pendingAtBottom];
|
|
312
|
+
}
|
|
267
313
|
|
|
268
314
|
return html`
|
|
269
315
|
<div class="space-y-4">
|
|
@@ -295,6 +341,7 @@ export const Envars = ({ onRestartRequired = () => {} }) => {
|
|
|
295
341
|
${grouped[g].map(
|
|
296
342
|
(v) =>
|
|
297
343
|
html`<${EnvRow}
|
|
344
|
+
key=${`${secretMaskEpoch}:${v.key}`}
|
|
298
345
|
envVar=${v}
|
|
299
346
|
onChange=${handleChange}
|
|
300
347
|
onDelete=${handleDelete}
|
|
@@ -39,18 +39,25 @@ export const FileViewerToolbar = ({
|
|
|
39
39
|
${pathSegments.map(
|
|
40
40
|
(segment, index) => html`
|
|
41
41
|
<span class="file-viewer-breadcrumb-item">
|
|
42
|
-
<span
|
|
42
|
+
<span
|
|
43
|
+
class=${index === pathSegments.length - 1 ? "is-current" : ""}
|
|
44
|
+
>
|
|
43
45
|
${segment}
|
|
44
46
|
</span>
|
|
45
|
-
${index < pathSegments.length - 1 &&
|
|
47
|
+
${index < pathSegments.length - 1 &&
|
|
48
|
+
html`<span class="file-viewer-sep">></span>`}
|
|
46
49
|
</span>
|
|
47
50
|
`,
|
|
48
51
|
)}
|
|
49
52
|
</span>
|
|
50
|
-
${isDirty
|
|
53
|
+
${isDirty
|
|
54
|
+
? html`<span class="file-viewer-dirty-dot" aria-hidden="true"></span>`
|
|
55
|
+
: null}
|
|
51
56
|
</div>
|
|
52
57
|
<div class="file-viewer-tabbar-spacer"></div>
|
|
53
|
-
${isPreviewOnly
|
|
58
|
+
${isPreviewOnly
|
|
59
|
+
? html`<div class="file-viewer-preview-pill">Preview</div>`
|
|
60
|
+
: null}
|
|
54
61
|
${!isDiffView &&
|
|
55
62
|
isMarkdownFile &&
|
|
56
63
|
html`
|
|
@@ -69,27 +76,32 @@ export const FileViewerToolbar = ({
|
|
|
69
76
|
? html`
|
|
70
77
|
${!isProtectedFile
|
|
71
78
|
? html`
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
onclick=${onRequestDelete}
|
|
79
|
+
<${ActionButton}
|
|
80
|
+
onClick=${onRequestDelete}
|
|
75
81
|
disabled=${!canDeleteFile || deleting}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
82
|
+
tone="secondary"
|
|
83
|
+
size="sm"
|
|
84
|
+
iconOnly=${true}
|
|
85
|
+
idleLabel=""
|
|
86
|
+
idleIcon=${DeleteBinLineIcon}
|
|
87
|
+
idleIconClassName="file-viewer-icon-action-icon"
|
|
88
|
+
className="file-viewer-save-action"
|
|
79
89
|
title=${isDeleteBlocked
|
|
80
90
|
? "Locked files cannot be deleted"
|
|
81
91
|
: "Delete file"}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
<${DeleteBinLineIcon} className="file-viewer-icon-action-icon" />
|
|
85
|
-
</button>
|
|
92
|
+
ariaLabel="Delete file"
|
|
93
|
+
/>
|
|
86
94
|
`
|
|
87
95
|
: null}
|
|
88
96
|
${isDirty
|
|
89
97
|
? html`
|
|
90
98
|
<${ActionButton}
|
|
91
99
|
onClick=${handleDiscard}
|
|
92
|
-
disabled=${loading ||
|
|
100
|
+
disabled=${loading ||
|
|
101
|
+
!canEditFile ||
|
|
102
|
+
isEditBlocked ||
|
|
103
|
+
deleting ||
|
|
104
|
+
saving}
|
|
93
105
|
tone="secondary"
|
|
94
106
|
size="sm"
|
|
95
107
|
idleLabel="Discard changes"
|