g3 console init

This commit is contained in:
Dhanji Prasanna
2025-11-07 09:25:17 +11:00
parent aaf918828f
commit cb43fcdecf
24 changed files with 2860 additions and 498 deletions

View File

@@ -103,10 +103,60 @@
</div>
</div>
<script src="/js/api.js"></script>
<script src="/js/state.js"></script>
<script src="/js/components.js"></script>
<script src="/js/router.js"></script>
<script src="/js/app.js"></script>
<!-- File Browser Modal -->
<div id="file-browser-modal" class="modal hidden">
<div class="modal-overlay"></div>
<div class="modal-content">
<div class="modal-header">
<h2 id="file-browser-title">Select Directory</h2>
<button id="file-browser-close" class="modal-close">&times;</button>
</div>
<div class="modal-body">
<div class="file-browser">
<div class="file-browser-path">
<label>Current Path:</label>
<input type="text" id="file-browser-current-path" readonly />
<button type="button" id="file-browser-parent" class="btn btn-secondary">↑ Parent</button>
</div>
<div class="file-browser-list" id="file-browser-list">
<div class="spinner-container">
<div class="spinner"></div>
<p>Loading...</p>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" id="file-browser-cancel" class="btn btn-secondary">Cancel</button>
<button type="button" id="file-browser-select" class="btn btn-primary">Select</button>
</div>
</div>
</div>
<!-- Full File View Modal -->
<div id="full-file-modal" class="modal hidden">
<div class="modal-overlay"></div>
<div class="modal-content" style="max-width: 900px; max-height: 90vh;">
<div class="modal-header">
<h2 id="full-file-title">File Content</h2>
<button id="full-file-close" class="modal-close">&times;</button>
</div>
<div class="modal-body" style="max-height: 70vh; overflow-y: auto;">
<div id="full-file-content">
<div class="spinner-container">
<div class="spinner"></div>
<p>Loading...</p>
</div>
</div>
</div>
</div>
</div>
<script src="/js/api.js?v=6"></script>
<script src="/js/state.js?v=6"></script>
<script src="/js/components.js?v=6"></script>
<script src="/js/file-browser.js?v=6"></script>
<script src="/js/router.js?v=6"></script>
<script src="/js/app.js?v=6"></script>
</body>
</html>

View File

@@ -32,12 +32,14 @@ const api = {
});
if (!response.ok) {
// Try to extract error message from response
let errorMessage = `Failed to launch instance (${response.status})`;
try {
const errorData = await response.json();
throw new Error(errorData.message || errorData.error || 'Failed to launch instance');
errorMessage = errorData.message || errorData.error || errorMessage;
} catch (e) {
throw new Error(`Failed to launch instance (${response.status})`);
// JSON parsing failed, use default message
}
throw new Error(errorMessage);
}
return response.json();
},
@@ -76,5 +78,26 @@ const api = {
});
if (!response.ok) throw new Error('Failed to save state');
return response.json();
},
// Browse filesystem
async browseFilesystem(path, browseType = 'directory') {
const response = await fetch(`${API_BASE}/browse`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: path, browse_type: browseType })
});
if (!response.ok) throw new Error('Failed to browse filesystem');
return response.json();
},
// Get full file content
async getFileContent(instanceId, fileName) {
const response = await fetch(`${API_BASE}/instances/${instanceId}/file?name=${encodeURIComponent(fileName)}`);
if (!response.ok) throw new Error('Failed to fetch file content');
return response.json();
}
};
// Expose to window for global access
window.api = api;

View File

@@ -98,44 +98,25 @@ const modal = {
},
browseDirectory(inputId) {
// Create a hidden file input with directory picker
const input = document.createElement('input');
input.type = 'file';
input.webkitdirectory = true;
input.directory = true;
input.multiple = false;
input.onchange = (e) => {
const files = e.target.files;
if (files.length > 0) {
// Get the directory path from the first file
const path = files[0].webkitRelativePath.split('/')[0];
// In browser context, we get relative path, so we need to construct full path
// For now, just use the directory name and let user adjust
// Use custom file browser
fileBrowser.open({
mode: 'directory',
initialPath: document.getElementById(inputId).value || '/Users',
callback: (path) => {
document.getElementById(inputId).value = path;
}
};
input.click();
});
},
browseFile(inputId) {
// Create a hidden file input
const input = document.createElement('input');
input.type = 'file';
input.accept = '*';
input.onchange = (e) => {
const files = e.target.files;
if (files.length > 0) {
// Get the file name
// Note: For security reasons, browsers don't give us the full path
// User will need to type the full path manually
document.getElementById(inputId).value = files[0].name;
// Use custom file browser
fileBrowser.open({
mode: 'file',
initialPath: document.getElementById(inputId).value || '/Users',
callback: (path) => {
document.getElementById(inputId).value = path;
}
};
input.click();
});
},
open() {
@@ -147,9 +128,9 @@ const modal = {
if (state.g3BinaryPath) {
form.g3_binary_path.value = state.g3BinaryPath;
}
form.provider.value = state.lastProvider;
this.updateModelOptions(state.lastProvider);
form.model.value = state.lastModel;
form.provider.value = state.lastProvider || 'databricks';
this.updateModelOptions(state.lastProvider || 'databricks');
form.model.value = state.lastModel || 'databricks-claude-sonnet-4-5';
this.element.classList.remove('hidden');
},
@@ -196,10 +177,11 @@ const modal = {
g3_binary_path: formData.get('g3_binary_path') || null
};
const submitBtn = form.querySelector('button[type="submit"]');
const modalBody = this.element.querySelector('.modal-body');
try {
// Show loading state
const submitBtn = form.querySelector('button[type="submit"]');
const modalBody = this.element.querySelector('.modal-body');
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="spinner" style="width: 1rem; height: 1rem; border-width: 2px; display: inline-block; vertical-align: middle;"></span> Starting g3 instance...';
@@ -255,7 +237,6 @@ const modal = {
// Insert error message at the top of modal body
modalBody.insertBefore(errorDiv, modalBody.firstChild);
const submitBtn = form.querySelector('button[type="submit"]');
submitBtn.disabled = false;
submitBtn.textContent = 'Start Instance';
}
@@ -281,9 +262,11 @@ function initTheme() {
async function init() {
// Prevent double initialization
if (window.g3Initialized) {
console.log('[App] init() called but already initialized, returning');
return;
}
window.g3Initialized = true;
console.log('[App] init() starting...');
// Load state
await state.load();
@@ -294,13 +277,21 @@ async function init() {
// Initialize modal
modal.init();
// Initialize file browser
fileBrowser.init();
// Expose modal to window for button access
window.modal = modal;
// New Run button
document.getElementById('new-run-btn').addEventListener('click', () => {
modal.open();
});
// Initialize router
console.log('[App] About to call router.init()');
router.init();
console.log('[App] init() complete');
}
// Simplified initialization - call exactly once when DOM is ready

View File

@@ -16,6 +16,12 @@ const components = {
// Render progress bar
progressBar(instance, stats) {
const duration = stats.duration_secs;
// Handle zero duration to avoid NaN
if (duration === 0) {
return this.singleProgressBar(0);
}
const estimated = duration * 1.5; // Simple estimation
const progress = Math.min((duration / estimated) * 100, 100);
@@ -41,16 +47,31 @@ const components = {
error: '#ef4444'
};
if (turns.length === 0) {
// Fallback to single progress bar if no turn data
return this.singleProgressBar(totalDuration);
}
let segments = '';
for (const turn of turns) {
const percentage = (turn.duration_secs / totalDuration) * 100;
// Handle zero total duration to avoid NaN
if (totalDuration === 0) {
continue;
}
// Ensure percentage never exceeds 100%
const rawPercentage = (turn.duration_secs / totalDuration) * 100;
const percentage = Math.min(rawPercentage, 100);
const color = colors[turn.agent] || colors.player;
const statusColor = turn.status === 'error' ? colors.error : color;
const agentLabel = turn.agent.charAt(0).toUpperCase() + turn.agent.slice(1);
const durationMin = Math.round(turn.duration_secs / 60);
const tooltip = `${agentLabel}: ${durationMin}m ${Math.round(turn.duration_secs % 60)}s - ${turn.status}`;
segments += `
<div class="progress-segment"
style="width: ${percentage}%; background-color: ${statusColor};"
title="${turn.agent}: ${turn.duration_secs}s - ${turn.status}">
title="${tooltip}">
</div>
`;
}
@@ -62,11 +83,28 @@ const components = {
</div>
`;
},
// Single progress bar (fallback)
singleProgressBar(duration) {
// Handle zero duration
if (duration === 0) {
return `<div class="progress-bar"><div class="progress-fill" style="width: 0%"></div><span class="progress-text">Starting...</span></div>`;
}
const estimated = duration * 1.5;
const progress = Math.min((duration / estimated) * 100, 100);
return `
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress}%"></div>
<span class="progress-text">${Math.round(duration / 60)}m elapsed</span>
</div>
`;
},
// Render instance panel
instancePanel(instance, stats, latestMessage) {
return `
<div class="instance-panel" data-id="${instance.id}" onclick="router.navigate('/instance/${instance.id}')">
<div class="instance-panel" data-id="${instance.id}" onclick="event.preventDefault(); event.stopPropagation(); window.router.navigate('/instance/${instance.id}')">
<div class="panel-header">
<div class="panel-title">
<h3>${instance.workspace}</h3>
@@ -155,10 +193,19 @@ const components = {
// Render chat message
chatMessage(message, agent = null) {
const agentClass = agent === 'coach' ? 'message-coach' : agent === 'player' ? 'message-player' : '';
// Handle agent as string or object
let agentStr = null;
if (typeof agent === 'string') {
agentStr = agent.toLowerCase();
} else if (agent && typeof agent === 'object') {
agentStr = String(agent).toLowerCase();
}
const agentClass = agentStr === 'coach' ? 'message-coach' : agentStr === 'player' ? 'message-player' : '';
return `
<div class="chat-message ${agentClass}">
${agent ? `<div class="message-agent">${agent}</div>` : ''}
${agentStr ? `<div class="message-agent">${agentStr}</div>` : ''}
<div class="message-content">${marked.parse(message)}</div>
</div>
`;
@@ -262,10 +309,12 @@ const components = {
<div class="project-file">
<div class="file-header" onclick="this.parentElement.classList.toggle('expanded')">
<span class="file-name">📄 requirements.md</span>
<button class="btn btn-sm btn-secondary" onclick="event.stopPropagation(); window.viewFullFile('requirements.md')" style="margin-left: auto; margin-right: 0.5rem;">View Full</button>
<span class="file-toggle">▼</span>
</div>
<div class="file-content">
<pre><code>${this.escapeHtml(projectFiles.requirements)}</code></pre>
<p class="text-muted" style="margin-top: 0.5rem; font-size: 0.875rem;">Showing first 10 lines...</p>
</div>
</div>
`;
@@ -276,10 +325,12 @@ const components = {
<div class="project-file">
<div class="file-header" onclick="this.parentElement.classList.toggle('expanded')">
<span class="file-name">📄 README.md</span>
<button class="btn btn-sm btn-secondary" onclick="event.stopPropagation(); window.viewFullFile('README.md')" style="margin-left: auto; margin-right: 0.5rem;">View Full</button>
<span class="file-toggle">▼</span>
</div>
<div class="file-content">
<pre><code>${this.escapeHtml(projectFiles.readme)}</code></pre>
<p class="text-muted" style="margin-top: 0.5rem; font-size: 0.875rem;">Showing first 10 lines...</p>
</div>
</div>
`;
@@ -290,10 +341,12 @@ const components = {
<div class="project-file">
<div class="file-header" onclick="this.parentElement.classList.toggle('expanded')">
<span class="file-name">📄 AGENTS.md</span>
<button class="btn btn-sm btn-secondary" onclick="event.stopPropagation(); window.viewFullFile('AGENTS.md')" style="margin-left: auto; margin-right: 0.5rem;">View Full</button>
<span class="file-toggle">▼</span>
</div>
<div class="file-content">
<pre><code>${this.escapeHtml(projectFiles.agents)}</code></pre>
<p class="text-muted" style="margin-top: 0.5rem; font-size: 0.875rem;">Showing first 10 lines...</p>
</div>
</div>
`;
@@ -309,3 +362,6 @@ const components = {
return div.innerHTML;
}
};
// Expose to window for global access
window.components = components;

View File

@@ -0,0 +1,164 @@
// File Browser Component
const fileBrowser = {
currentPath: '',
selectedPath: '',
mode: 'directory', // 'directory' or 'file'
callback: null,
init() {
const modal = document.getElementById('file-browser-modal');
const closeBtn = document.getElementById('file-browser-close');
const cancelBtn = document.getElementById('file-browser-cancel');
const selectBtn = document.getElementById('file-browser-select');
const parentBtn = document.getElementById('file-browser-parent');
closeBtn.addEventListener('click', () => this.close());
cancelBtn.addEventListener('click', () => this.close());
selectBtn.addEventListener('click', () => this.select());
parentBtn.addEventListener('click', () => this.goToParent());
// Close on overlay click
modal.querySelector('.modal-overlay').addEventListener('click', () => this.close());
},
async open(options = {}) {
this.mode = options.mode || 'directory';
this.callback = options.callback;
this.currentPath = options.initialPath || '/Users';
this.selectedPath = '';
// Update title
const title = this.mode === 'directory' ? 'Select Directory' : 'Select File';
document.getElementById('file-browser-title').textContent = title;
// Show modal
document.getElementById('file-browser-modal').classList.remove('hidden');
// Load initial directory
await this.loadDirectory(this.currentPath);
},
close() {
document.getElementById('file-browser-modal').classList.add('hidden');
this.callback = null;
},
select() {
if (this.selectedPath && this.callback) {
this.callback(this.selectedPath);
}
this.close();
},
async goToParent() {
const parts = this.currentPath.split('/').filter(p => p);
if (parts.length > 0) {
parts.pop();
const parentPath = '/' + parts.join('/');
await this.loadDirectory(parentPath);
}
},
async loadDirectory(path) {
const listContainer = document.getElementById('file-browser-list');
listContainer.innerHTML = '<div class="spinner-container"><div class="spinner"></div><p>Loading...</p></div>';
try {
const data = await api.browseFilesystem(path, this.mode);
this.currentPath = data.current_path;
this.selectedPath = this.mode === 'directory' ? this.currentPath : '';
// Update current path display
document.getElementById('file-browser-current-path').value = this.currentPath;
// Render items
this.renderItems(data.entries);
} catch (error) {
console.error('Failed to load directory:', error);
listContainer.innerHTML = `<div class="error-message">Failed to load directory: ${error.message}</div>`;
}
},
renderItems(entries) {
const listContainer = document.getElementById('file-browser-list');
if (entries.length === 0) {
listContainer.innerHTML = '<div style="padding: 2rem; text-align: center; color: var(--text-secondary);">Empty directory</div>';
return;
}
// Sort: directories first, then files, alphabetically
entries.sort((a, b) => {
if (a.is_dir !== b.is_dir) {
return a.is_dir ? -1 : 1;
}
return a.name.localeCompare(b.name);
});
let html = '';
for (const entry of entries) {
const icon = entry.is_dir ? '📁' : '📄';
const className = entry.is_dir ? 'directory' : 'file';
const isSelected = entry.path === this.selectedPath;
// Only show files if in file mode, always show directories
if (this.mode === 'file' && !entry.is_dir) {
html += `
<div class="file-browser-item ${className} ${isSelected ? 'selected' : ''}"
data-path="${entry.path}"
data-is-dir="${entry.is_dir}">
<span class="file-browser-icon">${icon}</span>
<span class="file-browser-name">${entry.name}</span>
</div>
`;
} else if (entry.is_dir) {
html += `
<div class="file-browser-item ${className} ${isSelected ? 'selected' : ''}"
data-path="${entry.path}"
data-is-dir="${entry.is_dir}">
<span class="file-browser-icon">${icon}</span>
<span class="file-browser-name">${entry.name}</span>
</div>
`;
}
}
listContainer.innerHTML = html;
// Add click handlers
listContainer.querySelectorAll('.file-browser-item').forEach(item => {
item.addEventListener('click', () => this.handleItemClick(item));
});
},
async handleItemClick(item) {
const path = item.dataset.path;
const isDir = item.dataset.isDir === 'true';
if (isDir) {
// Double-click to navigate into directory
if (this.selectedPath === path) {
await this.loadDirectory(path);
} else {
// Single click to select directory
this.selectedPath = path;
// Update UI
document.querySelectorAll('.file-browser-item').forEach(i => {
i.classList.remove('selected');
});
item.classList.add('selected');
}
} else {
// Select file
this.selectedPath = path;
// Update UI
document.querySelectorAll('.file-browser-item').forEach(i => {
i.classList.remove('selected');
});
item.classList.add('selected');
}
}
};
// Expose to window
window.fileBrowser = fileBrowser;

View File

@@ -1,26 +1,67 @@
// Simple client-side router
// Simple client-side router with proper state management
const router = {
currentRoute: '/',
refreshTimeout: null,
detailRefreshTimeout: null,
currentInstanceId: null,
initialized: false,
renderInProgress: false,
init() {
console.log('[Router] init() called');
if (this.initialized) {
console.log('[Router] Already initialized, skipping');
return;
}
this.initialized = true;
// Handle browser back/forward
window.addEventListener('popstate', () => {
console.log('[Router] popstate event');
this.handleRoute(window.location.pathname);
});
// Handle initial route
this.handleRoute(window.location.pathname);
// Handle initial route - call once after a short delay to ensure DOM is ready
setTimeout(() => {
console.log('[Router] Initial route handling');
this.handleRoute(window.location.pathname);
}, 100);
},
navigate(path) {
console.log('[Router] navigate:', path);
// Cancel any pending refreshes
this.cancelRefreshes();
window.history.pushState({}, '', path);
this.handleRoute(path);
},
cancelRefreshes() {
if (this.refreshTimeout) {
console.log('[Router] Cancelling home refresh timeout');
clearTimeout(this.refreshTimeout);
this.refreshTimeout = null;
}
if (this.detailRefreshTimeout) {
console.log('[Router] Cancelling detail refresh timeout');
clearTimeout(this.detailRefreshTimeout);
this.detailRefreshTimeout = null;
}
},
async handleRoute(path) {
this.currentRoute = path;
console.log('[Router] handleRoute:', path);
const container = document.getElementById('page-container');
if (!container) {
console.error('[Router] page-container not found!');
return;
}
// Cancel any pending refreshes when route changes
this.cancelRefreshes();
if (path === '/' || path === '') {
await this.renderHome(container);
} else if (path.startsWith('/instance/')) {
@@ -32,51 +73,87 @@ const router = {
},
async renderHome(container) {
container.innerHTML = components.spinner('Loading instances...');
console.log('[Router] renderHome called, renderInProgress:', this.renderInProgress);
// Prevent concurrent renders
if (this.renderInProgress) {
console.log('[Router] Render already in progress, skipping');
return;
}
this.renderInProgress = true;
try {
const instances = await api.getInstances();
console.log('[Router] Showing spinner');
container.innerHTML = components.spinner('Loading instances...');
if (instances.length === 0) {
container.innerHTML = components.emptyState(
'No running instances. Click "+ New Run" to start one.'
);
console.log('[Router] Fetching instances from API');
const instances = await api.getInstances();
console.log('[Router] Received', instances.length, 'instances');
// Check if we're still on the home route (user might have navigated away)
if (this.currentRoute !== '/' && this.currentRoute !== '') {
console.log('[Router] Route changed during fetch, aborting render');
return;
}
let html = '<div class="instances-list">';
for (const instance of instances) {
// Use stats from API response
const stats = instance.stats || { total_tokens: 0, tool_calls: 0, errors: 0, duration_secs: 0 };
html += components.instancePanel(instance, stats, instance.latest_message);
}
html += '</div>';
container.innerHTML = html;
// Auto-refresh every 5 seconds
setTimeout(() => {
if (this.currentRoute === '/') {
this.renderHome(container);
if (instances.length === 0) {
console.log('[Router] No instances, showing empty state');
container.innerHTML = components.emptyState(
'No running instances. Click "+ New Run" to start one.'
);
} else {
console.log('[Router] Building HTML for', instances.length, 'instances');
let html = '<div class="instances-list">';
for (const instance of instances) {
const stats = instance.stats || { total_tokens: 0, tool_calls: 0, errors: 0, duration_secs: 0 };
html += components.instancePanel(instance, stats, instance.latest_message);
}
}, 5000);
html += '</div>';
console.log('[Router] Setting innerHTML (', html.length, 'chars)');
container.innerHTML = html;
console.log('[Router] HTML set successfully');
}
// Schedule next refresh only if still on home route
if (this.currentRoute === '/' || this.currentRoute === '') {
console.log('[Router] Scheduling auto-refresh in 5 seconds');
this.refreshTimeout = setTimeout(() => {
console.log('[Router] Auto-refresh triggered');
this.renderHome(container);
}, 5000);
}
} catch (error) {
container.innerHTML = components.error(error.message);
console.error('[Router] Error in renderHome:', error);
container.innerHTML = components.error('Failed to load instances: ' + error.message);
} finally {
this.renderInProgress = false;
console.log('[Router] renderHome complete, renderInProgress reset to false');
}
},
async renderDetail(container, id) {
console.log('[Router] renderDetail called for', id);
this.currentInstanceId = id;
container.innerHTML = components.spinner('Loading instance details...');
try {
const instance = await api.getInstance(id);
const logs = await api.getInstanceLogs(id);
// Check if we're still on this detail route
if (this.currentRoute !== `/instance/${id}`) {
console.log('[Router] Route changed during fetch, aborting render');
return;
}
// Build detail view HTML
let html = `
<div class="detail-view">
<div class="detail-header">
<button class="btn btn-secondary" onclick="router.navigate('/')">&larr; Back</button>
<button class="btn btn-secondary" onclick="window.router.navigate('/')">&larr; Back</button>
<h2>${instance.workspace}</h2>
${components.statusBadge(instance.status)}
</div>
@@ -154,14 +231,56 @@ const router = {
hljs.highlightElement(block);
});
// Auto-refresh every 3 seconds
setTimeout(() => {
if (this.currentRoute === `/instance/${id}`) {
// Schedule next refresh only if still on this detail route
if (this.currentRoute === `/instance/${id}`) {
this.detailRefreshTimeout = setTimeout(() => {
this.renderDetail(container, id);
}
}, 3000);
}, 3000);
}
} catch (error) {
container.innerHTML = components.error(error.message);
console.error('[Router] Error in renderDetail:', error);
container.innerHTML = components.error('Failed to load instance: ' + error.message);
}
}
};
// Global function to view full file content
window.viewFullFile = async function(fileName) {
const modal = document.getElementById('full-file-modal');
const title = document.getElementById('full-file-title');
const content = document.getElementById('full-file-content');
// Show modal
modal.classList.remove('hidden');
title.textContent = fileName;
content.innerHTML = '<div class="spinner-container"><div class="spinner"></div><p>Loading...</p></div>';
try {
const instanceId = window.router.currentInstanceId;
if (!instanceId) {
throw new Error('No instance selected');
}
const data = await api.getFileContent(instanceId, fileName);
// Render full content with syntax highlighting
content.innerHTML = `<pre><code class="language-markdown">${components.escapeHtml(data.content)}</code></pre>`;
// Apply syntax highlighting
content.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
} catch (error) {
content.innerHTML = `<div class="error-message">Failed to load file: ${error.message}</div>`;
}
};
// Close full file modal
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('full-file-close')?.addEventListener('click', () => {
document.getElementById('full-file-modal').classList.add('hidden');
});
});
// Expose to window for global access
window.router = router;

View File

@@ -49,3 +49,6 @@ const state = {
this.save();
}
};
// Expose to window for global access
window.state = state;

View File

@@ -234,15 +234,22 @@ body {
height: 100%;
transition: width 0.3s;
cursor: help;
position: relative;
}
.progress-segment:not(:last-child) {
border-right: 2px solid var(--bg-primary);
}
.progress-segment:hover {
opacity: 0.8;
filter: brightness(1.1);
}
.progress-bar.ensemble .progress-text {
position: absolute;
z-index: 10;
pointer-events: none;
}
.progress-text {
@@ -324,6 +331,7 @@ body {
.modal-content {
position: relative;
z-index: 1001;
background-color: var(--bg-primary);
border-radius: 1rem;
max-width: 600px;
@@ -539,6 +547,11 @@ body {
color: var(--text-primary);
}
/* Detail content wrapper */
.detail-content {
margin-top: 2rem;
}
/* Chat View */
.chat-view {
margin-top: 2rem;
@@ -554,6 +567,8 @@ body {
display: flex;
flex-direction: column;
gap: 1rem;
max-height: 600px;
overflow-y: auto;
}
.chat-message {
@@ -820,3 +835,88 @@ body {
color: var(--text-secondary);
font-family: 'Monaco', 'Courier New', monospace;
}
/* File Browser */
.file-browser {
display: flex;
flex-direction: column;
gap: 1rem;
min-height: 400px;
}
.file-browser-path {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem;
background: var(--bg-secondary);
border-radius: 8px;
}
.file-browser-path label {
font-weight: 500;
white-space: nowrap;
}
.file-browser-path input {
flex: 1;
padding: 0.5rem;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 0.875rem;
}
.file-browser-list {
flex: 1;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--bg-secondary);
max-height: 400px;
}
.file-browser-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
cursor: pointer;
transition: background 0.2s;
border-bottom: 1px solid var(--border-color);
}
.file-browser-item:hover {
background: var(--bg-hover);
}
.file-browser-item.selected {
background: var(--primary-color);
color: white;
}
.file-browser-item.directory {
font-weight: 500;
}
.file-browser-item.file {
color: var(--text-secondary);
}
.file-browser-icon {
font-size: 1.25rem;
width: 1.5rem;
text-align: center;
}
.file-browser-name {
flex: 1;
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 0.875rem;
}
.file-browser-item:last-child {
border-bottom: none;
}