Skip to content
Merged
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
2 changes: 2 additions & 0 deletions crates/conch_tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ libc = "0.2"
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6"
objc2-foundation = { version = "0.3", features = ["NSString", "NSLocale"] }
core-text = "20"
core-foundation = "0.9"

[dev-dependencies]
tempfile = "3"
Expand Down
123 changes: 110 additions & 13 deletions crates/conch_tauri/frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -505,11 +505,43 @@
}
#bottom-panel.hidden { display: none; }
#bottom-panel-header {
display: flex; align-items: center; padding: 4px 10px;
font-size: var(--ui-font-small); color: var(--dim-fg); text-transform: uppercase;
letter-spacing: 0.5px; flex-shrink: 0;
border-bottom: 1px solid var(--tab-border);
}
display: flex; align-items: center; flex-shrink: 0;
border-bottom: 1px solid var(--tab-border); padding: 0;
}
#bottom-panel-tabs {
display: flex; flex: 1; overflow-x: auto; min-width: 0;
}
.bottom-tab {
padding: 5px 14px; cursor: pointer; border: none; background: none;
color: var(--text-muted); font-size: var(--ui-font-small);
border-bottom: 2px solid transparent; white-space: nowrap;
}
.bottom-tab:hover { color: var(--fg); }
.bottom-tab.active { color: var(--fg); border-bottom-color: var(--blue); }
#bottom-panel-actions {
display: flex; align-items: center; gap: 4px; padding: 0 8px; flex-shrink: 0;
}
.bottom-panel-action-btn {
border: none; background: none; color: var(--text-muted); cursor: pointer;
font-size: var(--ui-font-small); padding: 2px 6px; border-radius: 3px;
}
.bottom-panel-action-btn:hover { color: var(--fg); background: var(--active-highlight); }
.notif-entry {
display: flex; align-items: flex-start; gap: 8px; padding: 3px 0;
font-size: var(--ui-font-small); border-bottom: 1px solid var(--tab-border);
}
.notif-time { color: var(--text-muted); flex-shrink: 0; font-family: "JetBrains Mono", monospace; }
.notif-dot {
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; margin-top: 4px;
}
.notif-dot-info { background: var(--cyan); }
.notif-dot-success { background: var(--green); }
.notif-dot-warn { background: var(--yellow); }
.notif-dot-error { background: var(--red); }
.notif-text { flex: 1; min-width: 0; }
.notif-title { color: var(--fg); font-weight: 500; }
.notif-body { color: var(--text-muted); margin-left: 4px; }
.notif-empty { color: var(--text-muted); font-style: italic; padding: 12px 0; }
#bottom-panel-content { flex: 1; overflow-y: auto; padding: 4px 8px; }
#ssh-panel-wrap {
display: flex;
Expand Down Expand Up @@ -1125,7 +1157,10 @@
</div>
</div>
<div id="bottom-panel" class="hidden">
<div id="bottom-panel-header">Output</div>
<div id="bottom-panel-header">
<div id="bottom-panel-tabs"></div>
<div id="bottom-panel-actions"></div>
</div>
<div id="bottom-panel-content"></div>
</div>
</div>
Expand All @@ -1147,6 +1182,7 @@
<script src="utils.js"></script>
<script src="titlebar.js"></script>
<script src="toast.js"></script>
<script src="notification-panel.js"></script>
<script src="plugin-widgets.js"></script>
<script src="settings.js"></script>
<script src="file-icons.js"></script>
Expand Down Expand Up @@ -1297,6 +1333,17 @@
nativeNotifications: appCfg.native_notifications !== false,
});
}
if (window.notificationPanel) {
window.notificationPanel.init();
}
try {
const layoutData = await invoke('get_saved_layout');
if (layoutData.bottom_panel_visible === false) {
document.getElementById('bottom-panel').classList.add('hidden');
} else {
document.getElementById('bottom-panel').classList.remove('hidden');
}
} catch (_) {}

// Apply UI chrome font settings.
const r = document.documentElement.style;
Expand Down Expand Up @@ -2153,7 +2200,7 @@

