@chrysb/alphaclaw 0.3.2 → 0.3.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.
Files changed (36) hide show
  1. package/bin/alphaclaw.js +29 -2
  2. package/lib/cli/git-sync.js +25 -0
  3. package/lib/public/css/explorer.css +983 -0
  4. package/lib/public/css/shell.css +48 -4
  5. package/lib/public/css/theme.css +6 -1
  6. package/lib/public/icons/folder-line.svg +1 -0
  7. package/lib/public/icons/hashtag.svg +3 -0
  8. package/lib/public/icons/home-5-line.svg +1 -0
  9. package/lib/public/icons/save-fill.svg +3 -0
  10. package/lib/public/js/app.js +259 -158
  11. package/lib/public/js/components/action-button.js +12 -1
  12. package/lib/public/js/components/file-tree.js +322 -0
  13. package/lib/public/js/components/file-viewer.js +691 -0
  14. package/lib/public/js/components/icons.js +182 -0
  15. package/lib/public/js/components/sidebar-git-panel.js +149 -0
  16. package/lib/public/js/components/sidebar.js +272 -0
  17. package/lib/public/js/lib/api.js +26 -0
  18. package/lib/public/js/lib/browse-draft-state.js +109 -0
  19. package/lib/public/js/lib/file-highlighting.js +6 -0
  20. package/lib/public/js/lib/file-tree-utils.js +12 -0
  21. package/lib/public/js/lib/syntax-highlighters/css.js +124 -0
  22. package/lib/public/js/lib/syntax-highlighters/frontmatter.js +49 -0
  23. package/lib/public/js/lib/syntax-highlighters/html.js +209 -0
  24. package/lib/public/js/lib/syntax-highlighters/index.js +28 -0
  25. package/lib/public/js/lib/syntax-highlighters/javascript.js +134 -0
  26. package/lib/public/js/lib/syntax-highlighters/json.js +61 -0
  27. package/lib/public/js/lib/syntax-highlighters/markdown.js +37 -0
  28. package/lib/public/js/lib/syntax-highlighters/utils.js +13 -0
  29. package/lib/public/setup.html +1 -0
  30. package/lib/server/constants.js +1 -0
  31. package/lib/server/onboarding/workspace.js +3 -2
  32. package/lib/server/routes/browse.js +295 -0
  33. package/lib/server.js +24 -3
  34. package/lib/setup/core-prompts/TOOLS.md +3 -1
  35. package/lib/setup/skills/control-ui/SKILL.md +12 -20
  36. package/package.json +1 -1
@@ -40,3 +40,185 @@ export const CloseIcon = ({ className = "" }) => html`
40
40
  />
41
41
  </svg>
42
42
  `;
43
+
44
+ export const HomeLineIcon = ({ className = "" }) => html`
45
+ <svg
46
+ class=${className}
47
+ viewBox="0 0 24 24"
48
+ fill="currentColor"
49
+ aria-hidden="true"
50
+ >
51
+ <path
52
+ d="M13 19H19V9.97815L12 4.53371L5 9.97815V19H11V13H13V19ZM21 20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V9.48907C3 9.18048 3.14247 8.88917 3.38606 8.69972L11.3861 2.47749C11.7472 2.19663 12.2528 2.19663 12.6139 2.47749L20.6139 8.69972C20.8575 8.88917 21 9.18048 21 9.48907V20Z"
53
+ />
54
+ </svg>
55
+ `;
56
+
57
+ export const FolderLineIcon = ({ className = "" }) => html`
58
+ <svg
59
+ class=${className}
60
+ viewBox="0 0 24 24"
61
+ fill="currentColor"
62
+ aria-hidden="true"
63
+ >
64
+ <path
65
+ d="M4 5V19H20V7H11.5858L9.58579 5H4ZM12.4142 5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5Z"
66
+ />
67
+ </svg>
68
+ `;
69
+
70
+ export const MarkdownFillIcon = ({ className = "" }) => html`
71
+ <svg
72
+ class=${className}
73
+ viewBox="0 0 24 24"
74
+ fill="currentColor"
75
+ aria-hidden="true"
76
+ >
77
+ <path
78
+ d="M3 3H21C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3ZM7 15.5V11.5L9 13.5L11 11.5V15.5H13V8.5H11L9 10.5L7 8.5H5V15.5H7ZM18 12.5V8.5H16V12.5H14L17 15.5L20 12.5H18Z"
79
+ />
80
+ </svg>
81
+ `;
82
+
83
+ export const File3LineIcon = ({ className = "" }) => html`
84
+ <svg
85
+ class=${className}
86
+ viewBox="0 0 24 24"
87
+ fill="currentColor"
88
+ aria-hidden="true"
89
+ >
90
+ <path
91
+ d="M21 8V20.9932C21 21.5501 20.5552 22 20.0066 22H3.9934C3.44495 22 3 21.556 3 21.0082V2.9918C3 2.45531 3.4487 2 4.00221 2H14.9968L21 8ZM19 9H14V4H5V20H19V9Z"
92
+ />
93
+ </svg>
94
+ `;
95
+
96
+ export const JavascriptFillIcon = ({ className = "" }) => html`
97
+ <svg
98
+ class=${className}
99
+ viewBox="0 0 24 24"
100
+ fill="currentColor"
101
+ aria-hidden="true"
102
+ >
103
+ <path
104
+ d="M6 3C4.34315 3 3 4.34315 3 6V18C3 19.6569 4.34315 21 6 21H18C19.6569 21 21 19.6569 21 18V6C21 4.34315 19.6569 3 18 3H6ZM13.3344 16.055C14.0531 16.6343 14.7717 16.9203 15.4904 16.913C15.9304 16.913 16.2677 16.8323 16.5024 16.671C16.7297 16.517 16.8434 16.297 16.8434 16.011C16.8434 15.7177 16.7297 15.4683 16.5024 15.263C16.2677 15.0577 15.8241 14.8523 15.1714 14.647C14.3867 14.4197 13.7817 14.1263 13.3564 13.767C12.9384 13.4077 12.7257 12.9053 12.7184 12.26C12.7184 11.6513 12.9824 11.1417 13.5104 10.731C14.0237 10.3203 14.6801 10.115 15.4794 10.115C16.5941 10.115 17.4887 10.3863 18.1634 10.929L17.3934 12.128C17.1221 11.9153 16.8104 11.7613 16.4584 11.666C16.1064 11.556 15.7911 11.501 15.5124 11.501C15.1311 11.501 14.8267 11.5707 14.5994 11.71C14.3721 11.8493 14.2584 12.0327 14.2584 12.26C14.2584 12.5093 14.3977 12.722 14.6764 12.898C14.9551 13.0667 15.4317 13.2537 16.1064 13.459C16.9204 13.701 17.4997 14.0237 17.8444 14.427C18.1891 14.8303 18.3614 15.3437 18.3614 15.967C18.3614 16.605 18.1157 17.155 17.6244 17.617C17.1404 18.0717 16.4364 18.31 15.5124 18.332C14.3024 18.332 13.2904 17.969 12.4764 17.243L13.3344 16.055ZM7.80405 16.693C8.03872 16.8397 8.32105 16.913 8.65105 16.913C8.99572 16.913 9.28172 16.814 9.50905 16.616C9.73639 16.4107 9.85005 16.055 9.85005 15.549V10.247H11.3351V15.835C11.3131 16.7003 11.0637 17.3237 10.5871 17.705C10.3157 17.9323 10.0187 18.0937 9.69605 18.189C9.37339 18.2843 9.06172 18.332 8.76105 18.332C8.21105 18.332 7.72339 18.2367 7.29805 18.046C6.84339 17.8407 6.46205 17.4777 6.15405 16.957L7.18805 16.11C7.37872 16.3667 7.58405 16.561 7.80405 16.693Z"
105
+ />
106
+ </svg>
107
+ `;
108
+
109
+ export const Image2FillIcon = ({ className = "" }) => html`
110
+ <svg
111
+ class=${className}
112
+ viewBox="0 0 24 24"
113
+ fill="currentColor"
114
+ aria-hidden="true"
115
+ >
116
+ <path
117
+ d="M5 11.1005L7 9.1005L12.5 14.6005L16 11.1005L19 14.1005V5H5V11.1005ZM4 3H20C20.5523 3 21 3.44772 21 4V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3ZM15.5 10C14.6716 10 14 9.32843 14 8.5C14 7.67157 14.6716 7 15.5 7C16.3284 7 17 7.67157 17 8.5C17 9.32843 16.3284 10 15.5 10Z"
118
+ />
119
+ </svg>
120
+ `;
121
+
122
+ export const TerminalFillIcon = ({ className = "" }) => html`
123
+ <svg
124
+ class=${className}
125
+ viewBox="0 0 24 24"
126
+ fill="currentColor"
127
+ aria-hidden="true"
128
+ >
129
+ <path
130
+ d="M10.9999 12L3.92886 19.0711L2.51465 17.6569L8.1715 12L2.51465 6.34317L3.92886 4.92896L10.9999 12ZM10.9999 19H20.9999V21H10.9999V19Z"
131
+ />
132
+ </svg>
133
+ `;
134
+
135
+ export const BracesLineIcon = ({ className = "" }) => html`
136
+ <svg
137
+ class=${className}
138
+ viewBox="0 0 24 24"
139
+ fill="currentColor"
140
+ aria-hidden="true"
141
+ >
142
+ <path
143
+ d="M4 18V14.3C4 13.4716 3.32843 12.8 2.5 12.8H2V11.2H2.5C3.32843 11.2 4 10.5284 4 9.7V6C4 4.34315 5.34315 3 7 3H8V5H7C6.44772 5 6 5.44772 6 6V10.1C6 10.9858 5.42408 11.7372 4.62623 12C5.42408 12.2628 6 13.0142 6 13.9V18C6 18.5523 6.44772 19 7 19H8V21H7C5.34315 21 4 19.6569 4 18ZM20 14.3V18C20 19.6569 18.6569 21 17 21H16V19H17C17.5523 19 18 18.5523 18 18V13.9C18 13.0142 18.5759 12.2628 19.3738 12C18.5759 11.7372 18 10.9858 18 10.1V6C18 5.44772 17.5523 5 17 5H16V3H17C18.6569 3 20 4.34315 20 6V9.7C20 10.5284 20.6716 11.2 21.5 11.2H22V12.8H21.5C20.6716 12.8 20 13.4716 20 14.3Z"
144
+ />
145
+ </svg>
146
+ `;
147
+
148
+ export const FileCodeLineIcon = ({ className = "" }) => html`
149
+ <svg
150
+ class=${className}
151
+ viewBox="0 0 24 24"
152
+ fill="currentColor"
153
+ aria-hidden="true"
154
+ >
155
+ <path
156
+ d="M15 4H5V20H19V8H15V4ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918ZM17.6569 12L14.1213 15.5355L12.7071 14.1213L14.8284 12L12.7071 9.87868L14.1213 8.46447L17.6569 12ZM6.34315 12L9.87868 8.46447L11.2929 9.87868L9.17157 12L11.2929 14.1213L9.87868 15.5355L6.34315 12Z"
157
+ />
158
+ </svg>
159
+ `;
160
+
161
+ export const Database2LineIcon = ({ className = "" }) => html`
162
+ <svg
163
+ class=${className}
164
+ viewBox="0 0 24 24"
165
+ fill="currentColor"
166
+ aria-hidden="true"
167
+ >
168
+ <path
169
+ d="M5 12.5C5 12.8134 5.46101 13.3584 6.53047 13.8931C7.91405 14.5849 9.87677 15 12 15C14.1232 15 16.0859 14.5849 17.4695 13.8931C18.539 13.3584 19 12.8134 19 12.5V10.3287C17.35 11.3482 14.8273 12 12 12C9.17273 12 6.64996 11.3482 5 10.3287V12.5ZM19 15.3287C17.35 16.3482 14.8273 17 12 17C9.17273 17 6.64996 16.3482 5 15.3287V17.5C5 17.8134 5.46101 18.3584 6.53047 18.8931C7.91405 19.5849 9.87677 20 12 20C14.1232 20 16.0859 19.5849 17.4695 18.8931C18.539 18.3584 19 17.8134 19 17.5V15.3287ZM3 17.5V7.5C3 5.01472 7.02944 3 12 3C16.9706 3 21 5.01472 21 7.5V17.5C21 19.9853 16.9706 22 12 22C7.02944 22 3 19.9853 3 17.5ZM12 10C14.1232 10 16.0859 9.58492 17.4695 8.89313C18.539 8.3584 19 7.81342 19 7.5C19 7.18658 18.539 6.6416 17.4695 6.10687C16.0859 5.41508 14.1232 5 12 5C9.87677 5 7.91405 5.41508 6.53047 6.10687C5.46101 6.6416 5 7.18658 5 7.5C5 7.81342 5.46101 8.3584 6.53047 8.89313C7.91405 9.58492 9.87677 10 12 10Z"
170
+ />
171
+ </svg>
172
+ `;
173
+
174
+ export const HashtagIcon = ({ className = "" }) => html`
175
+ <svg
176
+ class=${className}
177
+ viewBox="0 0 24 24"
178
+ fill="currentColor"
179
+ aria-hidden="true"
180
+ >
181
+ <path
182
+ d="M7.78428 14L8.2047 10H4V8H8.41491L8.94043 3H10.9514L10.4259 8H14.4149L14.9404 3H16.9514L16.4259 8H20V10H16.2157L15.7953 14H20V16H15.5851L15.0596 21H13.0486L13.5741 16H9.58509L9.05957 21H7.04855L7.57407 16H4V14H7.78428ZM9.7953 14H13.7843L14.2047 10H10.2157L9.7953 14Z"
183
+ />
184
+ </svg>
185
+ `;
186
+
187
+ export const GitBranchLineIcon = ({ className = "" }) => html`
188
+ <svg
189
+ class=${className}
190
+ viewBox="0 0 24 24"
191
+ fill="currentColor"
192
+ aria-hidden="true"
193
+ >
194
+ <path
195
+ d="M7.10508 15.2101C8.21506 15.6501 9 16.7334 9 18C9 19.6569 7.65685 21 6 21C4.34315 21 3 19.6569 3 18C3 16.6938 3.83481 15.5825 5 15.1707V8.82929C3.83481 8.41746 3 7.30622 3 6C3 4.34315 4.34315 3 6 3C7.65685 3 9 4.34315 9 6C9 7.30622 8.16519 8.41746 7 8.82929V11.9996C7.83566 11.3719 8.87439 11 10 11H14C15.3835 11 16.5482 10.0635 16.8949 8.78991C15.7849 8.34988 15 7.26661 15 6C15 4.34315 16.3431 3 18 3C19.6569 3 21 4.34315 21 6C21 7.3332 20.1303 8.46329 18.9274 8.85392C18.5222 11.2085 16.4703 13 14 13H10C8.61653 13 7.45179 13.9365 7.10508 15.2101ZM6 17C5.44772 17 5 17.4477 5 18C5 18.5523 5.44772 19 6 19C6.55228 19 7 18.5523 7 18C7 17.4477 6.55228 17 6 17ZM6 5C5.44772 5 5 5.44772 5 6C5 6.55228 5.44772 7 6 7C6.55228 7 7 6.55228 7 6C7 5.44772 6.55228 5 6 5ZM18 5C17.4477 5 17 5.44772 17 6C17 6.55228 17.4477 7 18 7C18.5523 7 19 6.55228 19 6C19 5.44772 18.5523 5 18 5Z"
196
+ />
197
+ </svg>
198
+ `;
199
+
200
+ export const GithubFillIcon = ({ className = "" }) => html`
201
+ <svg
202
+ class=${className}
203
+ viewBox="0 0 24 24"
204
+ fill="currentColor"
205
+ aria-hidden="true"
206
+ >
207
+ <path
208
+ d="M12.001 2C6.47598 2 2.00098 6.475 2.00098 12C2.00098 16.425 4.86348 20.1625 8.83848 21.4875C9.33848 21.575 9.52598 21.275 9.52598 21.0125C9.52598 20.775 9.51348 19.9875 9.51348 19.15C7.00098 19.6125 6.35098 18.5375 6.15098 17.975C6.03848 17.6875 5.55098 16.8 5.12598 16.5625C4.77598 16.375 4.27598 15.9125 5.11348 15.9C5.90098 15.8875 6.46348 16.625 6.65098 16.925C7.55098 18.4375 8.98848 18.0125 9.56348 17.75C9.65098 17.1 9.91348 16.6625 10.201 16.4125C7.97598 16.1625 5.65098 15.3 5.65098 11.475C5.65098 10.3875 6.03848 9.4875 6.67598 8.7875C6.57598 8.5375 6.22598 7.5125 6.77598 6.1375C6.77598 6.1375 7.61348 5.875 9.52598 7.1625C10.326 6.9375 11.176 6.825 12.026 6.825C12.876 6.825 13.726 6.9375 14.526 7.1625C16.4385 5.8625 17.276 6.1375 17.276 6.1375C17.826 7.5125 17.476 8.5375 17.376 8.7875C18.0135 9.4875 18.401 10.375 18.401 11.475C18.401 15.3125 16.0635 16.1625 13.8385 16.4125C14.201 16.725 14.5135 17.325 14.5135 18.2625C14.5135 19.6 14.501 20.675 14.501 21.0125C14.501 21.275 14.6885 21.5875 15.1885 21.4875C19.259 20.1133 21.9999 16.2963 22.001 12C22.001 6.475 17.526 2 12.001 2Z"
209
+ />
210
+ </svg>
211
+ `;
212
+
213
+ export const SaveFillIcon = ({ className = "" }) => html`
214
+ <svg
215
+ class=${className}
216
+ viewBox="0 0 24 24"
217
+ fill="currentColor"
218
+ aria-hidden="true"
219
+ >
220
+ <path
221
+ d="M18 21V13H6V21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3H17L21 7V20C21 20.5523 20.5523 21 20 21H18ZM16 21H8V15H16V21Z"
222
+ />
223
+ </svg>
224
+ `;
@@ -0,0 +1,149 @@
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 { fetchBrowseGitSummary } from "../lib/api.js";
5
+ import { GitBranchLineIcon, GithubFillIcon } from "./icons.js";
6
+ import { LoadingSpinner } from "./loading-spinner.js";
7
+
8
+ const html = htm.bind(h);
9
+ const kRefreshMs = 10000;
10
+
11
+ const formatCommitTime = (unixSeconds) => {
12
+ if (!unixSeconds) return "";
13
+ try {
14
+ return new Date(unixSeconds * 1000).toLocaleString();
15
+ } catch {
16
+ return "";
17
+ }
18
+ };
19
+
20
+ const getRepoName = (summary) => {
21
+ const slug = String(summary?.repoSlug || "").trim();
22
+ if (slug) return slug;
23
+ const pathValue = String(summary?.repoPath || "");
24
+ const segment = pathValue.split("/").filter(Boolean).pop();
25
+ return segment || "repo";
26
+ };
27
+
28
+ export const SidebarGitPanel = () => {
29
+ const [loading, setLoading] = useState(true);
30
+ const [error, setError] = useState("");
31
+ const [summary, setSummary] = useState(null);
32
+
33
+ useEffect(() => {
34
+ let active = true;
35
+ let intervalId = null;
36
+
37
+ const loadSummary = async () => {
38
+ if (!active) return;
39
+ try {
40
+ const data = await fetchBrowseGitSummary();
41
+ if (!active) return;
42
+ setSummary(data);
43
+ setError("");
44
+ } catch (nextError) {
45
+ if (!active) return;
46
+ setError(nextError.message || "Could not load git summary");
47
+ } finally {
48
+ if (active) setLoading(false);
49
+ }
50
+ };
51
+
52
+ const handleFileSaved = () => {
53
+ loadSummary();
54
+ };
55
+
56
+ loadSummary();
57
+ intervalId = window.setInterval(loadSummary, kRefreshMs);
58
+ window.addEventListener("alphaclaw:browse-file-saved", handleFileSaved);
59
+
60
+ return () => {
61
+ active = false;
62
+ if (intervalId) window.clearInterval(intervalId);
63
+ window.removeEventListener("alphaclaw:browse-file-saved", handleFileSaved);
64
+ };
65
+ }, []);
66
+
67
+ if (loading) {
68
+ return html`
69
+ <div class="sidebar-git-panel sidebar-git-loading" aria-label="Loading git summary">
70
+ <${LoadingSpinner} className="h-4 w-4" />
71
+ </div>
72
+ `;
73
+ }
74
+
75
+ if (error) {
76
+ return html`<div class="sidebar-git-panel sidebar-git-panel-error">${error}</div>`;
77
+ }
78
+
79
+ if (!summary?.isRepo) {
80
+ return html`
81
+ <div class="sidebar-git-panel">
82
+ <div class="sidebar-git-meta">No git repo at this root</div>
83
+ </div>
84
+ `;
85
+ }
86
+
87
+ return html`
88
+ <div class="sidebar-git-panel">
89
+ <div class="sidebar-git-bar">
90
+ ${summary.repoUrl
91
+ ? html`
92
+ <a
93
+ class="sidebar-git-bar-main sidebar-git-link"
94
+ href=${summary.repoUrl}
95
+ target="_blank"
96
+ rel="noopener noreferrer"
97
+ title=${summary.repoUrl}
98
+ >
99
+ <${GithubFillIcon} className="sidebar-git-bar-icon" />
100
+ <span class="sidebar-git-repo-name">${getRepoName(summary)}</span>
101
+ </a>
102
+ `
103
+ : html`
104
+ <span class="sidebar-git-bar-main">
105
+ <${GithubFillIcon} className="sidebar-git-bar-icon" />
106
+ <span class="sidebar-git-repo-name">${getRepoName(summary)}</span>
107
+ </span>
108
+ `}
109
+ </div>
110
+ <div class="sidebar-git-bar sidebar-git-bar-secondary">
111
+ <span class="sidebar-git-bar-main">
112
+ <${GitBranchLineIcon} className="sidebar-git-bar-icon" />
113
+ <span class="sidebar-git-branch">${summary.branch || "unknown"}</span>
114
+ </span>
115
+ <span class=${`sidebar-git-dirty ${summary.isDirty ? "is-dirty" : "is-clean"}`}>
116
+ ${summary.isDirty ? "dirty" : "clean"}
117
+ </span>
118
+ </div>
119
+ ${(summary.commits || []).length > 0
120
+ ? html`
121
+ <ul class="sidebar-git-list">
122
+ ${(summary.commits || []).slice(0, 4).map(
123
+ (commit) => html`
124
+ <li title=${formatCommitTime(commit.timestamp)}>
125
+ ${commit.url
126
+ ? html`
127
+ <a
128
+ class="sidebar-git-commit-link"
129
+ href=${commit.url}
130
+ target="_blank"
131
+ rel="noopener noreferrer"
132
+ >
133
+ <span class="sidebar-git-hash">${commit.shortHash}</span>
134
+ <span>${commit.message}</span>
135
+ </a>
136
+ `
137
+ : html`
138
+ <span class="sidebar-git-hash">${commit.shortHash}</span>
139
+ <span>${commit.message}</span>
140
+ `}
141
+ </li>
142
+ `,
143
+ )}
144
+ </ul>
145
+ `
146
+ : null}
147
+ </div>
148
+ `;
149
+ };
@@ -0,0 +1,272 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import { useEffect, useRef, useState } from "https://esm.sh/preact/hooks";
3
+ import htm from "https://esm.sh/htm";
4
+ import { HomeLineIcon, FolderLineIcon } from "./icons.js";
5
+ import { FileTree } from "./file-tree.js";
6
+ import { UpdateActionButton } from "./update-action-button.js";
7
+ import { SidebarGitPanel } from "./sidebar-git-panel.js";
8
+
9
+ const html = htm.bind(h);
10
+ const kUiSettingsStorageKey = "alphaclaw.uiSettings";
11
+ const kLegacyUiSettingsStorageKey = "alphaclawUiSettings";
12
+ const kBrowseBottomPanelUiSettingKey = "browseBottomPanelHeightPx";
13
+ const kBrowsePanelMinHeightPx = 120;
14
+ const kBrowseBottomMinHeightPx = 120;
15
+ const kBrowseResizerHeightPx = 6;
16
+ const kDefaultBrowseBottomPanelHeightPx = 160;
17
+ const kLegacyBrowsePanelUiStorageKey = "alphaclawBrowsePanelHeightPx";
18
+
19
+ const readStoredBrowseBottomPanelHeight = () => {
20
+ try {
21
+ const rawSettings =
22
+ window.localStorage.getItem(kUiSettingsStorageKey) ||
23
+ window.localStorage.getItem(kLegacyUiSettingsStorageKey);
24
+ if (rawSettings) {
25
+ const parsedSettings = JSON.parse(rawSettings);
26
+ const fromSharedSettings = Number.parseInt(
27
+ String(parsedSettings?.[kBrowseBottomPanelUiSettingKey] || ""),
28
+ 10,
29
+ );
30
+ if (Number.isFinite(fromSharedSettings) && fromSharedSettings > 0) {
31
+ return fromSharedSettings;
32
+ }
33
+ }
34
+ const legacyRawValue = window.localStorage.getItem(kLegacyBrowsePanelUiStorageKey);
35
+ const parsedValue = Number.parseInt(String(legacyRawValue || ""), 10);
36
+ return Number.isFinite(parsedValue) && parsedValue > 0
37
+ ? parsedValue
38
+ : kDefaultBrowseBottomPanelHeightPx;
39
+ } catch {
40
+ return kDefaultBrowseBottomPanelHeightPx;
41
+ }
42
+ };
43
+
44
+ export const AppSidebar = ({
45
+ mobileSidebarOpen = false,
46
+ authEnabled = false,
47
+ menuRef = null,
48
+ menuOpen = false,
49
+ onToggleMenu = () => {},
50
+ onLogout = () => {},
51
+ sidebarTab = "menu",
52
+ onSelectSidebarTab = () => {},
53
+ navSections = [],
54
+ selectedNavId = "",
55
+ onSelectNavItem = () => {},
56
+ selectedBrowsePath = "",
57
+ onSelectBrowseFile = () => {},
58
+ acHasUpdate = false,
59
+ acLatest = "",
60
+ acDismissed = false,
61
+ acUpdating = false,
62
+ onAcUpdate = () => {},
63
+ }) => {
64
+ const browseLayoutRef = useRef(null);
65
+ const browseBottomPanelRef = useRef(null);
66
+ const browseResizeStartRef = useRef({ startY: 0, startHeight: 0 });
67
+ const [browseBottomPanelHeightPx, setBrowseBottomPanelHeightPx] = useState(
68
+ readStoredBrowseBottomPanelHeight,
69
+ );
70
+ const [isResizingBrowsePanels, setIsResizingBrowsePanels] = useState(false);
71
+
72
+ useEffect(() => {
73
+ try {
74
+ const rawSettings =
75
+ window.localStorage.getItem(kUiSettingsStorageKey) ||
76
+ window.localStorage.getItem(kLegacyUiSettingsStorageKey);
77
+ const parsedSettings = rawSettings ? JSON.parse(rawSettings) : {};
78
+ const nextSettings =
79
+ parsedSettings && typeof parsedSettings === "object"
80
+ ? { ...parsedSettings }
81
+ : {};
82
+ nextSettings[kBrowseBottomPanelUiSettingKey] = browseBottomPanelHeightPx;
83
+ window.localStorage.setItem(kUiSettingsStorageKey, JSON.stringify(nextSettings));
84
+ } catch {}
85
+ }, [browseBottomPanelHeightPx]);
86
+
87
+ const getClampedBrowseBottomPanelHeight = (value) => {
88
+ const layoutElement = browseLayoutRef.current;
89
+ if (!layoutElement) return value;
90
+ const layoutRect = layoutElement.getBoundingClientRect();
91
+ const maxHeight = Math.max(
92
+ kBrowseBottomMinHeightPx,
93
+ layoutRect.height - kBrowsePanelMinHeightPx - kBrowseResizerHeightPx,
94
+ );
95
+ return Math.max(
96
+ kBrowseBottomMinHeightPx,
97
+ Math.min(maxHeight, value),
98
+ );
99
+ };
100
+
101
+ const resizeBrowsePanelWithClientY = (clientY) => {
102
+ const { startY, startHeight } = browseResizeStartRef.current;
103
+ const proposedHeight = startHeight + (startY - clientY);
104
+ setBrowseBottomPanelHeightPx(getClampedBrowseBottomPanelHeight(proposedHeight));
105
+ };
106
+
107
+ useEffect(() => {
108
+ const layoutElement = browseLayoutRef.current;
109
+ if (!layoutElement || typeof ResizeObserver === "undefined") return () => {};
110
+ const observer = new ResizeObserver(() => {
111
+ setBrowseBottomPanelHeightPx((currentHeight) =>
112
+ getClampedBrowseBottomPanelHeight(currentHeight),
113
+ );
114
+ });
115
+ observer.observe(layoutElement);
116
+ return () => observer.disconnect();
117
+ }, []);
118
+
119
+ useEffect(() => {
120
+ if (!isResizingBrowsePanels) return () => {};
121
+ const handlePointerMove = (event) => resizeBrowsePanelWithClientY(event.clientY);
122
+ const handlePointerUp = () => setIsResizingBrowsePanels(false);
123
+ window.addEventListener("pointermove", handlePointerMove);
124
+ window.addEventListener("pointerup", handlePointerUp);
125
+ return () => {
126
+ window.removeEventListener("pointermove", handlePointerMove);
127
+ window.removeEventListener("pointerup", handlePointerUp);
128
+ };
129
+ }, [isResizingBrowsePanels]);
130
+
131
+ const onBrowsePanelResizerPointerDown = (event) => {
132
+ event.preventDefault();
133
+ const measuredHeight =
134
+ browseBottomPanelRef.current?.getBoundingClientRect().height ||
135
+ browseBottomPanelHeightPx;
136
+ browseResizeStartRef.current = {
137
+ startY: event.clientY,
138
+ startHeight: measuredHeight,
139
+ };
140
+ setBrowseBottomPanelHeightPx(getClampedBrowseBottomPanelHeight(measuredHeight));
141
+ setIsResizingBrowsePanels(true);
142
+ };
143
+
144
+ return html`
145
+ <div class=${`app-sidebar ${mobileSidebarOpen ? "mobile-open" : ""}`}>
146
+ <div class="sidebar-brand">
147
+ <img src="./img/logo.svg" alt="" width="20" height="20" />
148
+ <span><span style="color: var(--accent)">alpha</span>claw</span>
149
+ ${authEnabled && html`
150
+ <div class="brand-menu" ref=${menuRef}>
151
+ <button
152
+ class="brand-menu-trigger"
153
+ onclick=${onToggleMenu}
154
+ aria-label="Menu"
155
+ >
156
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
157
+ <circle cx="8" cy="3" r="1.5" />
158
+ <circle cx="8" cy="8" r="1.5" />
159
+ <circle cx="8" cy="13" r="1.5" />
160
+ </svg>
161
+ </button>
162
+ ${menuOpen && html`
163
+ <div class="brand-dropdown">
164
+ <a
165
+ href="#"
166
+ onclick=${(event) => {
167
+ event.preventDefault();
168
+ onLogout();
169
+ }}
170
+ >Log out</a>
171
+ </div>
172
+ `}
173
+ </div>
174
+ `}
175
+ </div>
176
+ <div class="sidebar-tabs">
177
+ <button
178
+ class=${`sidebar-tab ${sidebarTab === "menu" ? "active" : ""}`}
179
+ aria-label="Menu tab"
180
+ title="Menu"
181
+ onclick=${() => onSelectSidebarTab("menu")}
182
+ >
183
+ <${HomeLineIcon} className="sidebar-tab-icon" />
184
+ </button>
185
+ <button
186
+ class=${`sidebar-tab ${sidebarTab === "browse" ? "active" : ""}`}
187
+ aria-label="Browse tab"
188
+ title="Browse"
189
+ onclick=${() => onSelectSidebarTab("browse")}
190
+ >
191
+ <${FolderLineIcon} className="sidebar-tab-icon" />
192
+ </button>
193
+ </div>
194
+ ${sidebarTab === "menu"
195
+ ? navSections.map(
196
+ (section) => html`
197
+ <div class="sidebar-label">${section.label}</div>
198
+ <nav class="sidebar-nav">
199
+ ${section.items.map(
200
+ (item) => html`
201
+ <a
202
+ class=${selectedNavId === item.id ? "active" : ""}
203
+ onclick=${() => onSelectNavItem(item.id)}
204
+ >
205
+ ${item.label}
206
+ </a>
207
+ `,
208
+ )}
209
+ </nav>
210
+ `,
211
+ )
212
+ : html`
213
+ <div class="sidebar-browse-layout" ref=${browseLayoutRef}>
214
+ <div
215
+ class="sidebar-browse-panel"
216
+ >
217
+ <${FileTree}
218
+ onSelectFile=${onSelectBrowseFile}
219
+ selectedPath=${selectedBrowsePath}
220
+ />
221
+ </div>
222
+ <div
223
+ class=${`sidebar-browse-resizer ${isResizingBrowsePanels ? "is-resizing" : ""}`}
224
+ onpointerdown=${onBrowsePanelResizerPointerDown}
225
+ role="separator"
226
+ aria-orientation="horizontal"
227
+ aria-label="Resize browse and git panels"
228
+ ></div>
229
+ <div class="sidebar-browse-bottom">
230
+ <div
231
+ class="sidebar-browse-bottom-inner"
232
+ ref=${browseBottomPanelRef}
233
+ style=${{ height: `${browseBottomPanelHeightPx}px` }}
234
+ >
235
+ <${SidebarGitPanel} />
236
+ ${acHasUpdate && acLatest && !acDismissed
237
+ ? html`
238
+ <${UpdateActionButton}
239
+ onClick=${onAcUpdate}
240
+ loading=${acUpdating}
241
+ warning=${true}
242
+ idleLabel=${`Update to v${acLatest}`}
243
+ loadingLabel="Updating..."
244
+ className="w-full justify-center"
245
+ />
246
+ `
247
+ : null}
248
+ </div>
249
+ </div>
250
+ </div>
251
+ `}
252
+ ${sidebarTab === "menu"
253
+ ? html`
254
+ <div class="sidebar-footer">
255
+ ${acHasUpdate && acLatest && !acDismissed
256
+ ? html`
257
+ <${UpdateActionButton}
258
+ onClick=${onAcUpdate}
259
+ loading=${acUpdating}
260
+ warning=${true}
261
+ idleLabel=${`Update to v${acLatest}`}
262
+ loadingLabel="Updating..."
263
+ className="w-full justify-center"
264
+ />
265
+ `
266
+ : null}
267
+ </div>
268
+ `
269
+ : null}
270
+ </div>
271
+ `;
272
+ };
@@ -343,3 +343,29 @@ export async function fetchWebhookRequest(name, id) {
343
343
  );
344
344
  return parseJsonOrThrow(res, 'Could not load webhook request');
345
345
  }
346
+
347
+ export const fetchBrowseTree = async (depth = 10) => {
348
+ const params = new URLSearchParams({ depth: String(depth) });
349
+ const res = await authFetch(`/api/browse/tree?${params.toString()}`);
350
+ return parseJsonOrThrow(res, 'Could not load file tree');
351
+ };
352
+
353
+ export const fetchFileContent = async (filePath) => {
354
+ const params = new URLSearchParams({ path: String(filePath || '') });
355
+ const res = await authFetch(`/api/browse/read?${params.toString()}`);
356
+ return parseJsonOrThrow(res, 'Could not load file content');
357
+ };
358
+
359
+ export const saveFileContent = async (filePath, content) => {
360
+ const res = await authFetch('/api/browse/write', {
361
+ method: 'PUT',
362
+ headers: { 'Content-Type': 'application/json' },
363
+ body: JSON.stringify({ path: filePath, content }),
364
+ });
365
+ return parseJsonOrThrow(res, 'Could not save file');
366
+ };
367
+
368
+ export const fetchBrowseGitSummary = async () => {
369
+ const res = await authFetch('/api/browse/git-summary');
370
+ return parseJsonOrThrow(res, 'Could not load git summary');
371
+ };