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
4 changes: 3 additions & 1 deletion gemini-nexus/content/shortcuts.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
const DEFAULT_SHORTCUTS = {
quickAsk: "Ctrl+G",
openPanel: "Alt+S",
browserControl: "Ctrl+B"
browserControl: "Ctrl+B",
focusInput: "Ctrl+P",
switchModel: "Tab"
};

class ShortcutManager {
Expand Down
117 changes: 117 additions & 0 deletions gemini-nexus/css/chat.css
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,123 @@ body.layout-wide .msg.ai {
height: 14px;
}

/* Code Header Buttons Container */
.code-header-btns {
display: flex;
align-items: center;
gap: 8px;
}

/* Preview Button */
.preview-code-btn {
background: transparent;
border: none;
color: var(--text-tertiary);
cursor: pointer;
font-size: 11px;
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s;
}

.preview-code-btn:hover {
background: rgba(255,255,255,0.1);
color: var(--primary);
}
[data-theme="light"] .preview-code-btn:hover {
background: rgba(0,0,0,0.05);
}

.preview-code-btn svg {
width: 14px;
height: 14px;
}

/* HTML Preview Drawer */
.html-preview-drawer {
position: fixed;
top: 0;
right: 0;
width: 100%;
height: 100%;
background: var(--bg-primary);
box-shadow: -4px 0 20px rgba(0,0,0,0.3);
z-index: 3000;
transform: translateX(100%);
transition: transform 0.3s ease, visibility 0.3s ease;
display: flex;
flex-direction: column;
visibility: hidden;
}

.html-preview-drawer.open {
transform: translateX(0);
visibility: visible;
}

.html-preview-drawer-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
background: var(--bg-sidebar);
}

.html-preview-drawer-header h3 {
margin: 0;
font-size: 14px;
color: var(--text-primary);
}

.html-preview-drawer-close {
background: transparent;
border: none;
color: var(--text-tertiary);
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.2s;
}

.html-preview-drawer-close:hover {
background: var(--btn-hover);
color: var(--text-primary);
}

.html-preview-drawer-content {
flex: 1;
overflow: hidden;
}

.html-preview-drawer-content iframe {
width: 100%;
height: 100%;
border: none;
background: #fff;
}

.html-preview-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 2999;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}

.html-preview-overlay.open {
opacity: 1;
visibility: visible;
}

