Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions collab-electron/src/preload/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,9 @@ contextBridge.exposeInMainWorld("shellApi", {
ptyKillSession: (sessionId: string): Promise<void> =>
ipcRenderer.invoke("pty:kill", { sessionId }),

ptyWrite: (sessionId: string, data: string): Promise<void> =>
ipcRenderer.invoke("pty:write", { sessionId, data }),

onPtyStatusChanged: (
cb: (payload: { sessionId: string; foreground: string }) => void,
) => {
Expand Down
97 changes: 97 additions & 0 deletions collab-electron/src/windows/shell/src/shell.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
131 changes: 131 additions & 0 deletions collab-electron/src/windows/shell/src/tile-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ export function createTileManager({
spawnBrowserWebview(t);
saveCanvasImmediate();
},
onQuickCommand: (id, btnEl) => showQuickCommandDropdown(id, btnEl),
});

// Double-click title bar → center tile in viewport
Expand Down Expand Up @@ -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 = "&times;";
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) {
Expand Down
14 changes: 14 additions & 0 deletions collab-electron/src/windows/shell/src/tile-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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 = `<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M9.5 1L4 9h4l-.5 6L12 7H8z"/></svg>`;
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 = "&times;";
Expand Down
Loading