From 909978eb1045323f8485fd8d6821c1d0f9b64172 Mon Sep 17 00:00:00 2001 From: Hassan Al-Najjar Date: Fri, 11 Sep 2026 21:23:26 +0300 Subject: [PATCH 1/4] Build Helper: multi-root + pinned projects, recursive scan, reveal & suggest APIs Co-Authored-By: Claude Opus 4.8 (1M context) --- build-helper/lib/dashboard/data.js | 17 +++++++------- build-helper/lib/dashboard/share.js | 6 ++--- build-helper/lib/project/scan.js | 35 +++++++++++++++++++++++------ build-helper/lib/routes/apps.js | 11 ++++++++- build-helper/lib/routes/setup.js | 24 ++++++++++++++++++-- build-helper/lib/state.js | 27 ++++++++++++++++++---- 6 files changed, 95 insertions(+), 25 deletions(-) diff --git a/build-helper/lib/dashboard/data.js b/build-helper/lib/dashboard/data.js index a0d04c3..6ac70db 100644 --- a/build-helper/lib/dashboard/data.js +++ b/build-helper/lib/dashboard/data.js @@ -3,8 +3,8 @@ // artifact disk usage. The client computes KPIs/series/filters locally from `builds`. const fs = require('fs'); const path = require('path'); -const { ARTIFACTS_DIR, BUILDS_JSON, readJson, readConfig, projectsRoot, running } = require('../state'); -const { scanProjects, appHealth } = require('../project'); +const { ARTIFACTS_DIR, BUILDS_JSON, readJson, readConfig, projectsRoot, projectSources, running } = require('../state'); +const { scanAll, appHealth } = require('../project'); // Total + per-project artifact bytes under artifacts/. function storageData() { @@ -21,15 +21,16 @@ function storageData() { // The per-app scan is the expensive part; cache it briefly so nav-backs and feed refreshes reuse it. let _dashCache = { key: '', t: 0, projects: null }; -function dashboardData(root) { - const rt = root || projectsRoot(); +function dashboardData() { + const sources = projectSources(); + const key = JSON.stringify(sources); const all = readJson(BUILDS_JSON, []); const apps = readConfig().apps || {}; const now = Date.now(); let projects; - if (_dashCache.projects && _dashCache.key === rt && now - _dashCache.t < 4000) { projects = _dashCache.projects; } + if (_dashCache.projects && _dashCache.key === key && now - _dashCache.t < 4000) { projects = _dashCache.projects; } else { - projects = scanProjects(rt).map((p) => { + projects = scanAll(sources).map((p) => { const pb = all.filter((b) => b.path === p.path); const last = pb[0] || null; const h = appHealth(p.path, { needsConfig: p.needsConfig, firebase: p.firebase }); @@ -43,9 +44,9 @@ function dashboardData(root) { || ((a.order == null ? 1e9 : a.order) - (b.order == null ? 1e9 : b.order)) || ((b.lastBuild ? new Date(b.lastBuild.time) : 0) - (a.lastBuild ? new Date(a.lastBuild.time) : 0)) || a.name.localeCompare(b.name)); - _dashCache = { key: rt, t: now, projects }; + _dashCache = { key, t: now, projects }; } - return { builds: all.slice(0, 500), projects, storage: storageData(), running: { busy: running.busy, info: running.info }, root: projectsRoot() }; + return { builds: all.slice(0, 500), projects, storage: storageData(), running: { busy: running.busy, info: running.info }, root: projectsRoot(), sources }; } function cleanupArtifacts(body) { diff --git a/build-helper/lib/dashboard/share.js b/build-helper/lib/dashboard/share.js index 9373b05..8cf9867 100644 --- a/build-helper/lib/dashboard/share.js +++ b/build-helper/lib/dashboard/share.js @@ -3,9 +3,9 @@ // upload a generated card for a build. Single-user + local rclone (no team log to attach to). const fs = require('fs'); const path = require('path'); -const { ROOT, RCLONE, ARTIFACTS_DIR, readConfig, projectsRoot, sendJson } = require('../state'); +const { ROOT, RCLONE, ARTIFACTS_DIR, readConfig, projectSources, sendJson } = require('../state'); const { run, captureCmd, rc } = require('../shell'); -const { scanProjects, detectApp } = require('../project'); +const { scanAll, detectApp } = require('../project'); async function handleShareImage(req, res, body) { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' }); @@ -36,7 +36,7 @@ async function handleMkdirs(req, res) { const cfg = readConfig(); const base = `${cfg.onedrive.remote}:${cfg.onedrive.base}`; const mk = async (p) => { try { await run(RCLONE, rc(['mkdir', p]), ROOT, () => {}); log('✓ ' + p); } catch (e) { log('✖ ' + p + ' — ' + (e.message || '').split('\n')[0]); } }; - const projects = scanProjects(projectsRoot()); + const projects = scanAll(projectSources()); log(`Creating folders for ${projects.length} project(s)…`); for (const pr of projects) { const a = detectApp(pr.path); const name = a.repo || a.name; diff --git a/build-helper/lib/project/scan.js b/build-helper/lib/project/scan.js index ea7aceb..d784f45 100644 --- a/build-helper/lib/project/scan.js +++ b/build-helper/lib/project/scan.js @@ -16,18 +16,39 @@ function findAppIcon(projectPath) { return null; } -function scanProjects(root) { +// Dirs that never contain a sibling app — pruned so recursive scans stay fast. +const PRUNE = new Set(['node_modules', '.dart_tool', 'build', '.git', 'ios', 'android', 'macos', 'windows', 'linux', 'web', '.symlinks', '.fvm', '.idea', 'Pods']); +const isFlutter = (p) => fs.existsSync(path.join(p, 'pubspec.yaml')) && fs.existsSync(path.join(p, 'lib')); + +// One directory → the dashboard card projection, or null if it isn't a Flutter app. +function detectCard(p) { + if (!isFlutter(p)) return null; + const a = detectApp(p); + return { name: a.name, path: a.path, version: a.version, currentMode: a.currentMode, modes: Object.keys(a.modes), needsConfig: a.needsConfig, firebase: !!a.firebase, hasIcon: !!findAppIcon(p) }; +} + +// Scan a root for Flutter apps. recursive → descend up to `depth` levels (a Flutter dir is a leaf). +function scanProjects(root, recursive, depth = 3) { let ents; try { ents = fs.readdirSync(root, { withFileTypes: true }); } catch { return []; } const out = []; for (const e of ents) { - if (!e.isDirectory()) continue; + if (!e.isDirectory() || PRUNE.has(e.name) || e.name.startsWith('.')) continue; const p = path.join(root, e.name); - if (!fs.existsSync(path.join(p, 'pubspec.yaml'))) continue; - if (!fs.existsSync(path.join(p, 'lib'))) continue; - const a = detectApp(p); - out.push({ name: a.name, path: a.path, version: a.version, currentMode: a.currentMode, modes: Object.keys(a.modes), needsConfig: a.needsConfig, firebase: !!a.firebase, hasIcon: !!findAppIcon(p) }); + const card = detectCard(p); + if (card) out.push(card); + else if (recursive && depth > 1) out.push(...scanProjects(p, true, depth - 1)); } return out.sort((x, y) => x.name.localeCompare(y.name)); } -module.exports = { findAppIcon, scanProjects }; +// Every card across all scan roots + pinned project paths, deduped by absolute path. +function scanAll(sources) { + const { roots = [], pinned = [], recursive = false } = sources || {}; + const seen = new Set(); const out = []; + for (const c of [...roots.flatMap((r) => scanProjects(r, recursive)), ...pinned.map(detectCard)]) { + if (c && !seen.has(c.path)) { seen.add(c.path); out.push(c); } + } + return out; +} + +module.exports = { findAppIcon, scanProjects, detectCard, scanAll }; diff --git a/build-helper/lib/routes/apps.js b/build-helper/lib/routes/apps.js index 95279e0..fa56272 100644 --- a/build-helper/lib/routes/apps.js +++ b/build-helper/lib/routes/apps.js @@ -2,6 +2,7 @@ // Project + per-app routes: the dashboard payload, project list/detail, per-app config saves, // health, git metadata, changelog, and the app icon image. const fs = require('fs'); +const { spawn } = require('child_process'); const { BUILDS_JSON, readJson, readConfig, projectsRoot } = require('../state'); const { dashboardData, handleAppSave, handleFavorite, handleReorder } = require('../dashboard'); const { detectApp, scanProjects, gitBranches, gitLastCommit, gitChangelog, groupConventional, nextBuildNumber, appHealth, findAppIcon } = require('../project'); @@ -11,8 +12,16 @@ const { appleAuth, playAccount } = require('../signing-config'); function register(app) { const J = app.sendJson; - app.r('GET', '/api/dashboard', ({ res, q }) => J(res, 200, dashboardData(q.get('root')))); + app.r('GET', '/api/dashboard', ({ res }) => J(res, 200, dashboardData())); app.r('GET', '/api/projects', ({ res, q }) => { const root = q.get('root') || projectsRoot(); return J(res, 200, { root, projects: scanProjects(root) }); }); + // Open a project's folder in the OS file manager (Finder / Explorer / xdg). Args array — no shell. + app.r('POST', '/api/reveal', ({ res, body }) => { + const p = String((body || {}).path || '').trim(); + if (!p || !fs.existsSync(p)) return J(res, 400, { error: 'path not found' }); + const cmd = process.platform === 'darwin' ? 'open' : (process.platform === 'win32' ? 'explorer' : 'xdg-open'); + try { spawn(cmd, [p], { detached: true, stdio: 'ignore' }).unref(); } catch {} + return J(res, 200, { ok: true }); + }, { body: true }); app.r('GET', '/api/project', ({ res, q }) => { const p = q.get('path'); if (!p || !fs.existsSync(p)) return J(res, 404, { error: 'not found' }); diff --git a/build-helper/lib/routes/setup.js b/build-helper/lib/routes/setup.js index 29b3a09..0bbc961 100644 --- a/build-helper/lib/routes/setup.js +++ b/build-helper/lib/routes/setup.js @@ -3,8 +3,11 @@ // build-notification + email config, the two OAuth/login SSE flows (OneDrive, Firebase CLI), and the // environment preflight (doctor). Issue-tracker config is NOT re-wired here — the shared kit // connectors layer (/api/connectors*) already owns that store; setup.js handleSetupTracker is unused. +const os = require('os'); +const fs = require('fs'); +const path = require('path'); const { setupStatus, handleSetupPlay, handleSetupApple, handleSetupFirebaseApp, handleSetupFirebaseLogin, handleSetupNotify, handleSetupEmail } = require('../setup'); -const { setProjectsRoot } = require('../state'); +const { setSources, validateSource } = require('../state'); const { handleSetupRclone } = require('../onedrive'); const { doctorData, doctorInstallCmd } = require('../dashboard'); const { run } = require('../shell'); @@ -12,7 +15,17 @@ const { run } = require('../shell'); function register(app) { const J = app.sendJson; app.r('GET', '/api/setup', ({ res }) => J(res, 200, setupStatus())); - app.r('POST', '/api/setup/root', ({ res, body }) => J(res, 200, { ok: true, root: setProjectsRoot((body || {}).root) }), { body: true }); + // Persist project sources (roots + pinned paths + recursive). Validates each entry; returns the + // saved set plus any `invalid` ones the UI should flag (kept out of the persisted config). + app.r('POST', '/api/setup/sources', ({ res, body }) => { + const s = body || {}; const invalid = []; + const keep = (arr, pinned) => (arr || []).filter((p) => { const v = validateSource(p, pinned); if (!v.ok) invalid.push({ path: p, kind: pinned ? 'pinned' : 'root', reason: v.reason }); return v.ok; }); + const sources = setSources({ roots: keep(s.roots, false), pinned: keep(s.pinned, true), recursive: s.recursive }); + return J(res, 200, { ok: true, sources, invalid }); + }, { body: true }); + app.r('GET', '/api/setup/suggest-roots', ({ res }) => J(res, 200, { roots: suggestRoots() })); + // Back-compat single-root save (superseded by the sources manager UI). ponytail: drop once no caller. + app.r('POST', '/api/setup/root', ({ res, body }) => { const r = String((body || {}).root || '').trim(); const s = setSources({ roots: r ? [r] : [], pinned: [], recursive: false }); return J(res, 200, { ok: true, root: s.roots[0] || '' }); }, { body: true }); app.r('POST', '/api/setup/play', ({ res, body }) => handleSetupPlay(res, body), { body: true }); app.r('POST', '/api/setup/apple', ({ res, body }) => handleSetupApple(res, body), { body: true }); app.r('POST', '/api/setup/firebase-app', ({ res, body }) => handleSetupFirebaseApp(res, body), { body: true }); @@ -37,4 +50,11 @@ function installTool(res, body) { .catch((e) => { send('error', { message: e.message }); res.end(); }); } +// Common Flutter-project locations under $HOME that actually exist — one-click add suggestions. +function suggestRoots() { + const home = os.homedir(); + return ['Projects', 'dev', 'Development', 'StudioProjects', 'AndroidStudioProjects', 'mobileApps', 'code', 'src'] + .map((d) => path.join(home, d)).filter((p) => { try { return fs.statSync(p).isDirectory(); } catch { return false; } }); +} + module.exports = { register }; diff --git a/build-helper/lib/state.js b/build-helper/lib/state.js index 4c92790..d040afd 100644 --- a/build-helper/lib/state.js +++ b/build-helper/lib/state.js @@ -33,9 +33,28 @@ function readConfig() { return c; } function writeConfig(c) { writeJson(CONFIG_JSON, c); } -function projectsRoot() { return readConfig().root || process.env.FLUTTER_PROJECTS || path.join(os.homedir(), 'mobileApps'); } -// Persist (or clear) the scanned projects root. Blank → fall back to FLUTTER_PROJECTS / ~/mobileApps. -function setProjectsRoot(root) { const c = readConfig(); const v = (root || '').trim(); if (v) c.root = v; else delete c.root; writeConfig(c); return projectsRoot(); } +// Where Build Helper looks for apps: N scan roots + individually pinned project paths, plus a +// recursive flag. Migrates the legacy single `root` (or FLUTTER_PROJECTS / ~/mobileApps) into roots[]. +function projectSources() { + const c = readConfig(); + const roots = Array.isArray(c.roots) ? c.roots : [c.root || process.env.FLUTTER_PROJECTS || path.join(os.homedir(), 'mobileApps')]; + return { roots, pinned: Array.isArray(c.pinned) ? c.pinned : [], recursive: !!c.recursive }; +} +function projectsRoot() { return projectSources().roots[0] || path.join(os.homedir(), 'mobileApps'); } // back-compat single root +const _uniq = (a) => [...new Set((a || []).map((x) => String(x || '').trim()).filter(Boolean))]; +function setSources(s) { + const c = readConfig(); + c.roots = _uniq(s.roots); c.pinned = _uniq(s.pinned); c.recursive = !!s.recursive; delete c.root; // migrated + writeConfig(c); return projectSources(); +} +// A source is usable if it exists; a pinned path must also look like a Flutter project (pubspec + lib). +function validateSource(p, isPinned) { + const v = String(p || '').trim(); + if (!v) return { ok: false, reason: 'empty' }; + if (!fs.existsSync(v)) return { ok: false, reason: 'not found' }; + if (isPinned && !(fs.existsSync(path.join(v, 'pubspec.yaml')) && fs.existsSync(path.join(v, 'lib')))) return { ok: false, reason: 'not a Flutter project' }; + return { ok: true }; +} // ---------- running-build tracker (for Stop + one-at-a-time lock) ---------- const running = { children: new Set(), aborted: false, busy: false, info: null, log: [], seq: 0 }; @@ -62,6 +81,6 @@ function sendJson(res, code, obj) { module.exports = { ID, DATA, ROOT, PORT, FLUTTER, RCLONE, ONEDRIVE_REMOTE, ONEDRIVE_BASE, CONFIG_JSON, BUILDS_JSON, CREDS_DIR, ARTIFACTS_DIR, LOGS_DIR, CHANGELOGS_DIR, LOGICAL_ENVS, - readJson, writeJson, esc, readConfig, writeConfig, projectsRoot, setProjectsRoot, + readJson, writeJson, esc, readConfig, writeConfig, projectsRoot, projectSources, setSources, validateSource, running, stopRunning, feedClients, feedHistory, broadcast, sendJson, }; From 211480542b5a9ef49f7a74db41fb7d26b1150148 Mon Sep 17 00:00:00 2001 From: Hassan Al-Najjar Date: Fri, 11 Sep 2026 21:27:33 +0300 Subject: [PATCH 2/4] Build Helper UI: project sources manager (roots + pinned + recursive + suggestions + reveal) Co-Authored-By: Claude Opus 4.8 (1M context) --- build-helper/public/js/api.js | 4 ++- build-helper/public/js/components.js | 32 ++++++++++++++++++++ build-helper/public/js/state.js | 1 + build-helper/public/js/views.js | 45 +++++++++++++++++++--------- 4 files changed, 67 insertions(+), 15 deletions(-) diff --git a/build-helper/public/js/api.js b/build-helper/public/js/api.js index ea2d92e..0dd5343 100644 --- a/build-helper/public/js/api.js +++ b/build-helper/public/js/api.js @@ -16,7 +16,9 @@ window.API = { signing: (path) => _get('/api/signing/status?path=' + encodeURIComponent(path)), changelog: (path, env) => _get('/api/changelog?' + _q({ path, env, grouped: 1 })), - saveRoot: (root) => postJson('/api/setup/root', { root }), // persist the projects root + saveSources: (sources) => postJson('/api/setup/sources', sources), // { roots, pinned, recursive } + suggestRoots: () => _get('/api/setup/suggest-roots').then((r) => r.roots || []).catch(() => []), + reveal: (path) => postJson('/api/reveal', { path }), // open a folder in Finder post: (url, body) => postJson(url, body), // kit.js stream: (url, body, box, done) => streamSSE(url, body, box, done), // kit.js stopBuild: () => postJson('/api/build/stop', {}), diff --git a/build-helper/public/js/components.js b/build-helper/public/js/components.js index fb85d54..3aec5e2 100644 --- a/build-helper/public/js/components.js +++ b/build-helper/public/js/components.js @@ -42,6 +42,7 @@ window.C = { ${C.dot(health)} ${esc(p.name)} ${p.favorite ? '★' : ''} + ${ICON.folder} v${esc(verName(p.version))} ${C.envChips(p)} @@ -50,6 +51,37 @@ window.C = { `; }, + // The project-sources manager: scan roots + pinned projects + recursive toggle + suggestions. + sourcesPanel(BH, suggest) { + const s = BH.sources || { roots: [], pinned: [], recursive: false }; + const row = (val, kind) => `
+ ${esc(val)} + + +
`; + const list = (arr, kind, empty) => arr.length ? arr.map((v) => row(v, kind)).join('') : `
${empty}
`; + const sugg = (suggest || []).filter((p) => !s.roots.includes(p)); + return `
+
+
Project sources
+ +
+
+
+
Scan roots
+ ${list(s.roots, 'root', 'No roots — add one below.')} +
+ ${sugg.length ? `
${sugg.map((p) => ``).join('')}
` : ''} +
+
+
Pinned projects
+ ${list(s.pinned, 'pinned', 'No pinned projects.')} +
+
+
+
`; + }, + // One artifact toggle chip for the build form. artChip(a, on) { return ` + ${BH.projects.length} app${BH.projects.length === 1 ? '' : 's'} ${grid}`; - const setRoot = async () => { - const r = await API.saveRoot(el('#bh-root').value).catch(() => null); - if (r && r.root) { BH.root = r.root; toast('Projects root set', 'ok'); V.dashboard(); } - else toast('Could not set path', 'err'); - }; - el('#bh-setroot').onclick = setRoot; - el('#bh-root').onkeydown = (e) => { if (e.key === 'Enter') setRoot(); }; + wireSources(); el('#bh-refresh').onclick = () => V.dashboard(); _body().querySelectorAll('[data-open]').forEach((b) => b.onclick = () => V.project(b.dataset.open)); }; +// Wire the sources panel: add/remove roots & pinned, recursive toggle, suggestions, reveal. +function wireSources() { + const save = async () => { + const r = await API.saveSources(BH.sources).catch(() => null); + if (!r) return toast('Could not save sources', 'err'); + BH.sources = r.sources; (r.invalid || []).forEach((i) => toast(`Skipped ${i.path} — ${i.reason}`, 'err')); + V.dashboard(); + }; + const add = (inputId, key) => { const v = el('#' + inputId).value.trim(); if (v) { BH.sources[key] = [...BH.sources[key], v]; save(); } }; + el('#bh-addroot').onclick = () => add('bh-add-root', 'roots'); + el('#bh-add-root').onkeydown = (e) => { if (e.key === 'Enter') add('bh-add-root', 'roots'); }; + el('#bh-addpin').onclick = () => add('bh-add-pin', 'pinned'); + el('#bh-add-pin').onkeydown = (e) => { if (e.key === 'Enter') add('bh-add-pin', 'pinned'); }; + el('#bh-recursive').onchange = (e) => { BH.sources.recursive = e.target.checked; save(); }; + _body().querySelectorAll('[data-rm]').forEach((b) => b.onclick = () => { const key = b.dataset.kind === 'pinned' ? 'pinned' : 'roots'; BH.sources[key] = BH.sources[key].filter((x) => x !== b.dataset.rm); save(); }); + _body().querySelectorAll('[data-suggest]').forEach((b) => b.onclick = () => { BH.sources.roots = [...new Set([...BH.sources.roots, b.dataset.suggest])]; save(); }); + _body().querySelectorAll('[data-reveal]').forEach((b) => b.onclick = (e) => { e.stopPropagation(); API.reveal(b.dataset.reveal); }); +} + window.V.reports = async function () { BH.view = 'reports'; if (window.setShell) setShell('reports'); - const b = BH.builds.length ? BH.builds : (await API.dashboard(BH.root).catch(() => ({ builds: [] }))).builds || []; + const b = BH.builds.length ? BH.builds : (await API.dashboard().catch(() => ({ builds: [] }))).builds || []; BH.builds = b; const envs = [...new Set(b.map((x) => x.env).filter(Boolean))]; _body().innerHTML = ` From b62f23dcee7f5ee276215f9db55b64593cd8cb3c Mon Sep 17 00:00:00 2001 From: Hassan Al-Najjar Date: Fri, 11 Sep 2026 21:28:58 +0300 Subject: [PATCH 3/4] Build Helper UI: project search/filter + reveal wiring Co-Authored-By: Claude Opus 4.8 (1M context) --- build-helper/public/js/views.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/build-helper/public/js/views.js b/build-helper/public/js/views.js index 51b63e9..a69e87c 100644 --- a/build-helper/public/js/views.js +++ b/build-helper/public/js/views.js @@ -15,23 +15,34 @@ window.V.dashboard = async function () { ]); BH.projects = d.projects || []; BH.builds = d.builds || []; BH.storage = d.storage || {}; BH.running = d.running || {}; BH.sources = d.sources || { roots: [], pinned: [], recursive: false }; - const grid = BH.projects.length - ? `
${BH.projects.map(C.projectCard).join('')}
` - : `
No Flutter apps found. Add a scan root or pin a project in Project sources above.
`; _body().innerHTML = ` ${C.kpis(d)} ${C.sourcesPanel(BH, suggest)}
Projects
- ${BH.projects.length} app${BH.projects.length === 1 ? '' : 's'} + +
- ${grid}`; +
`; wireSources(); + renderGrid(''); + el('#bh-search').oninput = (e) => renderGrid(e.target.value); el('#bh-refresh').onclick = () => V.dashboard(); - _body().querySelectorAll('[data-open]').forEach((b) => b.onclick = () => V.project(b.dataset.open)); }; +// Filter the project grid client-side by name/path; (re)wire card open + reveal after each render. +function renderGrid(q) { + const s = (q || '').trim().toLowerCase(); + const list = s ? BH.projects.filter((p) => (p.name + ' ' + p.path).toLowerCase().includes(s)) : BH.projects; + el('#bh-count').textContent = s ? `${list.length} of ${BH.projects.length}` : `${BH.projects.length} app${BH.projects.length === 1 ? '' : 's'}`; + el('#bh-grid').innerHTML = list.length + ? `
${list.map(C.projectCard).join('')}
` + : `
${BH.projects.length ? 'No projects match your search.' : 'No Flutter apps found. Add a scan root or pin a project in Project sources above.'}
`; + el('#bh-grid').querySelectorAll('[data-open]').forEach((b) => b.onclick = () => V.project(b.dataset.open)); + el('#bh-grid').querySelectorAll('[data-reveal]').forEach((b) => b.onclick = (e) => { e.stopPropagation(); API.reveal(b.dataset.reveal); }); +} + // Wire the sources panel: add/remove roots & pinned, recursive toggle, suggestions, reveal. function wireSources() { const save = async () => { From 6d916b152d2306eb0b448ed9a51836d024033374 Mon Sep 17 00:00:00 2001 From: Hassan Al-Najjar Date: Fri, 11 Sep 2026 21:39:33 +0300 Subject: [PATCH 4/4] Launchpad: fix invisible filenames in preview tree (flex row: fixed prefix + truncating name) Co-Authored-By: Claude Opus 4.8 (1M context) --- flutter-launchpad/public/index.html | 7 ++++--- flutter-launchpad/public/js/output.js | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/flutter-launchpad/public/index.html b/flutter-launchpad/public/index.html index 8b35dc8..e76b635 100644 --- a/flutter-launchpad/public/index.html +++ b/flutter-launchpad/public/index.html @@ -23,13 +23,14 @@ .fl-cond{font-size:11px;color:var(--ink2);margin-top:5px;padding:6px 9px;border-left:2px solid var(--brand);background:var(--sunken);border-radius:0 8px 8px 0} .fl-tab{padding:6px 12px;font-size:12px;border-radius:8px;color:var(--ink2);cursor:pointer} .fl-tab.on{background:var(--raised);color:var(--ink);box-shadow:inset 0 0 0 1px var(--edge2)} - .fl-file{font-size:12px;padding:3px 8px;border-radius:7px;cursor:pointer;color:var(--ink2);font-family:'JetBrains Mono',monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} + .fl-file{font-size:12px;padding:3px 8px;border-radius:7px;cursor:pointer;color:var(--ink2);font-family:'JetBrains Mono',monospace;display:flex;align-items:center;min-width:0} .fl-file:hover{background:var(--hover);color:var(--ink)} .fl-file.on{background:var(--raised);color:var(--ink)} .fl-file.dir{cursor:default;color:var(--ink2)} .fl-file.dir:hover{background:transparent;color:var(--ink2)} - .tree-prefix{color:var(--muted);white-space:pre} - .fl-scroll{max-height:64vh;overflow:auto} + .tree-prefix{color:var(--muted);white-space:pre;flex:none} + .fl-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} + .fl-scroll{max-height:64vh;overflow-y:auto;overflow-x:hidden} .fl-updating{opacity:.6;transition:opacity .15s} .fl-sw{width:38px;height:22px;border-radius:999px;background:var(--edge2);position:relative;cursor:pointer;transition:.15s;flex:none} .fl-sw.on{background:var(--brand)} diff --git a/flutter-launchpad/public/js/output.js b/flutter-launchpad/public/js/output.js index c617675..e9b7639 100644 --- a/flutter-launchpad/public/js/output.js +++ b/flutter-launchpad/public/js/output.js @@ -68,7 +68,7 @@ window.FL_OUTPUT = {
{{r.prefix}}{{r.name}}
+ :title="r.path||r.name" @click="r.path && (sel=r.path)">{{r.prefix}}{{r.name}}