diff --git a/collab-electron/src/preload/shell.ts b/collab-electron/src/preload/shell.ts index b01ad4f8..cebe81c1 100644 --- a/collab-electron/src/preload/shell.ts +++ b/collab-electron/src/preload/shell.ts @@ -223,6 +223,9 @@ contextBridge.exposeInMainWorld("shellApi", { ptyKillSession: (sessionId: string): Promise => ipcRenderer.invoke("pty:kill", { sessionId }), + ptyWrite: (sessionId: string, data: string): Promise => + ipcRenderer.invoke("pty:write", { sessionId, data }), + onPtyStatusChanged: ( cb: (payload: { sessionId: string; foreground: string }) => void, ) => { diff --git a/collab-electron/src/windows/shell/src/shell.css b/collab-electron/src/windows/shell/src/shell.css index fa6b9510..d2dec81d 100644 --- a/collab-electron/src/windows/shell/src/shell.css +++ b/collab-electron/src/windows/shell/src/shell.css @@ -1178,3 +1178,100 @@ body.platform-win { .tile-url-input::placeholder { color: rgba(128, 128, 128, 0.5); } + +/* ── Quick command dropdown ── */ + +.quick-cmd-dropdown { + position: absolute; + min-width: 180px; + max-width: 320px; + max-height: 260px; + overflow-y: auto; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); + padding: 4px 0; + z-index: 9999; +} + +.quick-cmd-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 6px 12px; + font-size: 12px; + font-family: var(--font-mono); + color: var(--fg); + cursor: pointer; +} + +.quick-cmd-item-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.quick-cmd-del-btn { + all: unset; + font-size: 14px; + line-height: 1; + color: var(--muted); + cursor: pointer; + opacity: 0; + flex-shrink: 0; + padding: 0 2px; + border-radius: 3px; +} + +.quick-cmd-item:hover .quick-cmd-del-btn { + opacity: 0.6; +} + +.quick-cmd-del-btn:hover { + opacity: 1 !important; + color: #e53935; +} + +.quick-cmd-item:hover { + background: color-mix(in srgb, var(--fg) 8%, transparent); +} + +.quick-cmd-separator { + height: 1px; + margin: 4px 8px; + background: var(--border); +} + +.quick-cmd-add { + padding: 6px 12px; + font-size: 12px; + font-family: var(--font-mono); + color: var(--muted); + cursor: pointer; +} + +.quick-cmd-add:hover { + color: var(--fg); +} + +.quick-cmd-input-wrapper { + padding: 4px 8px; +} + +.quick-cmd-input { + width: 100%; + padding: 4px 6px; + font-size: 12px; + font-family: var(--font-mono); + color: var(--fg); + background: transparent; + border: 1px solid var(--border); + border-radius: 4px; + outline: none; +} + +.quick-cmd-input:focus { + border-color: var(--fg); +} diff --git a/collab-electron/src/windows/shell/src/tile-manager.js b/collab-electron/src/windows/shell/src/tile-manager.js index 07d17a59..31437d72 100644 --- a/collab-electron/src/windows/shell/src/tile-manager.js +++ b/collab-electron/src/windows/shell/src/tile-manager.js @@ -458,6 +458,7 @@ export function createTileManager({ spawnBrowserWebview(t); saveCanvasImmediate(); }, + onQuickCommand: (id, btnEl) => showQuickCommandDropdown(id, btnEl), }); // Double-click title bar → center tile in viewport @@ -663,6 +664,136 @@ export function createTileManager({ // -- Tile updates for external events -- + // -- Quick command dropdown -- + + let activeQuickDropdown = null; + + function closeQuickDropdown() { + if (activeQuickDropdown) { + activeQuickDropdown.remove(); + activeQuickDropdown = null; + } + } + + function showQuickCommandDropdown(tileId, btnEl) { + closeQuickDropdown(); + + const tile = getTile(tileId); + if (!tile || tile.type !== "term" || !tile.ptySessionId) return; + + const dropdown = document.createElement("div"); + dropdown.className = "quick-cmd-dropdown"; + activeQuickDropdown = dropdown; + + const renderCommands = async () => { + const commands = (await window.shellApi.getPref("quickCommands")) || []; + + dropdown.innerHTML = ""; + + if (commands.length > 0) { + for (let i = 0; i < commands.length; i++) { + const cmd = commands[i]; + const item = document.createElement("div"); + item.className = "quick-cmd-item"; + const label = document.createElement("span"); + label.className = "quick-cmd-item-label"; + label.textContent = cmd; + item.appendChild(label); + const delBtn = document.createElement("button"); + delBtn.className = "quick-cmd-del-btn"; + delBtn.title = "Delete"; + delBtn.innerHTML = "×"; + delBtn.addEventListener("mousedown", (e) => e.stopPropagation()); + delBtn.addEventListener("click", async (e) => { + e.stopPropagation(); + const updated = [...commands]; + updated.splice(i, 1); + await window.shellApi.setPref("quickCommands", updated); + renderCommands(); + }); + item.appendChild(delBtn); + item.addEventListener("mousedown", (e) => e.stopPropagation()); + item.addEventListener("click", (e) => { + if (e.target === delBtn) return; + e.stopPropagation(); + window.shellApi.ptyWrite(tile.ptySessionId, cmd + "\n"); + closeQuickDropdown(); + }); + dropdown.appendChild(item); + } + + const sep = document.createElement("div"); + sep.className = "quick-cmd-separator"; + dropdown.appendChild(sep); + } + + const addItem = document.createElement("div"); + addItem.className = "quick-cmd-add"; + addItem.textContent = "+ Add command\u2026"; + addItem.addEventListener("mousedown", (e) => e.stopPropagation()); + addItem.addEventListener("click", (e) => { + e.stopPropagation(); + addItem.replaceWith(createAddInput(commands, tile)); + }); + dropdown.appendChild(addItem); + }; + + renderCommands(); + + // Position dropdown relative to the button + const btnRect = btnEl.getBoundingClientRect(); + const tileContainer = btnEl.closest(".canvas-tile"); + const tileRect = tileContainer.getBoundingClientRect(); + dropdown.style.right = `${tileRect.right - btnRect.left}px`; + dropdown.style.top = `${btnRect.bottom - tileRect.top + 4}px`; + + tileContainer.appendChild(dropdown); + } + + function createAddInput(existingCommands, tile) { + const wrapper = document.createElement("div"); + wrapper.className = "quick-cmd-input-wrapper"; + + const input = document.createElement("input"); + input.className = "quick-cmd-input"; + input.placeholder = "Command\u2026"; + input.addEventListener("mousedown", (e) => e.stopPropagation()); + + input.addEventListener("keydown", async (e) => { + e.stopPropagation(); + if (e.key === "Enter") { + const cmd = input.value.trim(); + if (!cmd) return; + const updated = [...existingCommands, cmd]; + await window.shellApi.setPref("quickCommands", updated); + window.shellApi.ptyWrite(tile.ptySessionId, cmd + "\n"); + closeQuickDropdown(); + } + if (e.key === "Escape") { + closeQuickDropdown(); + } + }); + + wrapper.appendChild(input); + // Auto-focus after DOM insert + requestAnimationFrame(() => input.focus()); + return wrapper; + } + + // Click outside closes dropdown + document.addEventListener("mousedown", (e) => { + if (activeQuickDropdown && !activeQuickDropdown.contains(e.target)) { + closeQuickDropdown(); + } + }); + + // Escape key closes dropdown + document.addEventListener("keydown", (e) => { + if (e.key === "Escape" && activeQuickDropdown) { + closeQuickDropdown(); + } + }); + function updateTileForRename(oldPath, newPath) { let anyUpdated = false; for (const t of tiles) { diff --git a/collab-electron/src/windows/shell/src/tile-renderer.js b/collab-electron/src/windows/shell/src/tile-renderer.js index f393877e..f5a81aa5 100644 --- a/collab-electron/src/windows/shell/src/tile-renderer.js +++ b/collab-electron/src/windows/shell/src/tile-renderer.js @@ -8,6 +8,7 @@ import { splitDisplayPath } from "@collab/shared/path-utils"; * @param {(id: string, e?: MouseEvent) => void} callbacks.onFocus * @param {((id: string) => void)|null} [callbacks.onOpenInViewer] * @param {((id: string, url: string) => void)|null} [callbacks.onNavigate] + * @param {((id: string, btnEl: HTMLElement) => void)|null} [callbacks.onQuickCommand] */ export function createTileDOM(tile, callbacks) { const container = document.createElement("div"); @@ -145,6 +146,19 @@ export function createTileDOM(tile, callbacks) { btnGroup.appendChild(viewBtn); } + if (tile.type === "term" && callbacks.onQuickCommand) { + const quickBtn = document.createElement("button"); + quickBtn.className = "tile-action-btn tile-quick-cmd-btn"; + quickBtn.innerHTML = ``; + quickBtn.title = "Quick commands"; + quickBtn.addEventListener("mousedown", (e) => e.stopPropagation()); + quickBtn.addEventListener("click", (e) => { + e.stopPropagation(); + callbacks.onQuickCommand(tile.id, quickBtn); + }); + btnGroup.insertBefore(quickBtn, btnGroup.firstChild); + } + const closeBtn = document.createElement("button"); closeBtn.className = "tile-action-btn tile-close-btn"; closeBtn.innerHTML = "×";