// Save window layout on resize (debounced).
let windowResaveSaveTimer = null;
window.addEventListener('resize', () => {
function debouncedSaveLayout() {
if (windowResaveSaveTimer) clearTimeout(windowResaveSaveTimer);
windowResaveSaveTimer = setTimeout(() => {
invoke('save_window_layout', {
Expand All @@ -2162,10 +2209,12 @@
ssh_panel_visible: window.sshPanel ? !window.sshPanel.isHidden() : true,
files_panel_width: filesPanelEl.offsetWidth,
files_panel_visible: window.filesPanel ? !window.filesPanel.isHidden() : true,
bottom_panel_visible: !document.getElementById('bottom-panel').classList.contains('hidden'),
},
}).catch(() => {});
}, 500);
});
}
window.addEventListener('resize', debouncedSaveLayout);

// ---- Vault init ----
if (window.vault) {
Expand Down Expand Up @@ -2208,8 +2257,10 @@
// Listen for plugin panel registrations and create panel containers.
listenOnCurrentWindow('plugin-panel-registered', async (event) => {
const { handle, plugin, name, location } = event.payload;
// For now, add plugin panels as additional content in the right sidebar.
// Future: support left/bottom panel locations with tabbed UI.
// Bottom-panel plugins are routed to the tabbed bottom panel via
// plugin-widgets.js — skip creating a sidebar container for them.
if (location === 'bottom') return;
// Add non-bottom plugin panels as additional content in the right sidebar.
const sshPanel = document.getElementById('ssh-panel');
if (!sshPanel) return;

Expand Down Expand Up @@ -2242,6 +2293,24 @@
console.error('Initial plugin render failed:', e);
}
});

// Clean up sidebar containers when a plugin is disabled/unloaded.
listenOnCurrentWindow('plugin-panels-removed', (event) => {
const { handles } = event.payload;
for (const handle of handles) {
const container = document.querySelector(`[data-plugin-handle="${handle}"]`);
if (container) {
// Remove the separator and header preceding the container.
const prev = container.previousElementSibling;
if (prev && prev.classList.contains('ssh-section-header')) {
const sep = prev.previousElementSibling;
if (sep && sep.classList.contains('ssh-panel-separator')) sep.remove();
prev.remove();
}
container.remove();
}
}
});
}

// ---- Drag and drop ----
Expand Down Expand Up @@ -2525,11 +2594,14 @@
renameActiveTab();
return;
}
// Toggle bottom panel (placeholder — no bottom panel content yet).
// Toggle bottom panel.
if (action === 'toggle-bottom-panel') {
const bp = document.getElementById('bottom-panel');
if (bp) bp.classList.toggle('hidden');
setTimeout(() => fitAndResizeTab(currentTab()), 50);
if (bp) {
bp.classList.toggle('hidden');
setTimeout(() => fitAndResizeTab(currentTab()), 50);
debouncedSaveLayout();
}
return;
}
if (action === 'about') {
Expand Down Expand Up @@ -2867,6 +2939,31 @@
document.body.style.fontSize = appCfg.ui_font_size + 'px';
}

// Re-apply terminal font/size to all open panes.
try {
const termCfg = await invoke('get_terminal_config');
let newTermFont = '"JetBrains Mono", "Fira Code", "Cascadia Code"' + FONT_FALLBACKS;
if (termCfg.font_family) {
newTermFont = '"' + termCfg.font_family + '", "Fira Code", "Cascadia Code"' + FONT_FALLBACKS;
}
termFontFamily = newTermFont;
const newTermSize = termCfg.font_size > 0 ? termCfg.font_size : 14;
termFontSize = newTermSize;
for (const pane of panes.values()) {
pane.term.options.fontFamily = newTermFont;
pane.term.options.fontSize = newTermSize;
}
// Allow the browser to load/measure the new font before re-fitting.
requestAnimationFrame(() => {
for (const pane of panes.values()) {
if (pane.fitAddon) pane.fitAddon.fit();
pane.term.refresh(0, pane.term.rows - 1);
}
});
} catch (e) {
console.warn('Failed to reload terminal font:', e);
}

// Re-apply notification settings.
if (window.toast && window.toast.configure) {
window.toast.configure({
Expand Down
145 changes: 145 additions & 0 deletions crates/conch_tauri/frontend/notification-panel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Bottom panel with tabbed interface — built-in Notifications tab + plugin tabs.

(function (exports) {
'use strict';

let tabsEl = null;
let actionsEl = null;
let contentEl = null;
let activeTabId = 'notifications';
const pluginTabs = new Map();

function init() {
tabsEl = document.getElementById('bottom-panel-tabs');
actionsEl = document.getElementById('bottom-panel-actions');
contentEl = document.getElementById('bottom-panel-content');

addTab('notifications', 'Notifications');
activateTab('notifications');

const clearBtn = document.createElement('button');
clearBtn.className = 'bottom-panel-action-btn';
clearBtn.textContent = 'Clear';
clearBtn.title = 'Clear notification history';
clearBtn.addEventListener('click', () => {
if (window.toast && window.toast.clearHistory) window.toast.clearHistory();
});
actionsEl.appendChild(clearBtn);

if (window.toast && window.toast.onNotification) {
window.toast.onNotification((record) => {
if (activeTabId === 'notifications') renderNotifications();
});
}
}

function addTab(id, label) {
const btn = document.createElement('button');
btn.className = 'bottom-tab';
btn.textContent = label;
btn.dataset.tabId = id;
btn.addEventListener('click', () => activateTab(id));
tabsEl.appendChild(btn);
}

function removeTab(id) {
const btn = tabsEl.querySelector('[data-tab-id="' + id + '"]');
if (btn) btn.remove();
pluginTabs.delete(id);
if (activeTabId === id) activateTab('notifications');
}

function activateTab(id) {
activeTabId = id;
for (const btn of tabsEl.querySelectorAll('.bottom-tab')) {
btn.classList.toggle('active', btn.dataset.tabId === id);
}
if (actionsEl) {
actionsEl.style.display = id === 'notifications' ? '' : 'none';
}
if (id === 'notifications') {
renderNotifications();
} else {
const plugin = pluginTabs.get(id);
if (plugin && plugin.renderFn) {
contentEl.innerHTML = '';
plugin.renderFn(contentEl);
}
}
}

function renderNotifications() {
if (!contentEl) return;
contentEl.innerHTML = '';

const history = (window.toast && window.toast.getHistory) ? window.toast.getHistory() : [];
if (history.length === 0) {
const empty = document.createElement('div');
empty.className = 'notif-empty';
empty.textContent = 'No notifications yet.';
contentEl.appendChild(empty);
return;
}

const frag = document.createDocumentFragment();
for (const entry of history) {
const row = document.createElement('div');
row.className = 'notif-entry';

const time = document.createElement('span');
time.className = 'notif-time';
const d = entry.timestamp;
time.textContent = String(d.getHours()).padStart(2, '0') + ':' +
String(d.getMinutes()).padStart(2, '0') + ':' +
String(d.getSeconds()).padStart(2, '0');
row.appendChild(time);

const dot = document.createElement('span');
dot.className = 'notif-dot notif-dot-' + (entry.level || 'info');
row.appendChild(dot);

const text = document.createElement('span');
text.className = 'notif-text';
const title = document.createElement('span');
title.className = 'notif-title';
title.textContent = entry.title || '';
text.appendChild(title);
if (entry.body) {
const body = document.createElement('span');
body.className = 'notif-body';
body.textContent = entry.body;
text.appendChild(body);
}
row.appendChild(text);

frag.appendChild(row);
}
contentEl.appendChild(frag);
}

function addPluginTab(id, name, renderFn) {
if (pluginTabs.has(id)) return;
pluginTabs.set(id, { name, renderFn });
addTab(id, name);
}

function removePluginTab(id) {
removeTab(id);
}

function updatePluginTab(id, renderFn) {
const plugin = pluginTabs.get(id);
if (plugin) {
plugin.renderFn = renderFn;
if (activeTabId === id) activateTab(id);
}
}

exports.notificationPanel = {
init,
activateTab,
addPluginTab,
removePluginTab,
updatePluginTab,
};
})(window);
Loading
Loading