.code-block-wrapper pre {
background: transparent;
color: var(--code-text);
Expand Down
23 changes: 16 additions & 7 deletions gemini-nexus/css/input.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,6 @@ body.layout-wide .footer {
padding-left: 300px; /* Sidebar width + padding */
}

/* Hide unsupported buttons in wide (full-page) mode */
body.layout-wide .tool-btn.context-aware {
display: none !important;
}

body.layout-wide .input-wrapper {
margin: 0 auto;
}
Expand Down Expand Up @@ -107,7 +102,13 @@ body.layout-wide .tools-container {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.tools-container:hover .scroll-nav-btn {
.tools-container.has-overflow:hover .scroll-nav-btn {
opacity: 0;
pointer-events: none;
}

.tools-container.has-overflow.can-scroll-left:hover .scroll-nav-btn.left,
.tools-container.has-overflow.can-scroll-right:hover .scroll-nav-btn.right {
opacity: 1;
transform: translateY(-50%) scale(1);
pointer-events: auto;
Expand Down Expand Up @@ -143,6 +144,14 @@ body.layout-wide .tools-container {
-webkit-overflow-scrolling: touch;
scrollbar-width: none; /* Firefox */
}

.tools-container.has-overflow.can-scroll-left .tools-row {
padding-left: 32px;
}

.tools-container.has-overflow.can-scroll-right .tools-row {
padding-right: 32px;
}
.tools-row::-webkit-scrollbar {
display: none; /* Chrome/Safari */
}
Expand Down Expand Up @@ -386,4 +395,4 @@ body.layout-wide .tools-container {
.input-wrapper {
padding: 6px 6px 6px 12px;
}
}
}
4 changes: 3 additions & 1 deletion gemini-nexus/lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
export const DEFAULT_SHORTCUTS = {
quickAsk: "Ctrl+G",
openPanel: "Alt+S",
browserControl: "Ctrl+B"
browserControl: "Ctrl+B",
focusInput: "Ctrl+P",
switchModel: "Tab"
};

export const DEFAULT_MODEL = "gemini-3-flash";
92 changes: 87 additions & 5 deletions gemini-nexus/sandbox/boot/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,38 @@
// sandbox/boot/events.js
import { sendToBackground } from '../../lib/messaging.js';
import { t } from '../core/i18n.js';
import { DEFAULT_SHORTCUTS } from '../../lib/constants.js';

// Module-level shortcut config
let currentShortcuts = { ...DEFAULT_SHORTCUTS };

export function updateShortcuts(shortcuts) {
if (shortcuts) {
currentShortcuts = { ...DEFAULT_SHORTCUTS, ...shortcuts };
}
}

function matchShortcut(event, shortcutString) {
if (!shortcutString) return false;

const parts = shortcutString.split('+').map(p => p.trim().toLowerCase());
const key = event.key.toLowerCase();

const hasCtrl = parts.includes('ctrl');
const hasAlt = parts.includes('alt');
const hasShift = parts.includes('shift');
const hasMeta = parts.includes('meta') || parts.includes('command');

if (event.ctrlKey !== hasCtrl) return false;
if (event.altKey !== hasAlt) return false;
if (event.shiftKey !== hasShift) return false;
if (event.metaKey !== hasMeta) return false;

const mainKeys = parts.filter(p => !['ctrl','alt','shift','meta','command'].includes(p));
if (mainKeys.length !== 1) return false;

return key === mainKeys[0];
}

export function bindAppEvents(app, ui, setResizeRef) {
// New Chat Buttons
Expand All @@ -25,14 +57,63 @@ export function bindAppEvents(app, ui, setResizeRef) {
const toolsRow = document.getElementById('tools-row');
const scrollLeftBtn = document.getElementById('tools-scroll-left');
const scrollRightBtn = document.getElementById('tools-scroll-right');
const toolsContainer = toolsRow ? toolsRow.closest('.tools-container') : null;

const SCROLL_STEP = 150;
const EPSILON = 2;

const updateToolsScrollState = () => {
if (!toolsRow || !toolsContainer) return;
const { scrollLeft, scrollWidth, clientWidth } = toolsRow;
const style = window.getComputedStyle(toolsRow);
const paddingLeft = parseFloat(style.paddingLeft) || 0;
const paddingRight = parseFloat(style.paddingRight) || 0;

const maxScrollLeft = scrollWidth - clientWidth;
const hasOverflow = maxScrollLeft > EPSILON;
const canScrollLeft = scrollLeft > paddingLeft + EPSILON;
const canScrollRight = scrollLeft + clientWidth < scrollWidth - paddingRight - EPSILON;

toolsContainer.classList.toggle('has-overflow', hasOverflow);
toolsContainer.classList.toggle('can-scroll-left', hasOverflow && canScrollLeft);
toolsContainer.classList.toggle('can-scroll-right', hasOverflow && canScrollRight);
};

const scheduleToolsScrollUpdate = (() => {
let rafId = 0;
return () => {
if (rafId) return;
rafId = requestAnimationFrame(() => {
rafId = 0;
updateToolsScrollState();
});
};
})();

if (toolsRow && scrollLeftBtn && scrollRightBtn) {
scrollLeftBtn.addEventListener('click', () => {
toolsRow.scrollBy({ left: -150, behavior: 'smooth' });
toolsRow.scrollBy({ left: -SCROLL_STEP, behavior: 'smooth' });
});
scrollRightBtn.addEventListener('click', () => {
toolsRow.scrollBy({ left: 150, behavior: 'smooth' });
toolsRow.scrollBy({ left: SCROLL_STEP, behavior: 'smooth' });
});

toolsRow.addEventListener('scroll', scheduleToolsScrollUpdate, { passive: true });
window.addEventListener('resize', scheduleToolsScrollUpdate);

if (typeof ResizeObserver !== 'undefined') {
const resizeObserver = new ResizeObserver(scheduleToolsScrollUpdate);
resizeObserver.observe(toolsRow);
}

if (typeof MutationObserver !== 'undefined') {
const mutationObserver = new MutationObserver(scheduleToolsScrollUpdate);
mutationObserver.observe(toolsRow, { childList: true, subtree: true, characterData: true });
}

// Initial state after layout settles.
setTimeout(scheduleToolsScrollUpdate, 0);
setTimeout(scheduleToolsScrollUpdate, 100);
}

// Tools
Expand Down Expand Up @@ -124,8 +205,8 @@ export function bindAppEvents(app, ui, setResizeRef) {

if (inputFn && sendBtn) {
inputFn.addEventListener('keydown', (e) => {
// Tab Cycle Models
if (e.key === 'Tab') {
// Switch Model shortcut (configurable, default: Tab)
if (matchShortcut(e, currentShortcuts.switchModel)) {
e.preventDefault();
if (modelSelect) {
const direction = e.shiftKey ? -1 : 1;
Expand All @@ -152,7 +233,8 @@ export function bindAppEvents(app, ui, setResizeRef) {
}

document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'p') {
// Focus Input shortcut (configurable, default: Ctrl+P)
if (matchShortcut(e, currentShortcuts.focusInput)) {
e.preventDefault();
if(inputFn) inputFn.focus();
}
Expand Down
2 changes: 2 additions & 0 deletions gemini-nexus/sandbox/boot/messaging.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

// sandbox/boot/messaging.js
import { updateShortcuts as updateEventShortcuts } from './events.js';

export class AppMessageBridge {
constructor() {
Expand Down Expand Up @@ -49,6 +50,7 @@ export class AppMessageBridge {
dispatch(action, payload, event) {
if (action === 'RESTORE_SHORTCUTS') {
this.ui.updateShortcuts(payload);
updateEventShortcuts(payload); // Update event handlers
return;
}
if (action === 'RESTORE_THEME') {
Expand Down
2 changes: 2 additions & 0 deletions gemini-nexus/sandbox/core/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export const translations = {
"noTabsFound": "No open tabs found.",
"lockTab": "Lock Tab",
"unlockTab": "Unlock Tab (Auto-follow Active)",
"htmlPreview": "HTML Preview",
},
zh: {
"mcpTools": "外部 MCP 工具",
Expand Down Expand Up @@ -253,6 +254,7 @@ export const translations = {
"noTabsFound": "未找到打开的标签页。",
"lockTab": "锁定标签页",
"unlockTab": "解锁 (自动跟随活动标签页)",
"htmlPreview": "HTML 预览",
}
};

Expand Down
22 changes: 17 additions & 5 deletions gemini-nexus/sandbox/render/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,26 @@ export function configureMarkdown() {
highlighted = escapeHtml(code);
}

// Check if the code is HTML for preview button (use original lang to detect)
const langLower = (lang || '').toLowerCase();
const isHtml = langLower === 'html' || langLower === 'htm' || validLang === 'html';
const previewBtn = isHtml ? `
<button class="preview-code-btn" aria-label="Preview HTML">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
<span>Preview</span>
</button>` : '';

return `
<div class="code-block-wrapper">
<div class="code-block-wrapper"${isHtml ? ' data-html-preview="true"' : ''}>
<div class="code-header">
<span class="code-lang">${validLang}</span>
<button class="copy-code-btn" aria-label="Copy code">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
<span>Copy</span>
</button>
<div class="code-header-btns">
${previewBtn}
<button class="copy-code-btn" aria-label="Copy code">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
<span>Copy</span>
</button>
</div>
</div>
<pre><code class="hljs language-${validLang}">${highlighted}</code></pre>
</div>`;
Expand Down
5 changes: 5 additions & 0 deletions gemini-nexus/sandbox/render/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import { transformMarkdown } from './pipeline.js';
export function renderContent(contentDiv, text, role) {
// Render Markdown and Math for AI responses
if (role === 'ai') {
if (typeof marked === 'undefined') {
// Avoid rendering raw HTML before markdown is ready.
contentDiv.textContent = text || '';
return;
}

// Use shared pipeline
const html = transformMarkdown(text);
Expand Down
Loading