@chrysb/alphaclaw 0.4.1-beta.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 = kSizeClassBySize[size] || kSizeClassBySize.sm;
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
- baselineSignatureRef.current = getVarsSignature(vars);
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).trim(),
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 class=${index === pathSegments.length - 1 ? "is-current" : ""}>
42
+ <span
43
+ class=${index === pathSegments.length - 1 ? "is-current" : ""}
44
+ >
43
45
  ${segment}
44
46
  </span>
45
- ${index < pathSegments.length - 1 && html`<span class="file-viewer-sep">></span>`}
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 ? html`<span class="file-viewer-dirty-dot" aria-hidden="true"></span>` : null}
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 ? html`<div class="file-viewer-preview-pill">Preview</div>` : null}
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
- <button
73
- type="button"
74
- onclick=${onRequestDelete}
79
+ <${ActionButton}
80
+ onClick=${onRequestDelete}
75
81
  disabled=${!canDeleteFile || deleting}
76
- class=${`file-viewer-icon-action file-viewer-delete-action ${
77
- !canDeleteFile || deleting ? "is-disabled" : ""
78
- }`.trim()}
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
- aria-label="Delete file"
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 || !canEditFile || isEditBlocked || deleting || saving}
100
+ disabled=${loading ||
101
+ !canEditFile ||
102
+ isEditBlocked ||
103
+ deleting ||
104
+ saving}
93
105
  tone="secondary"
94
106
  size="sm"
95
107
  idleLabel="Discard changes"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.4.1-beta.2",
3
+ "version": "0.4.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },