myrlin-workbook 0.9.26 → 0.9.27
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/package.json +6 -2
- package/scripts/build-lucide-bundle.mjs +244 -0
- package/scripts/build-material-bundle.mjs +108 -0
- package/src/state/store.js +2 -1
- package/src/web/public/app.js +117 -21
- package/src/web/public/index.html +2 -0
- package/src/web/public/styles.css +138 -16
- package/src/web/public/terminal.js +5 -1
- package/src/web/public/vendor/lucide.bundle.js +7 -0
- package/src/web/public/vendor/material-icons.bundle.js +8 -0
- package/src/web/server.js +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myrlin-workbook",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.27",
|
|
4
4
|
"description": "Browser-based project manager for Claude Code sessions - session discovery, multi-terminal, cost tracking, docs, and kanban board",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"mcp:visual-qa": "node src/mcp/visual-qa.js",
|
|
21
21
|
"gui:cdp": "node src/supervisor.js --cdp",
|
|
22
22
|
"postinstall": "node scripts/postinstall.js",
|
|
23
|
-
"restart": "bash scripts/restart-gui.sh"
|
|
23
|
+
"restart": "bash scripts/restart-gui.sh",
|
|
24
|
+
"build:icons": "node scripts/build-lucide-bundle.mjs && node scripts/build-material-bundle.mjs"
|
|
24
25
|
},
|
|
25
26
|
"repository": {
|
|
26
27
|
"type": "git",
|
|
@@ -46,17 +47,20 @@
|
|
|
46
47
|
"author": "Arthur",
|
|
47
48
|
"license": "AGPL-3.0-only",
|
|
48
49
|
"dependencies": {
|
|
50
|
+
"@lucide/lab": "^0.1.2",
|
|
49
51
|
"blessed": "^0.1.81",
|
|
50
52
|
"blessed-contrib": "^4.11.0",
|
|
51
53
|
"chalk": "^5.6.2",
|
|
52
54
|
"chrome-remote-interface": "^0.34.0",
|
|
53
55
|
"express": "^5.2.1",
|
|
56
|
+
"lucide": "^1.8.0",
|
|
54
57
|
"node-pty": "^1.1.0",
|
|
55
58
|
"qrcode": "^1.5.4",
|
|
56
59
|
"simple-git": "^3.33.0",
|
|
57
60
|
"ws": "^8.19.0"
|
|
58
61
|
},
|
|
59
62
|
"devDependencies": {
|
|
63
|
+
"@material-icons/svg": "^1.0.33",
|
|
60
64
|
"@playwright/test": "^1.58.2",
|
|
61
65
|
"@xterm/addon-fit": "^0.11.0",
|
|
62
66
|
"@xterm/addon-web-links": "^0.12.0",
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Build script: generates public/vendor/lucide.bundle.js
|
|
4
|
+
* Imports curated icons from `lucide` and `@lucide/lab`, converts to SVG strings,
|
|
5
|
+
* and outputs a plain JS bundle that sets window.__lucideIcons.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { dirname, resolve } from 'path';
|
|
11
|
+
import vm from 'vm';
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const ROOT = resolve(__dirname, '..');
|
|
15
|
+
const OUT = resolve(ROOT, 'src/web/public/vendor/lucide.bundle.js');
|
|
16
|
+
|
|
17
|
+
// ── Curated icon list ────────────────────────────────────────────────────────
|
|
18
|
+
// Format: { category: [[kebab-name, 'lucide' | 'lab'], ...], ... }
|
|
19
|
+
const ICON_CATEGORIES = {
|
|
20
|
+
'Folders & Files': [
|
|
21
|
+
['folder', 'lucide'],
|
|
22
|
+
['folder-open', 'lucide'],
|
|
23
|
+
['folder-code', 'lucide'],
|
|
24
|
+
['folder-git-2', 'lucide'],
|
|
25
|
+
['folder-sync', 'lucide'],
|
|
26
|
+
['folder-key', 'lucide'],
|
|
27
|
+
['folder-search', 'lucide'],
|
|
28
|
+
['folders', 'lucide'],
|
|
29
|
+
['file', 'lucide'],
|
|
30
|
+
['file-code', 'lucide'],
|
|
31
|
+
['file-text', 'lucide'],
|
|
32
|
+
['files', 'lucide'],
|
|
33
|
+
['notebook', 'lucide'],
|
|
34
|
+
['notebook-pen', 'lucide'],
|
|
35
|
+
],
|
|
36
|
+
'Code & Dev': [
|
|
37
|
+
['code', 'lucide'],
|
|
38
|
+
['code-xml', 'lucide'],
|
|
39
|
+
['square-terminal', 'lucide'],
|
|
40
|
+
['terminal', 'lucide'],
|
|
41
|
+
['git-branch', 'lucide'],
|
|
42
|
+
['git-merge', 'lucide'],
|
|
43
|
+
['git-commit-vertical', 'lucide'],
|
|
44
|
+
['git-fork', 'lucide'],
|
|
45
|
+
['braces', 'lucide'],
|
|
46
|
+
['brackets', 'lucide'],
|
|
47
|
+
['hash', 'lucide'],
|
|
48
|
+
['variable', 'lucide'],
|
|
49
|
+
],
|
|
50
|
+
'Infrastructure': [
|
|
51
|
+
['database', 'lucide'],
|
|
52
|
+
['database-zap', 'lucide'],
|
|
53
|
+
['server', 'lucide'],
|
|
54
|
+
['server-cog', 'lucide'],
|
|
55
|
+
['cpu', 'lucide'],
|
|
56
|
+
['hard-drive', 'lucide'],
|
|
57
|
+
['network', 'lucide'],
|
|
58
|
+
['wifi', 'lucide'],
|
|
59
|
+
['cloud', 'lucide'],
|
|
60
|
+
['cloud-upload', 'lucide'],
|
|
61
|
+
['globe', 'lucide'],
|
|
62
|
+
['layers', 'lucide'],
|
|
63
|
+
],
|
|
64
|
+
'Apps & Tools': [
|
|
65
|
+
['box', 'lucide'],
|
|
66
|
+
['package', 'lucide'],
|
|
67
|
+
['package-2', 'lucide'],
|
|
68
|
+
['workflow', 'lucide'],
|
|
69
|
+
['settings', 'lucide'],
|
|
70
|
+
['settings-2', 'lucide'],
|
|
71
|
+
['wrench', 'lucide'],
|
|
72
|
+
['hammer', 'lucide'],
|
|
73
|
+
['cog', 'lucide'],
|
|
74
|
+
['tool-case', 'lucide'],
|
|
75
|
+
['puzzle', 'lucide'],
|
|
76
|
+
['blocks', 'lucide'],
|
|
77
|
+
['plug', 'lucide'],
|
|
78
|
+
['cpu', 'lucide'],
|
|
79
|
+
],
|
|
80
|
+
'Design & Art': [
|
|
81
|
+
['palette', 'lucide'],
|
|
82
|
+
['brush', 'lucide'],
|
|
83
|
+
['paintbrush', 'lucide'],
|
|
84
|
+
['wand', 'lucide'],
|
|
85
|
+
['wand-sparkles', 'lucide'],
|
|
86
|
+
['sparkles', 'lucide'],
|
|
87
|
+
['pen-tool', 'lucide'],
|
|
88
|
+
['pencil', 'lucide'],
|
|
89
|
+
['scissors', 'lucide'],
|
|
90
|
+
['crop', 'lucide'],
|
|
91
|
+
['image', 'lucide'],
|
|
92
|
+
['camera', 'lucide'],
|
|
93
|
+
],
|
|
94
|
+
'Learning & Science': [
|
|
95
|
+
['book-open', 'lucide'],
|
|
96
|
+
['book', 'lucide'],
|
|
97
|
+
['scroll', 'lucide'],
|
|
98
|
+
['flask-conical', 'lucide'],
|
|
99
|
+
['flask-round', 'lucide'],
|
|
100
|
+
['microscope', 'lucide'],
|
|
101
|
+
['test-tube', 'lucide'],
|
|
102
|
+
['brain', 'lucide'],
|
|
103
|
+
['brain-circuit', 'lucide'],
|
|
104
|
+
['atom', 'lucide'],
|
|
105
|
+
['dna', 'lucide'],
|
|
106
|
+
['telescope', 'lucide'],
|
|
107
|
+
],
|
|
108
|
+
'People & Status': [
|
|
109
|
+
['user', 'lucide'],
|
|
110
|
+
['users', 'lucide'],
|
|
111
|
+
['user-round', 'lucide'],
|
|
112
|
+
['bot', 'lucide'],
|
|
113
|
+
['rocket', 'lucide'],
|
|
114
|
+
['star', 'lucide'],
|
|
115
|
+
['heart', 'lucide'],
|
|
116
|
+
['zap', 'lucide'],
|
|
117
|
+
['shield', 'lucide'],
|
|
118
|
+
['lock', 'lucide'],
|
|
119
|
+
['key', 'lucide'],
|
|
120
|
+
['flag', 'lucide'],
|
|
121
|
+
['trophy', 'lucide'],
|
|
122
|
+
['medal', 'lucide'],
|
|
123
|
+
['crown', 'lucide'],
|
|
124
|
+
],
|
|
125
|
+
'Home & Work': [
|
|
126
|
+
['house', 'lucide'],
|
|
127
|
+
['briefcase', 'lucide'],
|
|
128
|
+
['building', 'lucide'],
|
|
129
|
+
['building-2', 'lucide'],
|
|
130
|
+
['store', 'lucide'],
|
|
131
|
+
['landmark', 'lucide'],
|
|
132
|
+
['map-pin', 'lucide'],
|
|
133
|
+
['compass', 'lucide'],
|
|
134
|
+
['map', 'lucide'],
|
|
135
|
+
['globe-2', 'lucide'],
|
|
136
|
+
['mountain', 'lucide'],
|
|
137
|
+
],
|
|
138
|
+
'Media & Comms': [
|
|
139
|
+
['monitor', 'lucide'],
|
|
140
|
+
['laptop', 'lucide'],
|
|
141
|
+
['smartphone', 'lucide'],
|
|
142
|
+
['mail', 'lucide'],
|
|
143
|
+
['message-circle', 'lucide'],
|
|
144
|
+
['bell', 'lucide'],
|
|
145
|
+
['phone', 'lucide'],
|
|
146
|
+
['mic', 'lucide'],
|
|
147
|
+
['music', 'lucide'],
|
|
148
|
+
['headphones', 'lucide'],
|
|
149
|
+
['video', 'lucide'],
|
|
150
|
+
['radio', 'lucide'],
|
|
151
|
+
],
|
|
152
|
+
'Nature & Fun': [
|
|
153
|
+
['sun', 'lucide'],
|
|
154
|
+
['moon', 'lucide'],
|
|
155
|
+
['cloud-sun', 'lucide'],
|
|
156
|
+
['flame', 'lucide'],
|
|
157
|
+
['waves', 'lucide'],
|
|
158
|
+
['coffee', 'lucide'],
|
|
159
|
+
['leaf', 'lucide'],
|
|
160
|
+
['flower', 'lucide'],
|
|
161
|
+
['flower-2', 'lucide'],
|
|
162
|
+
['tree-pine', 'lucide'],
|
|
163
|
+
['trees', 'lucide'],
|
|
164
|
+
['snail', 'lucide'],
|
|
165
|
+
['fish', 'lucide'],
|
|
166
|
+
['rabbit', 'lucide'],
|
|
167
|
+
['turtle', 'lucide'],
|
|
168
|
+
['bird', 'lucide'],
|
|
169
|
+
['dog', 'lucide'],
|
|
170
|
+
['cat', 'lucide'],
|
|
171
|
+
],
|
|
172
|
+
'Lab Extras': [
|
|
173
|
+
['owl', 'lab'],
|
|
174
|
+
['planet', 'lab'],
|
|
175
|
+
['bee', 'lab'],
|
|
176
|
+
['venn', 'lab'],
|
|
177
|
+
['copy-code', 'lab'],
|
|
178
|
+
['grid-lines', 'lab'],
|
|
179
|
+
['farm', 'lab'],
|
|
180
|
+
['toolbox', 'lab'],
|
|
181
|
+
['toolbox-2', 'lab'],
|
|
182
|
+
['snowman', 'lab'],
|
|
183
|
+
['mountain-snow', 'lab'],
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// ── SVG serializer ────────────────────────────────────────────────────────────
|
|
188
|
+
function iconDataToSvg(iconData) {
|
|
189
|
+
const children = iconData.map(([tag, attrs]) => {
|
|
190
|
+
// Filter out non-SVG metadata attrs ('key' is a React/lucide internal)
|
|
191
|
+
const entries = Object.entries(attrs).filter(([k]) => k !== 'key');
|
|
192
|
+
const attrStr = entries.map(([k, v]) => `${k}="${v}"`).join(' ');
|
|
193
|
+
return `<${tag} ${attrStr}/>`;
|
|
194
|
+
}).join('');
|
|
195
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">${children}</svg>`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
199
|
+
const icons = {}; // { "folder": "<svg>...</svg>", ... }
|
|
200
|
+
const categories = {}; // { "Folders & Files": ["folder", "folder-open", ...], ... }
|
|
201
|
+
const skipped = [];
|
|
202
|
+
|
|
203
|
+
for (const [category, entries] of Object.entries(ICON_CATEGORIES)) {
|
|
204
|
+
const catIcons = [];
|
|
205
|
+
for (const [name, pkg] of entries) {
|
|
206
|
+
// De-dup (e.g. 'cpu' appears twice in the list above)
|
|
207
|
+
if (icons[name]) { catIcons.push(name); continue; }
|
|
208
|
+
|
|
209
|
+
const iconDir = pkg === 'lab'
|
|
210
|
+
? resolve(ROOT, `node_modules/@lucide/lab/dist/esm/icons/${name}.js`)
|
|
211
|
+
: resolve(ROOT, `node_modules/lucide/dist/esm/icons/${name}.js`);
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const src = readFileSync(iconDir, 'utf8');
|
|
215
|
+
// Extract: const <Name> = <array>;\nexport { <Name> as default }
|
|
216
|
+
// Use greedy match up to the last ";" before the export statement
|
|
217
|
+
const match = src.match(/const \w+ = ([\s\S]+?);\s*\nexport/);
|
|
218
|
+
if (!match) { skipped.push(`${name} (parse fail)`); continue; }
|
|
219
|
+
// Evaluate JS object literal (has unquoted keys, not valid JSON)
|
|
220
|
+
const iconData = vm.runInNewContext(`(${match[1]})`);
|
|
221
|
+
icons[name] = iconDataToSvg(iconData);
|
|
222
|
+
catIcons.push(name);
|
|
223
|
+
} catch {
|
|
224
|
+
skipped.push(`${name} (${pkg})`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (catIcons.length > 0) categories[category] = catIcons;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Output ────────────────────────────────────────────────────────────────────
|
|
231
|
+
mkdirSync(resolve(ROOT, 'src/web/public/vendor'), { recursive: true });
|
|
232
|
+
|
|
233
|
+
const output = `/* lucide icons bundle — generated by scripts/build-lucide-bundle.mjs */
|
|
234
|
+
/* Includes ${Object.keys(icons).length} icons from lucide + @lucide/lab */
|
|
235
|
+
(function() {
|
|
236
|
+
window.__lucideIcons = ${JSON.stringify(icons)};
|
|
237
|
+
window.__lucideIconCategories = ${JSON.stringify(categories)};
|
|
238
|
+
document.dispatchEvent(new Event('lucide-ready'));
|
|
239
|
+
})();
|
|
240
|
+
`;
|
|
241
|
+
|
|
242
|
+
writeFileSync(OUT, output);
|
|
243
|
+
console.log(`✓ Built ${Object.keys(icons).length} icons → src/web/public/vendor/lucide.bundle.js`);
|
|
244
|
+
if (skipped.length) console.log(` Skipped (not found): ${skipped.join(', ')}`);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Build script: generates public/vendor/material-icons.bundle.js
|
|
4
|
+
* Includes ALL ~2191 icons from @material-icons/svg (baseline/filled variant),
|
|
5
|
+
* organized by their official categories from data.json.
|
|
6
|
+
*
|
|
7
|
+
* Icon names are stored with a "mi/" prefix when used in ws.icon so they
|
|
8
|
+
* can coexist with Lucide icon names without collision.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import { dirname, resolve } from 'path';
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const ROOT = resolve(__dirname, '..');
|
|
17
|
+
const ICONS_DIR = resolve(ROOT, 'node_modules/@material-icons/svg/svg');
|
|
18
|
+
const DATA_JSON = resolve(ROOT, 'node_modules/@material-icons/svg/data.json');
|
|
19
|
+
const OUT = resolve(ROOT, 'src/web/public/vendor/material-icons.bundle.js');
|
|
20
|
+
|
|
21
|
+
// Official category short-name → display name
|
|
22
|
+
const CATEGORY_LABELS = {
|
|
23
|
+
action: 'Action',
|
|
24
|
+
alert: 'Alert',
|
|
25
|
+
av: 'Audio & Video',
|
|
26
|
+
communication:'Communication',
|
|
27
|
+
content: 'Content',
|
|
28
|
+
device: 'Device',
|
|
29
|
+
editor: 'Editor',
|
|
30
|
+
file: 'File',
|
|
31
|
+
hardware: 'Hardware',
|
|
32
|
+
home: 'Home',
|
|
33
|
+
image: 'Image',
|
|
34
|
+
maps: 'Maps',
|
|
35
|
+
math: 'Math',
|
|
36
|
+
navigation: 'Navigation',
|
|
37
|
+
notification: 'Notification',
|
|
38
|
+
places: 'Places',
|
|
39
|
+
search: 'Search',
|
|
40
|
+
shopping: 'Shopping',
|
|
41
|
+
social: 'Social',
|
|
42
|
+
toggle: 'Toggle',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ── SVG processor ─────────────────────────────────────────────────────────────
|
|
46
|
+
function processSvg(raw) {
|
|
47
|
+
return raw
|
|
48
|
+
.replace(/width="24"/, 'width="14"')
|
|
49
|
+
.replace(/height="24"/, 'height="14"')
|
|
50
|
+
// Make color inherit from CSS (without this, icons always render black)
|
|
51
|
+
.replace('<svg ', '<svg fill="currentColor" ');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Load icon metadata ────────────────────────────────────────────────────────
|
|
55
|
+
const data = JSON.parse(readFileSync(DATA_JSON, 'utf8'));
|
|
56
|
+
const iconMeta = data.icons; // [{ name, categories: [string], tags: [string] }]
|
|
57
|
+
|
|
58
|
+
// Build category → [name] map using official categories
|
|
59
|
+
const categoryMap = {}; // { 'Audio & Video': ['10k', ...], ... }
|
|
60
|
+
for (const icon of iconMeta) {
|
|
61
|
+
for (const cat of (icon.categories || [])) {
|
|
62
|
+
const label = CATEGORY_LABELS[cat] || cat;
|
|
63
|
+
if (!categoryMap[label]) categoryMap[label] = [];
|
|
64
|
+
categoryMap[label].push(icon.name);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Process all icons ─────────────────────────────────────────────────────────
|
|
69
|
+
const icons = {}; // { name: '<svg>...' }
|
|
70
|
+
const categories = {}; // { 'Audio & Video': ['10k', ...] }
|
|
71
|
+
const skipped = [];
|
|
72
|
+
|
|
73
|
+
for (const icon of iconMeta) {
|
|
74
|
+
const svgPath = resolve(ICONS_DIR, icon.name, 'baseline.svg');
|
|
75
|
+
if (!existsSync(svgPath)) {
|
|
76
|
+
skipped.push(icon.name);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const raw = readFileSync(svgPath, 'utf8').trim();
|
|
81
|
+
icons[icon.name] = processSvg(raw);
|
|
82
|
+
} catch {
|
|
83
|
+
skipped.push(icon.name);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Build final categories (only include icons that were successfully loaded)
|
|
88
|
+
for (const [label, names] of Object.entries(categoryMap)) {
|
|
89
|
+
const valid = names.filter(n => icons[n]);
|
|
90
|
+
if (valid.length > 0) categories[label] = valid;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Output ────────────────────────────────────────────────────────────────────
|
|
94
|
+
mkdirSync(resolve(ROOT, 'src/web/public/vendor'), { recursive: true });
|
|
95
|
+
|
|
96
|
+
const output = `/* Material Icons bundle — generated by scripts/build-material-bundle.mjs */
|
|
97
|
+
/* Includes ${Object.keys(icons).length} icons from @material-icons/svg (baseline variant) */
|
|
98
|
+
/* Icon names are stored as "mi/<name>" in workspace records to avoid Lucide collisions */
|
|
99
|
+
(function() {
|
|
100
|
+
window.__materialIcons = ${JSON.stringify(icons)};
|
|
101
|
+
window.__materialIconCategories = ${JSON.stringify(categories)};
|
|
102
|
+
document.dispatchEvent(new Event('material-icons-ready'));
|
|
103
|
+
})();
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
writeFileSync(OUT, output);
|
|
107
|
+
console.log(`✓ Built ${Object.keys(icons).length} Material icons across ${Object.keys(categories).length} categories → src/web/public/vendor/material-icons.bundle.js`);
|
|
108
|
+
if (skipped.length) console.log(` Skipped (not found): ${skipped.join(', ')}`);
|
package/src/state/store.js
CHANGED
|
@@ -452,7 +452,7 @@ class Store extends EventEmitter {
|
|
|
452
452
|
|
|
453
453
|
// ─── Workspace CRUD ──────────────────────────────────────
|
|
454
454
|
|
|
455
|
-
createWorkspace({ name, description = '', color = 'cyan' }) {
|
|
455
|
+
createWorkspace({ name, description = '', color = 'cyan', icon = null }) {
|
|
456
456
|
const id = crypto.randomUUID();
|
|
457
457
|
const now = new Date().toISOString();
|
|
458
458
|
const workspace = {
|
|
@@ -460,6 +460,7 @@ class Store extends EventEmitter {
|
|
|
460
460
|
name,
|
|
461
461
|
description,
|
|
462
462
|
color,
|
|
463
|
+
icon,
|
|
463
464
|
sessions: [],
|
|
464
465
|
createdAt: now,
|
|
465
466
|
lastActive: now,
|
package/src/web/public/app.js
CHANGED
|
@@ -1369,11 +1369,17 @@ class CWMApp {
|
|
|
1369
1369
|
const newTaskBtn = e.target.closest('.ws-new-task-btn');
|
|
1370
1370
|
if (newTaskBtn) { e.stopPropagation(); this.openNewTaskDialog(newTaskBtn.dataset.wsId); return; }
|
|
1371
1371
|
|
|
1372
|
-
const
|
|
1373
|
-
if (
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1372
|
+
const moreBtn = e.target.closest('.ws-more-btn');
|
|
1373
|
+
if (moreBtn) {
|
|
1374
|
+
e.stopPropagation();
|
|
1375
|
+
const wsId = moreBtn.dataset.id;
|
|
1376
|
+
const rect = moreBtn.getBoundingClientRect();
|
|
1377
|
+
this._renderContextItems('Workspace', [
|
|
1378
|
+
{ icon: '✏️', label: 'Rename', action: () => this.renameWorkspace(wsId) },
|
|
1379
|
+
{ icon: '🗑️', label: 'Delete', danger: true, action: () => this.deleteWorkspace(wsId) },
|
|
1380
|
+
], rect.right, rect.bottom);
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1377
1383
|
|
|
1378
1384
|
const wsSessionItem = e.target.closest('.ws-session-item');
|
|
1379
1385
|
if (wsSessionItem) {
|
|
@@ -2193,6 +2199,7 @@ class CWMApp {
|
|
|
2193
2199
|
{ key: 'name', label: 'Name', placeholder: 'my-project', required: true },
|
|
2194
2200
|
{ key: 'description', label: 'Description', placeholder: 'What is this project for?', type: 'textarea' },
|
|
2195
2201
|
{ key: 'color', label: 'Color', type: 'color' },
|
|
2202
|
+
{ key: 'icon', label: 'Icon', type: 'icon' },
|
|
2196
2203
|
],
|
|
2197
2204
|
confirmText: 'Create',
|
|
2198
2205
|
confirmClass: 'btn-primary',
|
|
@@ -2220,6 +2227,7 @@ class CWMApp {
|
|
|
2220
2227
|
{ key: 'name', label: 'Name', value: ws.name, required: true },
|
|
2221
2228
|
{ key: 'description', label: 'Description', value: ws.description || '', type: 'textarea' },
|
|
2222
2229
|
{ key: 'color', label: 'Color', type: 'color', value: ws.color },
|
|
2230
|
+
{ key: 'icon', label: 'Icon', type: 'icon', value: ws.icon || '' },
|
|
2223
2231
|
],
|
|
2224
2232
|
confirmText: 'Save',
|
|
2225
2233
|
confirmClass: 'btn-primary',
|
|
@@ -7884,6 +7892,53 @@ class CWMApp {
|
|
|
7884
7892
|
</div>`;
|
|
7885
7893
|
return;
|
|
7886
7894
|
}
|
|
7895
|
+
if (f.type === 'icon') {
|
|
7896
|
+
// Merge Lucide icons (bare name) and Material icons (mi/ prefix)
|
|
7897
|
+
const lucideIcons = window.__lucideIcons || {};
|
|
7898
|
+
const lucideCats = window.__lucideIconCategories || {};
|
|
7899
|
+
const materialIcons = window.__materialIcons || {};
|
|
7900
|
+
const materialCats = window.__materialIconCategories || {};
|
|
7901
|
+
|
|
7902
|
+
const selectedIcon = f.value || '';
|
|
7903
|
+
let gridHtml = `<div class="icon-swatch icon-swatch-none${!selectedIcon ? ' selected' : ''}" data-icon="" title="No icon">
|
|
7904
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4" stroke-dasharray="3 2"/></svg>
|
|
7905
|
+
</div>`;
|
|
7906
|
+
|
|
7907
|
+
// Lucide section
|
|
7908
|
+
if (Object.keys(lucideCats).length > 0) {
|
|
7909
|
+
gridHtml += `<span class="icon-picker-set-sep" data-set="lucide">Lucide</span>`;
|
|
7910
|
+
for (const [cat, names] of Object.entries(lucideCats)) {
|
|
7911
|
+
gridHtml += `<span class="icon-picker-cat-sep" data-cat="${this.escapeHtml(cat)}">${this.escapeHtml(cat)}</span>`;
|
|
7912
|
+
for (const name of names) {
|
|
7913
|
+
if (!lucideIcons[name]) continue;
|
|
7914
|
+
gridHtml += `<div class="icon-swatch${selectedIcon === name ? ' selected' : ''}" data-icon="${this.escapeHtml(name)}" title="${this.escapeHtml(name)}">${lucideIcons[name]}</div>`;
|
|
7915
|
+
}
|
|
7916
|
+
}
|
|
7917
|
+
}
|
|
7918
|
+
|
|
7919
|
+
// Material section
|
|
7920
|
+
if (Object.keys(materialCats).length > 0) {
|
|
7921
|
+
gridHtml += `<span class="icon-picker-set-sep" data-set="material">Material</span>`;
|
|
7922
|
+
for (const [cat, names] of Object.entries(materialCats)) {
|
|
7923
|
+
gridHtml += `<span class="icon-picker-cat-sep" data-cat="${this.escapeHtml('Material — ' + cat)}">${this.escapeHtml(cat)}</span>`;
|
|
7924
|
+
for (const name of names) {
|
|
7925
|
+
if (!materialIcons[name]) continue;
|
|
7926
|
+
const storedName = 'mi/' + name;
|
|
7927
|
+
gridHtml += `<div class="icon-swatch${selectedIcon === storedName ? ' selected' : ''}" data-icon="${this.escapeHtml(storedName)}" title="${this.escapeHtml(name)}">${materialIcons[name]}</div>`;
|
|
7928
|
+
}
|
|
7929
|
+
}
|
|
7930
|
+
}
|
|
7931
|
+
|
|
7932
|
+
bodyHtml += `
|
|
7933
|
+
<div class="input-group">
|
|
7934
|
+
<label class="input-label">${f.label} <span class="field-optional">optional</span></label>
|
|
7935
|
+
<div class="icon-picker" id="modal-field-${f.key}">
|
|
7936
|
+
<input type="text" class="input icon-picker-search" placeholder="Search icons..." autocomplete="off" spellcheck="false">
|
|
7937
|
+
<div class="icon-picker-grid">${gridHtml}</div>
|
|
7938
|
+
</div>
|
|
7939
|
+
</div>`;
|
|
7940
|
+
return;
|
|
7941
|
+
}
|
|
7887
7942
|
if (f.type === 'checkbox') {
|
|
7888
7943
|
const checked = f.value ? 'checked' : '';
|
|
7889
7944
|
bodyHtml += `
|
|
@@ -7939,6 +7994,44 @@ class CWMApp {
|
|
|
7939
7994
|
});
|
|
7940
7995
|
});
|
|
7941
7996
|
|
|
7997
|
+
// Icon picker behavior
|
|
7998
|
+
const iconPickers = this.els.modalBody.querySelectorAll('.icon-picker');
|
|
7999
|
+
iconPickers.forEach(picker => {
|
|
8000
|
+
const grid = picker.querySelector('.icon-picker-grid');
|
|
8001
|
+
const search = picker.querySelector('.icon-picker-search');
|
|
8002
|
+
// Selection
|
|
8003
|
+
grid.addEventListener('click', (e) => {
|
|
8004
|
+
const swatch = e.target.closest('.icon-swatch');
|
|
8005
|
+
if (!swatch) return;
|
|
8006
|
+
grid.querySelectorAll('.icon-swatch').forEach(s => s.classList.remove('selected'));
|
|
8007
|
+
swatch.classList.add('selected');
|
|
8008
|
+
});
|
|
8009
|
+
// Search filter
|
|
8010
|
+
if (search) {
|
|
8011
|
+
search.addEventListener('input', () => {
|
|
8012
|
+
const q = search.value.toLowerCase().trim();
|
|
8013
|
+
grid.querySelectorAll('.icon-swatch').forEach(swatch => {
|
|
8014
|
+
if (!swatch.dataset.icon) return; // "none" always visible
|
|
8015
|
+
// Match against stored name (mi/folder → "folder" for search)
|
|
8016
|
+
const searchable = swatch.dataset.icon.replace(/^mi\//, '').replace(/_/g, ' ');
|
|
8017
|
+
swatch.hidden = !!q && !searchable.includes(q);
|
|
8018
|
+
});
|
|
8019
|
+
// Hide category separators when their section is empty
|
|
8020
|
+
grid.querySelectorAll('.icon-picker-cat-sep, .icon-picker-set-sep').forEach(sep => {
|
|
8021
|
+
if (!q) { sep.hidden = false; return; }
|
|
8022
|
+
let next = sep.nextElementSibling;
|
|
8023
|
+
let hasVisible = false;
|
|
8024
|
+
while (next && !next.classList.contains('icon-picker-cat-sep') && !next.classList.contains('icon-picker-set-sep')) {
|
|
8025
|
+
if (!next.hidden) { hasVisible = true; break; }
|
|
8026
|
+
next = next.nextElementSibling;
|
|
8027
|
+
}
|
|
8028
|
+
sep.hidden = !hasVisible;
|
|
8029
|
+
});
|
|
8030
|
+
});
|
|
8031
|
+
search.addEventListener('keydown', e => e.stopPropagation());
|
|
8032
|
+
}
|
|
8033
|
+
});
|
|
8034
|
+
|
|
7942
8035
|
// Re-enable confirm button (may have been disabled by previous modal interaction)
|
|
7943
8036
|
this.els.modalConfirmBtn.disabled = false;
|
|
7944
8037
|
|
|
@@ -7951,6 +8044,9 @@ class CWMApp {
|
|
|
7951
8044
|
if (f.type === 'color') {
|
|
7952
8045
|
const selected = this.els.modalBody.querySelector(`#modal-field-${f.key} .color-swatch.selected`);
|
|
7953
8046
|
result[f.key] = selected ? selected.dataset.color : 'mauve';
|
|
8047
|
+
} else if (f.type === 'icon') {
|
|
8048
|
+
const selected = this.els.modalBody.querySelector(`#modal-field-${f.key} .icon-swatch.selected`);
|
|
8049
|
+
result[f.key] = (selected && selected.dataset.icon) ? selected.dataset.icon : null;
|
|
7954
8050
|
} else if (f.type === 'checkbox') {
|
|
7955
8051
|
const el = document.getElementById(`modal-field-${f.key}`);
|
|
7956
8052
|
if (el) result[f.key] = el.checked;
|
|
@@ -8547,12 +8643,13 @@ class CWMApp {
|
|
|
8547
8643
|
: '';
|
|
8548
8644
|
|
|
8549
8645
|
// Build meta row (badges + size + time) — only if there's something to show
|
|
8550
|
-
const metaParts = [badges, sizeStr ? `<span class="ws-session-size">${sizeStr}</span>` : ''
|
|
8646
|
+
const metaParts = [badges, sizeStr ? `<span class="ws-session-size">${sizeStr}</span>` : ''].filter(Boolean).join('');
|
|
8551
8647
|
const metaRow = metaParts ? `<div class="ws-session-meta-row">${metaParts}</div>` : '';
|
|
8648
|
+
const timeEl = timeStr ? `<span class="ws-session-time">${timeStr}</span>` : '';
|
|
8552
8649
|
|
|
8553
8650
|
return `<div class="ws-session-item${isHidden ? ' ws-session-hidden' : ''}" data-session-id="${s.id}" draggable="true" title="${this.escapeHtml(s.workingDir || '')}">
|
|
8554
8651
|
<span class="ws-session-dot${tristateAttr}" style="background: ${statusDot}"></span>${pip}
|
|
8555
|
-
<span class="ws-session-name">${this.escapeHtml(name)}</span
|
|
8652
|
+
<span class="ws-session-name">${this.escapeHtml(name)}</span>${timeEl}
|
|
8556
8653
|
${metaRow}
|
|
8557
8654
|
</div>`;
|
|
8558
8655
|
};
|
|
@@ -8595,25 +8692,24 @@ class CWMApp {
|
|
|
8595
8692
|
|
|
8596
8693
|
return `
|
|
8597
8694
|
<div class="workspace-accordion${isWsHidden ? ' hidden-item' : ''}" data-id="${ws.id}">
|
|
8598
|
-
<div class="workspace-item${isActive ? ' active' : ''}" data-id="${ws.id}" draggable="true">
|
|
8695
|
+
<div class="workspace-item${isActive ? ' active' : ''}" data-id="${ws.id}" draggable="true" style="--ws-color: ${color}">
|
|
8599
8696
|
<span class="ws-chevron${showBody ? ' open' : ''}">▶</span>
|
|
8600
|
-
|
|
8697
|
+
${(() => {
|
|
8698
|
+
const iconSvg = ws.icon
|
|
8699
|
+
? (ws.icon.startsWith('mi/')
|
|
8700
|
+
? window.__materialIcons?.[ws.icon.slice(3)]
|
|
8701
|
+
: window.__lucideIcons?.[ws.icon])
|
|
8702
|
+
: null;
|
|
8703
|
+
return iconSvg
|
|
8704
|
+
? `<span class="workspace-icon" style="color: ${color}">${iconSvg}</span>`
|
|
8705
|
+
: `<div class="workspace-color-dot" style="background: ${color}"></div>`;
|
|
8706
|
+
})()}
|
|
8601
8707
|
<div class="workspace-info">
|
|
8602
|
-
<div class="workspace-name">${this.escapeHtml(ws.name)}</div>
|
|
8603
|
-
<div class="workspace-session-count">${sessionCount} session${sessionCount !== 1 ? 's' : ''}</div>
|
|
8708
|
+
<div class="workspace-name">${this.escapeHtml(ws.name)}<span class="ws-count-badge">${sessionCount}</span></div>
|
|
8604
8709
|
</div>
|
|
8605
8710
|
<div class="workspace-actions">
|
|
8606
8711
|
${this.state.settings.enableWorktreeTasks ? `<button class="btn btn-ghost btn-icon btn-sm ws-new-task-btn" data-ws-id="${ws.id}" title="New Task">+</button>` : ''}
|
|
8607
|
-
<button class="btn btn-ghost btn-icon btn-sm ws-
|
|
8608
|
-
<svg width="12" height="12" viewBox="0 0 14 14" fill="none">
|
|
8609
|
-
<path d="M8.5 2.5l3 3M2 9.5V12h2.5L11 5.5l-3-3L2 9.5z" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
8610
|
-
</svg>
|
|
8611
|
-
</button>
|
|
8612
|
-
<button class="btn btn-ghost btn-icon btn-sm btn-danger-hover ws-delete-btn" data-id="${ws.id}" title="Delete">
|
|
8613
|
-
<svg width="12" height="12" viewBox="0 0 14 14" fill="none">
|
|
8614
|
-
<path d="M2.5 4h9M5 4V2.5h4V4M3.5 4v7.5a1 1 0 001 1h5a1 1 0 001-1V4" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
8615
|
-
</svg>
|
|
8616
|
-
</button>
|
|
8712
|
+
<button class="btn btn-ghost btn-icon btn-sm ws-more-btn" data-id="${ws.id}" title="More actions">…</button>
|
|
8617
8713
|
</div>
|
|
8618
8714
|
</div>
|
|
8619
8715
|
<div class="workspace-accordion-body"${showBody ? '' : ' hidden'}>
|
|
@@ -1656,6 +1656,8 @@
|
|
|
1656
1656
|
<!-- Hidden file input for image uploads (reused by all terminal panes) -->
|
|
1657
1657
|
<input type="file" id="image-upload-input" accept="image/*" hidden>
|
|
1658
1658
|
|
|
1659
|
+
<script src="vendor/lucide.bundle.js"></script>
|
|
1660
|
+
<script src="vendor/material-icons.bundle.js"></script>
|
|
1659
1661
|
<script src="vendor/qrcode.min.js"></script>
|
|
1660
1662
|
<script src="vendor/xterm/xterm.min.js"></script>
|
|
1661
1663
|
<script src="vendor/xterm-addon-fit/xterm-addon-fit.min.js"></script>
|