Files
Trailhead/src/App.jsx
T
David c860959631 Min width = 1 column, add header minimize, Claude → PowerShell
- Width: drop the header-fits-too floor; popup width is now
  max(COLUMN_WIDTH, columns-required, tabs-required). The brand title in
  the header is allowed to ellipsize at the 216px minimum (full name
  preserved in tray hover and h1 title attr).
- Minimize: new -/▢ button in the header next to pin. Toggling hides the
  tab strip + body, locks width at COLUMN_WIDTH, and lets the existing
  ResizeObserver collapse height to just the header. State persists in
  config.minimized so reopening the popup remembers it. Add-column +
  button is hidden while minimized.
- Claude launcher: now spawns powershell.exe -NoExit -Command claude in
  the wt new-tab (was cmd.exe /K claude). Cmd-fallback updated to also
  start powershell directly when the trailing args target it.
2026-04-29 11:05:10 -04:00

244 lines
9.0 KiB
React

import React, { useEffect, useState, useCallback, useMemo } from 'react';
import Popup from './components/Popup.jsx';
export default function App() {
const [tabs, setTabs] = useState([]);
const [activeTabId, setActiveTabId] = useState(null);
const [columns, setColumns] = useState([]);
const [bookmarks, setBookmarks] = useState([]);
const [pinned, setPinned] = useState(false);
const [minimized, setMinimized] = useState(false);
const [loading, setLoading] = useState(true);
const [claudeAvailable, setClaudeAvailable] = useState(true);
const [buttonVisibility, setButtonVisibility] = useState({ claude: true, terminal: true, redeploy: true });
const [autoStart, setAutoStart] = useState(true);
// Bumped on each popup show so BookmarkRow re-runs its folder inspect.
// Rows stay mounted across hide/show cycles, so without a revision tick
// they'd never notice newly-added .sln or 1ReDeploy.bat files.
const [inspectRevision, setInspectRevision] = useState(0);
const loadConfig = useCallback(async () => {
const [cfg, hasClaude] = await Promise.all([
window.bookmarks.getConfig(),
window.bookmarks.isClaudeAvailable(),
]);
setTabs(Array.isArray(cfg.tabs) ? cfg.tabs : []);
setActiveTabId(cfg.activeTabId || null);
setColumns(Array.isArray(cfg.columns) ? cfg.columns : []);
setBookmarks(Array.isArray(cfg.bookmarks) ? cfg.bookmarks : []);
setPinned(!!cfg.pinned);
setMinimized(!!cfg.minimized);
setClaudeAvailable(!!hasClaude);
if (cfg.buttonVisibility) {
setButtonVisibility({
claude: cfg.buttonVisibility.claude !== false,
terminal: cfg.buttonVisibility.terminal !== false,
redeploy: cfg.buttonVisibility.redeploy !== false,
});
}
setAutoStart(cfg.autoStart !== false);
setLoading(false);
}, []);
useEffect(() => {
loadConfig();
const offConfig = window.bookmarks.onConfigUpdated((payload) => {
if (!payload) return;
if (Array.isArray(payload.tabs)) setTabs(payload.tabs);
if (Array.isArray(payload.columns)) setColumns(payload.columns);
if (Array.isArray(payload.bookmarks)) setBookmarks(payload.bookmarks);
if (typeof payload.activeTabId === 'string' || payload.activeTabId === null) {
setActiveTabId(payload.activeTabId);
}
if (payload.buttonVisibility) {
setButtonVisibility({
claude: payload.buttonVisibility.claude !== false,
terminal: payload.buttonVisibility.terminal !== false,
redeploy: payload.buttonVisibility.redeploy !== false,
});
}
if (typeof payload.autoStart === 'boolean') setAutoStart(payload.autoStart);
if (typeof payload.minimized === 'boolean') setMinimized(payload.minimized);
});
const offShown = window.bookmarks.onPopupShown(() => {
setInspectRevision((r) => r + 1);
});
return () => { offConfig(); offShown(); };
}, [loadConfig]);
const handleAdd = useCallback(async ({ alias, path, color, columnId }) => {
const next = await window.bookmarks.addBookmark({ alias, path, color, columnId });
setBookmarks(next || []);
}, []);
const handleEdit = useCallback(async ({ id, alias, path, color, columnId, hideDeploy }) => {
const next = await window.bookmarks.addBookmark({ id, alias, path, color, columnId, hideDeploy });
setBookmarks(next || []);
}, []);
const handleRemove = useCallback(async (id) => {
const next = await window.bookmarks.removeBookmark(id);
setBookmarks(next || []);
}, []);
const handleMoveBookmark = useCallback(async (id, delta) => {
const target = bookmarks.find((b) => b.id === id);
if (!target) return;
const colMates = bookmarks.filter((b) => b.columnId === target.columnId);
const idx = colMates.findIndex((b) => b.id === id);
const swapIdx = idx + delta;
if (idx < 0 || swapIdx < 0 || swapIdx >= colMates.length) return;
const aId = colMates[idx].id;
const bId = colMates[swapIdx].id;
const newIds = bookmarks.map((b) => {
if (b.id === aId) return bId;
if (b.id === bId) return aId;
return b.id;
});
const next = await window.bookmarks.reorderBookmarks(newIds);
setBookmarks(next || []);
}, [bookmarks]);
const handleAddColumn = useCallback(async (name) => {
const next = await window.bookmarks.addColumn(name, activeTabId);
setColumns(next || []);
}, [activeTabId]);
const handleRenameColumn = useCallback(async (id, name) => {
const next = await window.bookmarks.renameColumn(id, name);
setColumns(next || []);
}, []);
const handleRemoveColumn = useCallback(async (id, reassignTo) => {
const res = await window.bookmarks.removeColumn(id, reassignTo);
if (res && Array.isArray(res.columns)) setColumns(res.columns);
if (res && Array.isArray(res.bookmarks)) setBookmarks(res.bookmarks);
}, []);
const handleReorderColumns = useCallback(async (ids) => {
const next = await window.bookmarks.reorderColumns(ids);
setColumns(next || []);
}, []);
const handleMoveColumnToTab = useCallback(async (columnId, tabId) => {
const res = await window.bookmarks.moveColumnToTab(columnId, tabId);
if (res && Array.isArray(res.columns)) setColumns(res.columns);
if (res && Array.isArray(res.tabs)) setTabs(res.tabs);
}, []);
const handleAddTab = useCallback(async (name) => {
const res = await window.bookmarks.addTab(name);
if (res && Array.isArray(res.tabs)) setTabs(res.tabs);
if (res && res.activeTabId) setActiveTabId(res.activeTabId);
}, []);
const handleRenameTab = useCallback(async (id, name) => {
const next = await window.bookmarks.renameTab(id, name);
setTabs(next || []);
}, []);
const handleRemoveTab = useCallback(async (id, reassignTo) => {
const res = await window.bookmarks.removeTab(id, reassignTo);
if (res && Array.isArray(res.tabs)) setTabs(res.tabs);
if (res && Array.isArray(res.columns)) setColumns(res.columns);
if (res && Array.isArray(res.bookmarks)) setBookmarks(res.bookmarks);
if (res && (typeof res.activeTabId === 'string' || res.activeTabId === null)) {
setActiveTabId(res.activeTabId);
}
}, []);
const handleReorderTabs = useCallback(async (ids) => {
const next = await window.bookmarks.reorderTabs(ids);
setTabs(next || []);
}, []);
const handleSelectTab = useCallback(async (id) => {
setActiveTabId(id);
await window.bookmarks.setActiveTab(id);
}, []);
const handleTogglePin = useCallback(async () => {
const next = await window.bookmarks.setPinned(!pinned);
setPinned(!!next);
}, [pinned]);
const handleToggleMinimized = useCallback(async () => {
setMinimized((prev) => !prev);
const next = await window.bookmarks.setMinimized(!minimized);
if (typeof next === 'boolean') setMinimized(next);
}, [minimized]);
const handleSetButtonVisibility = useCallback(async (patch) => {
// Optimistic update so the checkbox flips immediately even before main responds.
setButtonVisibility((prev) => ({ ...prev, ...patch }));
const next = await window.bookmarks.setButtonVisibility(patch);
if (next) setButtonVisibility({
claude: next.claude !== false,
terminal: next.terminal !== false,
redeploy: next.redeploy !== false,
});
}, []);
const handleSetAutoStart = useCallback(async (value) => {
setAutoStart(value);
const next = await window.bookmarks.setAutoStart(value);
if (typeof next === 'boolean') setAutoStart(next);
}, []);
const recentColors = useMemo(() => {
const seen = new Set();
const ordered = [];
for (const b of bookmarks) {
if (b && typeof b.color === 'string' && /^#[0-9a-fA-F]{6}$/.test(b.color)) {
const c = b.color.toLowerCase();
if (!seen.has(c)) { seen.add(c); ordered.push(c); }
}
}
return ordered;
}, [bookmarks]);
if (loading) {
return (
<div className="app loading">
<div className="spinner" />
</div>
);
}
return (
<div className="app">
<Popup
tabs={tabs}
activeTabId={activeTabId}
columns={columns}
bookmarks={bookmarks}
pinned={pinned}
claudeAvailable={claudeAvailable}
inspectRevision={inspectRevision}
recentColors={recentColors}
buttonVisibility={buttonVisibility}
onSetButtonVisibility={handleSetButtonVisibility}
autoStart={autoStart}
onSetAutoStart={handleSetAutoStart}
minimized={minimized}
onToggleMinimized={handleToggleMinimized}
onTogglePin={handleTogglePin}
onAdd={handleAdd}
onEdit={handleEdit}
onRemove={handleRemove}
onMoveBookmark={handleMoveBookmark}
onAddColumn={handleAddColumn}
onRenameColumn={handleRenameColumn}
onRemoveColumn={handleRemoveColumn}
onReorderColumns={handleReorderColumns}
onMoveColumnToTab={handleMoveColumnToTab}
onAddTab={handleAddTab}
onRenameTab={handleRenameTab}
onRemoveTab={handleRemoveTab}
onReorderTabs={handleReorderTabs}
onSelectTab={handleSelectTab}
/>
</div>
);
}