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.
This commit is contained in:
David
2026-04-29 11:05:10 -04:00
parent 8993fbd9f6
commit c860959631
5 changed files with 59 additions and 52 deletions
Binary file not shown.
+10
View File
@@ -296,6 +296,7 @@ function truncatePath(p) {
function broadcastConfig() { function broadcastConfig() {
if (popupWindow && !popupWindow.isDestroyed()) { if (popupWindow && !popupWindow.isDestroyed()) {
const cfg = readConfig();
popupWindow.webContents.send('config-updated', { popupWindow.webContents.send('config-updated', {
tabs: getTabs(), tabs: getTabs(),
columns: getColumns(), columns: getColumns(),
@@ -303,6 +304,7 @@ function broadcastConfig() {
activeTabId: getActiveTabId(), activeTabId: getActiveTabId(),
buttonVisibility: getButtonVisibility(), buttonVisibility: getButtonVisibility(),
autoStart: getAutoStart(), autoStart: getAutoStart(),
minimized: !!cfg.minimized,
}); });
} }
} }
@@ -488,6 +490,7 @@ ipcMain.handle('get-config', () => {
activeTabId: getActiveTabId(), activeTabId: getActiveTabId(),
buttonVisibility: getButtonVisibility(), buttonVisibility: getButtonVisibility(),
autoStart: getAutoStart(), autoStart: getAutoStart(),
minimized: !!cfg.minimized,
}; };
}); });
@@ -712,6 +715,13 @@ ipcMain.handle('run-redeploy', (_event, p) => launchers.runRedeploy(p));
ipcMain.handle('copy-path', (_event, text) => launchers.copyPath(text)); ipcMain.handle('copy-path', (_event, text) => launchers.copyPath(text));
ipcMain.handle('is-claude-available', () => launchers.isClaudeAvailable()); ipcMain.handle('is-claude-available', () => launchers.isClaudeAvailable());
ipcMain.handle('set-minimized', (_event, value) => {
const next = !!value;
writeConfig({ minimized: next });
broadcastConfig();
return next;
});
ipcMain.handle('set-pinned', (_event, value) => { ipcMain.handle('set-pinned', (_event, value) => {
pinned = !!value; pinned = !!value;
writeConfig({ pinned }); writeConfig({ pinned });
+1
View File
@@ -40,6 +40,7 @@ contextBridge.exposeInMainWorld('bookmarks', {
// Window // Window
setPinned: (value) => ipcRenderer.invoke('set-pinned', value), setPinned: (value) => ipcRenderer.invoke('set-pinned', value),
setMinimized: (value) => ipcRenderer.invoke('set-minimized', value),
setPopupHeight: (height) => ipcRenderer.invoke('set-popup-height', height), setPopupHeight: (height) => ipcRenderer.invoke('set-popup-height', height),
setPopupWidth: (width) => ipcRenderer.invoke('set-popup-width', width), setPopupWidth: (width) => ipcRenderer.invoke('set-popup-width', width),
+11
View File
@@ -7,6 +7,7 @@ export default function App() {
const [columns, setColumns] = useState([]); const [columns, setColumns] = useState([]);
const [bookmarks, setBookmarks] = useState([]); const [bookmarks, setBookmarks] = useState([]);
const [pinned, setPinned] = useState(false); const [pinned, setPinned] = useState(false);
const [minimized, setMinimized] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [claudeAvailable, setClaudeAvailable] = useState(true); const [claudeAvailable, setClaudeAvailable] = useState(true);
const [buttonVisibility, setButtonVisibility] = useState({ claude: true, terminal: true, redeploy: true }); const [buttonVisibility, setButtonVisibility] = useState({ claude: true, terminal: true, redeploy: true });
@@ -26,6 +27,7 @@ export default function App() {
setColumns(Array.isArray(cfg.columns) ? cfg.columns : []); setColumns(Array.isArray(cfg.columns) ? cfg.columns : []);
setBookmarks(Array.isArray(cfg.bookmarks) ? cfg.bookmarks : []); setBookmarks(Array.isArray(cfg.bookmarks) ? cfg.bookmarks : []);
setPinned(!!cfg.pinned); setPinned(!!cfg.pinned);
setMinimized(!!cfg.minimized);
setClaudeAvailable(!!hasClaude); setClaudeAvailable(!!hasClaude);
if (cfg.buttonVisibility) { if (cfg.buttonVisibility) {
setButtonVisibility({ setButtonVisibility({
@@ -56,6 +58,7 @@ export default function App() {
}); });
} }
if (typeof payload.autoStart === 'boolean') setAutoStart(payload.autoStart); if (typeof payload.autoStart === 'boolean') setAutoStart(payload.autoStart);
if (typeof payload.minimized === 'boolean') setMinimized(payload.minimized);
}); });
const offShown = window.bookmarks.onPopupShown(() => { const offShown = window.bookmarks.onPopupShown(() => {
setInspectRevision((r) => r + 1); setInspectRevision((r) => r + 1);
@@ -159,6 +162,12 @@ export default function App() {
setPinned(!!next); setPinned(!!next);
}, [pinned]); }, [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) => { const handleSetButtonVisibility = useCallback(async (patch) => {
// Optimistic update so the checkbox flips immediately even before main responds. // Optimistic update so the checkbox flips immediately even before main responds.
setButtonVisibility((prev) => ({ ...prev, ...patch })); setButtonVisibility((prev) => ({ ...prev, ...patch }));
@@ -211,6 +220,8 @@ export default function App() {
onSetButtonVisibility={handleSetButtonVisibility} onSetButtonVisibility={handleSetButtonVisibility}
autoStart={autoStart} autoStart={autoStart}
onSetAutoStart={handleSetAutoStart} onSetAutoStart={handleSetAutoStart}
minimized={minimized}
onToggleMinimized={handleToggleMinimized}
onTogglePin={handleTogglePin} onTogglePin={handleTogglePin}
onAdd={handleAdd} onAdd={handleAdd}
onEdit={handleEdit} onEdit={handleEdit}
+25 -40
View File
@@ -22,6 +22,8 @@ export default function Popup({
onSetButtonVisibility, onSetButtonVisibility,
autoStart, autoStart,
onSetAutoStart, onSetAutoStart,
minimized,
onToggleMinimized,
onTogglePin, onTogglePin,
onAdd, onAdd,
onEdit, onEdit,
@@ -42,14 +44,11 @@ export default function Popup({
const heightDebounceRef = useRef(null); const heightDebounceRef = useRef(null);
const settingsRef = useRef(null); const settingsRef = useRef(null);
const tabStripRef = useRef(null); const tabStripRef = useRef(null);
const brandRef = useRef(null);
const actionsRef = useRef(null);
const [creatingColumn, setCreatingColumn] = useState(false); const [creatingColumn, setCreatingColumn] = useState(false);
const [draftColumnName, setDraftColumnName] = useState(''); const [draftColumnName, setDraftColumnName] = useState('');
const [settingsOpen, setSettingsOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false);
const [confirmRequest, setConfirmRequest] = useState(null); const [confirmRequest, setConfirmRequest] = useState(null);
const [tabStripScrollWidth, setTabStripScrollWidth] = useState(0); const [tabStripScrollWidth, setTabStripScrollWidth] = useState(0);
const [headerMinWidth, setHeaderMinWidth] = useState(0);
const requestConfirm = useCallback((opts) => { const requestConfirm = useCallback((opts) => {
setConfirmRequest(opts); setConfirmRequest(opts);
@@ -90,34 +89,6 @@ export default function Popup({
}; };
}, []); }, []);
// Measure the popup header's intrinsic width so the brand never gets
// ellipsized. The h1 has overflow:hidden + text-overflow:ellipsis to keep
// it from forcing the popup wider than its content; we ask for that width
// explicitly through scrollWidth (which reports the full text width even
// when the element is clipped).
useEffect(() => {
if (!brandRef.current || !actionsRef.current) return undefined;
const update = () => {
const brand = brandRef.current;
const actions = actionsRef.current;
if (!brand || !actions) return;
const iconEl = brand.querySelector('img');
const titleEl = brand.querySelector('h1');
const iconW = iconEl ? iconEl.offsetWidth : 0;
const titleW = titleEl ? titleEl.scrollWidth : 0;
const brandW = iconW + (titleW > 0 ? 8 + titleW : 0);
const actionsW = actions.scrollWidth;
// 28px = popup-inner horizontal padding (14 each side); 8px = gap
// between brand and actions inside popup-header.
setHeaderMinWidth(brandW + 8 + actionsW + 28);
};
update();
const ro = new ResizeObserver(update);
ro.observe(brandRef.current);
ro.observe(actionsRef.current);
return () => ro.disconnect();
}, []);
// Watch the tab strip's scrollWidth so renames or new tabs widen the popup. // Watch the tab strip's scrollWidth so renames or new tabs widen the popup.
// ResizeObserver picks up both layout changes (window width changes) and // ResizeObserver picks up both layout changes (window width changes) and
// content changes (a tab rename growing the strip). // content changes (a tab rename growing the strip).
@@ -142,10 +113,17 @@ export default function Popup({
// to the screen's work area, so wide tab strips either fit (most monitors) // to the screen's work area, so wide tab strips either fit (most monitors)
// or scroll horizontally inside the popup once they would push it off-screen. // or scroll horizontally inside the popup once they would push it off-screen.
useEffect(() => { useEffect(() => {
if (minimized) {
window.bookmarks.setPopupWidth(COLUMN_WIDTH);
return;
}
const colsWidth = Math.max(1, visibleColumns.length || 1) * COLUMN_WIDTH; const colsWidth = Math.max(1, visibleColumns.length || 1) * COLUMN_WIDTH;
const tabsWidth = tabStripScrollWidth > 0 ? tabStripScrollWidth + TAB_STRIP_PADDING : 0; const tabsWidth = tabStripScrollWidth > 0 ? tabStripScrollWidth + TAB_STRIP_PADDING : 0;
window.bookmarks.setPopupWidth(Math.max(colsWidth, tabsWidth, headerMinWidth)); // Floor at COLUMN_WIDTH (one column) — the long brand title in the header
}, [visibleColumns.length, tabStripScrollWidth, headerMinWidth]); // is allowed to ellipsize when the popup is at this minimum; the full
// name is preserved in the tray hover.
window.bookmarks.setPopupWidth(Math.max(COLUMN_WIDTH, colsWidth, tabsWidth));
}, [visibleColumns.length, tabStripScrollWidth, minimized]);
const submitNewColumn = async () => { const submitNewColumn = async () => {
const name = draftColumnName.trim(); const name = draftColumnName.trim();
@@ -184,19 +162,19 @@ export default function Popup({
<div className="popup"> <div className="popup">
<div className="popup-inner" ref={rootRef}> <div className="popup-inner" ref={rootRef}>
<div className="popup-header drag-region"> <div className="popup-header drag-region">
<div className="popup-brand" ref={brandRef}> <div className="popup-brand">
<img src={iconUrl} alt="" className="popup-brand-icon" /> <img src={iconUrl} alt="" className="popup-brand-icon" />
<h1>Trailhead Path Bookmarker</h1> <h1 title="Trailhead Path Bookmarker">Trailhead Path Bookmarker</h1>
</div> </div>
<div className="header-actions" ref={actionsRef}> <div className="header-actions">
{!creatingColumn ? ( {!minimized && !creatingColumn ? (
<button <button
className="icon-button" className="icon-button"
onClick={() => setCreatingColumn(true)} onClick={() => setCreatingColumn(true)}
title={canAddColumn ? 'Add a new column to this tab' : 'Add a tab first'} title={canAddColumn ? 'Add a new column to this tab' : 'Add a tab first'}
disabled={!canAddColumn} disabled={!canAddColumn}
>+</button> >+</button>
) : ( ) : !minimized && creatingColumn ? (
<div className="header-add-column"> <div className="header-add-column">
<input <input
autoFocus autoFocus
@@ -219,7 +197,12 @@ export default function Popup({
title="Cancel" title="Cancel"
>×</button> >×</button>
</div> </div>
)} ) : null}
<button
className="icon-button"
onClick={onToggleMinimized}
title={minimized ? 'Restore' : 'Minimize'}
>{minimized ? '▢' : ''}</button>
<div className="popup-settings" ref={settingsRef}> <div className="popup-settings" ref={settingsRef}>
<button <button
className={`icon-button ${settingsOpen ? 'active' : ''}`} className={`icon-button ${settingsOpen ? 'active' : ''}`}
@@ -275,6 +258,7 @@ export default function Popup({
</div> </div>
</div> </div>
{!minimized && (
<TabBar <TabBar
tabs={tabs} tabs={tabs}
activeTabId={activeTabId} activeTabId={activeTabId}
@@ -287,8 +271,9 @@ export default function Popup({
onReorder={onReorderTabs} onReorder={onReorderTabs}
onRequestConfirm={requestConfirm} onRequestConfirm={requestConfirm}
/> />
)}
{hasNoTabs ? ( {minimized ? null : hasNoTabs ? (
<div className="empty-state"> <div className="empty-state">
No tabs yet click <strong>+</strong> in the tab bar to create your first. No tabs yet click <strong>+</strong> in the tab bar to create your first.
</div> </div>