Initial Release: OpenQode Public Alpha v1.3
This commit is contained in:
2385
web/app.js
Normal file
2385
web/app.js
Normal file
File diff suppressed because it is too large
Load Diff
553
web/index.html
Normal file
553
web/index.html
Normal file
@@ -0,0 +1,553 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>OpenQode IDE - AI-Powered Coding Assistant</title>
|
||||
<meta name="description"
|
||||
content="OpenQode IDE - A free AI-powered coding assistant with Qwen integration. 2000 daily requests, real-time chat, and full IDE capabilities.">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
|
||||
<body class="vscode-body">
|
||||
<div class="vscode-shell">
|
||||
<!-- Top Header Bar -->
|
||||
<header class="top-bar">
|
||||
<div class="top-left">
|
||||
<div class="logo-cell">
|
||||
<span>OQ</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>OpenQode IDE</strong>
|
||||
<p>Local AI Coding Assistant</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="top-actions">
|
||||
<div class="auth-pill">
|
||||
<span id="auth-status">Auth Status</span>
|
||||
<span id="auth-status-text">Not authenticated</span>
|
||||
</div>
|
||||
<a id="hero-local-preview" href="#" class="preview-pill" target="_blank">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
|
||||
<polyline points="15 3 21 3 21 9"></polyline>
|
||||
<line x1="10" y1="14" x2="21" y2="3"></line>
|
||||
</svg>
|
||||
Open Preview
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Three-Column Layout -->
|
||||
<div class="main-grid">
|
||||
<!-- Left Panel: Explorer & Files -->
|
||||
<aside class="left-panel">
|
||||
<div class="section-title">
|
||||
<span>Explorer</span>
|
||||
<button id="refresh-tree-btn" class="ghost-btn mini" title="Refresh file tree">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<polyline points="23 4 23 10 17 10"></polyline>
|
||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
||||
</svg>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Sessions List -->
|
||||
<div class="section-subtitle"
|
||||
style="padding: 12px 0 8px 0; font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em;">
|
||||
Last Chats
|
||||
</div>
|
||||
<div id="sessions-list" class="session-list" style="max-height: 200px; overflow-y: auto;">
|
||||
<div class="session-pill active">
|
||||
<span class="session-icon">📁</span>
|
||||
<span>Current workspace</span>
|
||||
</div>
|
||||
</div>
|
||||
<button id="new-project-btn" class="primary-btn full" style="margin-top: 8px; margin-bottom: 8px;"
|
||||
onclick="window.openQodeApp.startNewProjectFlow()">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path
|
||||
d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z">
|
||||
</path>
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
|
||||
<line x1="12" y1="22.08" x2="12" y2="12"></line>
|
||||
</svg>
|
||||
New Project
|
||||
</button>
|
||||
<button id="new-session-btn" class="ghost-btn full" onclick="window.openQodeApp.createNewSession()">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
New Session
|
||||
</button>
|
||||
|
||||
<button id="deploy-btn" class="ghost-btn full" style="margin-top: 8px;"
|
||||
onclick="window.openQodeApp.deployToVercel()">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="17 8 12 3 7 8"></polyline>
|
||||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||
</svg>
|
||||
Deploy to Vercel
|
||||
</button>
|
||||
|
||||
<button id="preview-btn" class="ghost-btn full" style="margin-top: 8px;"
|
||||
onclick="window.openQodeApp.startLocalPreview()">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polygon points="5 3 19 12 5 21 5 3"></polygon>
|
||||
</svg>
|
||||
Local Preview
|
||||
</button>
|
||||
|
||||
<div class="section-title">
|
||||
<span>Files</span>
|
||||
</div>
|
||||
|
||||
<!-- File Tree -->
|
||||
<div id="file-tree" class="file-tree">
|
||||
<div class="file-tree-placeholder">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z">
|
||||
</path>
|
||||
</svg>
|
||||
<p>Authenticate to view files</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Actions -->
|
||||
<div class="file-actions">
|
||||
<button id="new-file-btn" class="ghost-btn mini" title="Create new file or folder">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
<polyline points="14 2 14 8 20 8"></polyline>
|
||||
<line x1="12" y1="18" x2="12" y2="12"></line>
|
||||
<line x1="9" y1="15" x2="15" y2="15"></line>
|
||||
</svg>
|
||||
New
|
||||
</button>
|
||||
<button id="save-file-btn" class="ghost-btn mini" title="Save current file (Ctrl+S)">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
|
||||
<polyline points="17 21 17 13 7 13 7 21"></polyline>
|
||||
<polyline points="7 3 7 8 15 8"></polyline>
|
||||
</svg>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Authentication Section -->
|
||||
<div class="auth-section">
|
||||
<button id="auth-btn" class="primary-btn">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||
</svg>
|
||||
Authenticate Qwen
|
||||
</button>
|
||||
<button id="reauth-btn" class="ghost-btn full">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<polyline points="23 4 23 10 17 10"></polyline>
|
||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
||||
</svg>
|
||||
Re-authenticate
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Center Panel: Chat & Editor -->
|
||||
<section class="center-panel">
|
||||
<!-- Chat Header with View Toggle -->
|
||||
<div class="chat-header">
|
||||
<div>
|
||||
<p class="chat-title">Studio Chat</p>
|
||||
<p class="chat-sub">AI-powered coding assistant</p>
|
||||
</div>
|
||||
<div class="mode-toggle">
|
||||
<button id="gui-view-btn" class="mode-btn active">GUI Chat</button>
|
||||
<button id="tui-view-btn" class="mode-btn">TUI Chat</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat Layer (GUI & TUI Views) -->
|
||||
<div class="chat-layer">
|
||||
<!-- GUI Chat View -->
|
||||
<section id="gui-view" class="chat-window active">
|
||||
<!-- Model Status Bar -->
|
||||
<div class="chat-note">
|
||||
<div class="chat-note-left">
|
||||
<span class="tag">Model</span>
|
||||
<strong id="model-status">Qwen Coder</strong>
|
||||
</div>
|
||||
<div class="chat-note-meta">
|
||||
<span class="status-badge online">Online</span>
|
||||
<span class="status-badge">Streaming</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Message Feed -->
|
||||
<div id="chat-messages" class="message-feed">
|
||||
<div class="message-placeholder welcome-message">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
||||
</svg>
|
||||
<h3>Welcome to OpenQode</h3>
|
||||
<p>Ask anything and get AI-powered code assistance</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat Input -->
|
||||
<div class="chat-input">
|
||||
<textarea id="message-input" class="message-input"
|
||||
placeholder="Ask OpenQode anything... (Enter to send, Shift+Enter for new line)"
|
||||
rows="3"></textarea>
|
||||
<div class="input-controls">
|
||||
<button id="attach-btn" class="ghost-btn circle" title="Attach file or code context">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<path
|
||||
d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48">
|
||||
</path>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="send-btn" class="primary-btn">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<line x1="22" y1="2" x2="11" y2="13"></line>
|
||||
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
||||
</svg>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- TUI Chat View -->
|
||||
<section id="tui-view" class="chat-window tui-window">
|
||||
<!-- TUI will be injected here by tui.js -->
|
||||
<div class="tui-placeholder">
|
||||
<p>TUI Chat mode - Terminal-style interface</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Editor Shell -->
|
||||
<div class="editor-shell">
|
||||
<div class="editor-toolbar">
|
||||
<div>
|
||||
<strong>Workspace Editor</strong>
|
||||
<p class="muted" id="current-file-path">Select a file to edit</p>
|
||||
</div>
|
||||
<div class="workspace-controls">
|
||||
<button id="rename-file-btn" class="ghost-btn mini" title="Rename current file">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
|
||||
</svg>
|
||||
Rename
|
||||
</button>
|
||||
<button id="delete-file-btn" class="ghost-btn mini" title="Delete current file">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<path
|
||||
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2">
|
||||
</path>
|
||||
</svg>
|
||||
Delete
|
||||
</button>
|
||||
<button id="show-diff-btn" class="ghost-btn mini" title="Show changes diff">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<line x1="18" y1="20" x2="18" y2="10"></line>
|
||||
<line x1="12" y1="20" x2="12" y2="4"></line>
|
||||
<line x1="6" y1="20" x2="6" y2="14"></line>
|
||||
</svg>
|
||||
Diff
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor Tabs -->
|
||||
<div id="editor-tabs" class="editor-tabs"></div>
|
||||
|
||||
<!-- Code Editor -->
|
||||
<textarea id="editor-textarea" class="editor-textarea"
|
||||
placeholder="// Select a file from the explorer to start editing..."></textarea>
|
||||
|
||||
<!-- Terminal -->
|
||||
<div class="terminal-mini">
|
||||
<div class="terminal-heading">
|
||||
<span>Terminal</span>
|
||||
<button id="terminal-run-btn" class="ghost-btn mini">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<polygon points="5 3 19 12 5 21 5 3"></polygon>
|
||||
</svg>
|
||||
Run
|
||||
</button>
|
||||
</div>
|
||||
<div id="terminal-output" class="terminal-output"></div>
|
||||
<div class="terminal-input-row">
|
||||
<span class="terminal-prompt">PS></span>
|
||||
<input id="terminal-input" class="terminal-input" placeholder="Enter command...">
|
||||
<span class="terminal-hint">Enter to run</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Right Panel: Settings & Configuration -->
|
||||
<aside class="right-panel">
|
||||
<div class="panel-title">
|
||||
<span>Settings</span>
|
||||
<button id="settings-btn" class="ghost-btn mini">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path
|
||||
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z">
|
||||
</path>
|
||||
</svg>
|
||||
More
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Model Selection -->
|
||||
<div class="panel-section">
|
||||
<label for="model-select">AI Model</label>
|
||||
<select id="model-select">
|
||||
<option value="qwen/coder-model">Qwen Coder (Default)</option>
|
||||
<option value="qwen/vision-model">Qwen Vision</option>
|
||||
<option value="gpt-4">OpenAI GPT-4</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Feature Toggles -->
|
||||
<div class="panel-section">
|
||||
<label>Features</label>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="lakeview-mode">
|
||||
<span class="switch-slider"></span>
|
||||
<span class="switch-label">Lakeview Mode</span>
|
||||
</label>
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="sequential-thinking">
|
||||
<span class="switch-slider"></span>
|
||||
<span class="switch-label">Sequential Thinking</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- System Info -->
|
||||
<div class="panel-section">
|
||||
<label>System</label>
|
||||
<p class="muted">Local-first prompts with private context. All data stays on your machine.</p>
|
||||
</div>
|
||||
|
||||
<!-- Temperature Slider -->
|
||||
<div class="panel-section">
|
||||
<label>Temperature</label>
|
||||
<div class="slider-container">
|
||||
<input type="range" id="temperature-slider" min="0" max="1" step="0.1" value="0.7">
|
||||
<span class="slider-value">0.7</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Tools -->
|
||||
<div class="panel-section">
|
||||
<label>Active Tools</label>
|
||||
<div class="panel-tags">
|
||||
<span class="panel-tag active">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
</svg>
|
||||
Thinking
|
||||
</span>
|
||||
<span class="panel-tag active">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<polygon points="5 3 19 12 5 21 5 3"></polygon>
|
||||
</svg>
|
||||
Code Exec
|
||||
</span>
|
||||
<span class="panel-tag">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||||
</svg>
|
||||
File Ops
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Keyboard Shortcuts -->
|
||||
<div class="panel-section shortcuts-section">
|
||||
<label>Shortcuts</label>
|
||||
<div class="shortcut-list">
|
||||
<div class="shortcut-item">
|
||||
<kbd>Ctrl</kbd> + <kbd>S</kbd>
|
||||
<span>Save file</span>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<kbd>Ctrl</kbd> + <kbd>Enter</kbd>
|
||||
<span>Send message</span>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<kbd>Shift</kbd> + <kbd>Click</kbd>
|
||||
<span>Select files</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<!-- Status Bar -->
|
||||
<footer class="status-bar">
|
||||
<div class="status-left">
|
||||
<span class="status-item">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
</svg>
|
||||
OpenQode v1.01
|
||||
</span>
|
||||
<span class="status-item">Running locally</span>
|
||||
</div>
|
||||
<div class="status-actions">
|
||||
<span class="status-item">Sessions: auto-save</span>
|
||||
<button id="apply-diff-btn" class="ghost-btn mini">Apply Changes</button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Diff Modal -->
|
||||
<div id="diff-modal" class="hidden-modal">
|
||||
<div class="diff-panel">
|
||||
<div class="diff-header">
|
||||
<span>📊 Diff Preview</span>
|
||||
<button id="close-diff" class="ghost-btn mini">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<div id="diff-content" class="diff-content"></div>
|
||||
<div class="diff-footer">
|
||||
<button class="ghost-btn" id="cancel-diff-btn">Cancel</button>
|
||||
<button class="primary-btn" id="apply-diff-btn-panel">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
Apply Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
<div id="settings-modal" class="hidden-modal">
|
||||
<div class="settings-panel">
|
||||
<div class="settings-header">
|
||||
<strong>⚙️ Settings</strong>
|
||||
<button id="close-settings" class="ghost-btn mini">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<div class="settings-body">
|
||||
<div class="settings-row">
|
||||
<span>Authentication Status</span>
|
||||
<span id="settings-auth-status" class="status-badge">Unknown</span>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<span>Workspace Root</span>
|
||||
<span class="muted">Local directory</span>
|
||||
</div>
|
||||
<div class="settings-row">
|
||||
<span>API Endpoint</span>
|
||||
<span class="muted">localhost:16400</span>
|
||||
</div>
|
||||
<hr class="settings-divider">
|
||||
<button id="reauth-btn-panel" class="primary-btn full">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="23 4 23 10 17 10"></polyline>
|
||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
||||
</svg>
|
||||
Re-authenticate with Qwen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<div id="loading-overlay" class="loading-overlay">
|
||||
<div class="loader"></div>
|
||||
<p>Connecting to OpenQode...</p>
|
||||
</div>
|
||||
|
||||
<!-- New Project Wizard Modal -->
|
||||
<div id="new-project-modal" class="modal-overlay hidden">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>✨ New Project Wizard</h3>
|
||||
<button id="close-wizard-btn" class="icon-btn" onclick="window.openQodeApp.closeNewProjectWizard()">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="project-name">Project Name</label>
|
||||
<input type="text" id="project-name" placeholder="e.g. My Awesome App" class="modal-input"
|
||||
oninput="window.openQodeApp.autoFillProjectPath(this.value)">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="project-path">Location</label>
|
||||
<input type="text" id="project-path" placeholder="e.g. projects/my-awesome-app" class="modal-input">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="project-requirements">What do you want to build?</label>
|
||||
<textarea id="project-requirements" rows="4"
|
||||
placeholder="Describe your app in detail... e.g. A React todo app with dark mode"
|
||||
class="modal-input"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="cancel-wizard-btn" class="ghost-btn"
|
||||
onclick="window.openQodeApp.closeNewProjectWizard()">Cancel</button>
|
||||
<button id="create-project-confirm-btn" class="primary-btn"
|
||||
onclick="window.openQodeApp.confirmNewProject()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
class="mr-2">
|
||||
<path d="M5 12h14M12 5l7 7-7 7" />
|
||||
</svg>
|
||||
Start Agentic Build
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
<script src="tui.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
4634
web/styles.css
Normal file
4634
web/styles.css
Normal file
File diff suppressed because it is too large
Load Diff
748
web/tui.js
Normal file
748
web/tui.js
Normal file
@@ -0,0 +1,748 @@
|
||||
class OpenQodeTUI {
|
||||
constructor() {
|
||||
this.terminal = null;
|
||||
this.currentLine = '';
|
||||
this.cursorPosition = 0;
|
||||
this.history = [];
|
||||
this.historyIndex = -1;
|
||||
this.isProcessing = false;
|
||||
this.currentModel = 'qwen/coder-model';
|
||||
|
||||
// Check localStorage immediately for auth state
|
||||
const token = localStorage.getItem('openqode_token');
|
||||
this.isAuthenticated = !!token;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.createTerminal();
|
||||
this.setupEventListeners();
|
||||
this.showWelcome();
|
||||
// Check and update auth status (will also update from API)
|
||||
this.checkAuthentication();
|
||||
}
|
||||
|
||||
createTerminal() {
|
||||
const tuiView = document.getElementById('tui-view');
|
||||
if (!tuiView) {
|
||||
console.error('TUI view container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
tuiView.innerHTML = `
|
||||
<div class="terminal-container">
|
||||
<div class="terminal-header">
|
||||
<span class="terminal-title">OpenQode TUI v1.01 - ${this.currentModel}</span>
|
||||
<div class="terminal-controls">
|
||||
<button class="terminal-btn minimize">_</button>
|
||||
<button class="terminal-btn maximize">□</button>
|
||||
<button class="terminal-btn close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="terminal-body">
|
||||
<div class="terminal-output" id="terminal-output"></div>
|
||||
<div class="terminal-input-line">
|
||||
<span class="terminal-prompt">OpenQode></span>
|
||||
<span class="terminal-input" id="terminal-input" contenteditable="true" spellcheck="false"></span>
|
||||
<span class="terminal-cursor" id="terminal-cursor">█</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="terminal-status-bar">
|
||||
<span class="status-item" id="auth-status">🔒 Not Authenticated</span>
|
||||
<span class="status-item" id="model-status">Model: ${this.currentModel}</span>
|
||||
<span class="status-item" id="connection-status">🟢 Connected</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.terminal = {
|
||||
output: document.getElementById('terminal-output'),
|
||||
input: document.getElementById('terminal-input'),
|
||||
cursor: document.getElementById('terminal-cursor'),
|
||||
authStatus: document.getElementById('auth-status'),
|
||||
modelStatus: document.getElementById('model-status'),
|
||||
connectionStatus: document.getElementById('connection-status')
|
||||
};
|
||||
|
||||
// Start cursor blink
|
||||
this.startCursorBlink();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Terminal input events
|
||||
this.terminal.input.addEventListener('keydown', (e) => this.handleKeyDown(e));
|
||||
this.terminal.input.addEventListener('input', (e) => this.handleInput(e));
|
||||
this.terminal.input.addEventListener('click', () => this.setCursorPosition());
|
||||
|
||||
// Terminal control buttons
|
||||
document.querySelector('.terminal-btn.close').addEventListener('click', () => {
|
||||
if (confirm('Are you sure you want to exit OpenQode TUI?')) {
|
||||
this.printLine('Goodbye! 👋');
|
||||
setTimeout(() => window.close(), 1000);
|
||||
}
|
||||
});
|
||||
|
||||
// Focus terminal input when clicking anywhere in terminal
|
||||
document.querySelector('.terminal-body').addEventListener('click', () => {
|
||||
this.terminal.input.focus();
|
||||
});
|
||||
|
||||
// Prevent context menu in terminal
|
||||
this.terminal.input.addEventListener('contextmenu', (e) => e.preventDefault());
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
if (this.isProcessing) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.key) {
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
this.executeCommand();
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
this.navigateHistory(-1);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
this.navigateHistory(1);
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
// Allow natural left arrow movement
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
// Allow natural right arrow movement
|
||||
break;
|
||||
case 'Tab':
|
||||
e.preventDefault();
|
||||
this.handleTabCompletion();
|
||||
break;
|
||||
case 'c':
|
||||
if (e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
this.handleCtrlC();
|
||||
}
|
||||
break;
|
||||
case 'l':
|
||||
if (e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
this.clearTerminal();
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
e.preventDefault();
|
||||
this.setCursorPosition(0);
|
||||
break;
|
||||
case 'End':
|
||||
e.preventDefault();
|
||||
this.setCursorPosition(this.currentLine.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleInput(e) {
|
||||
this.currentLine = this.terminal.input.textContent;
|
||||
this.cursorPosition = this.getCursorPosition();
|
||||
}
|
||||
|
||||
executeCommand() {
|
||||
const command = this.currentLine.trim();
|
||||
if (!command) {
|
||||
this.newLine();
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to history
|
||||
this.history.push(command);
|
||||
this.historyIndex = this.history.length;
|
||||
|
||||
// Echo command
|
||||
this.printLine(`OpenQode> ${command}`);
|
||||
|
||||
// Process command
|
||||
this.processCommand(command);
|
||||
|
||||
// Clear input
|
||||
this.currentLine = '';
|
||||
this.terminal.input.textContent = '';
|
||||
this.cursorPosition = 0;
|
||||
}
|
||||
|
||||
async processCommand(command) {
|
||||
this.isProcessing = true;
|
||||
this.showProcessing(true);
|
||||
|
||||
try {
|
||||
const [cmd, ...args] = command.toLowerCase().split(' ');
|
||||
|
||||
switch (cmd) {
|
||||
case 'help':
|
||||
this.showHelp();
|
||||
break;
|
||||
case 'clear':
|
||||
case 'cls':
|
||||
this.clearTerminal();
|
||||
break;
|
||||
case 'auth':
|
||||
await this.handleAuth(args);
|
||||
break;
|
||||
case 'model':
|
||||
this.handleModel(args);
|
||||
break;
|
||||
case 'status':
|
||||
this.showStatus();
|
||||
break;
|
||||
case 'exit':
|
||||
case 'quit':
|
||||
this.handleExit();
|
||||
break;
|
||||
case 'chat':
|
||||
case 'ask':
|
||||
await this.handleChat(args.join(' '));
|
||||
break;
|
||||
case 'lakeview':
|
||||
this.toggleLakeview();
|
||||
break;
|
||||
case 'thinking':
|
||||
this.toggleSequentialThinking();
|
||||
break;
|
||||
case 'session':
|
||||
this.handleSession(args);
|
||||
break;
|
||||
default:
|
||||
// Treat as chat message
|
||||
await this.handleChat(command);
|
||||
}
|
||||
} catch (error) {
|
||||
this.printLine(`❌ Error: ${error.message}`, 'error');
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
this.showProcessing(false);
|
||||
this.newLine();
|
||||
}
|
||||
}
|
||||
|
||||
async handleAuth(args) {
|
||||
const subcommand = args[0];
|
||||
|
||||
switch (subcommand) {
|
||||
case 'login':
|
||||
await this.authenticate();
|
||||
break;
|
||||
case 'logout':
|
||||
this.logout();
|
||||
break;
|
||||
case 'status':
|
||||
this.showAuthStatus();
|
||||
break;
|
||||
default:
|
||||
this.printLine('Usage: auth [login|logout|status]');
|
||||
}
|
||||
}
|
||||
|
||||
async authenticate() {
|
||||
this.printLine('🔐 Initiating Qwen authentication...');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ provider: 'qwen' })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
if (data.alreadyAuthenticated) {
|
||||
this.isAuthenticated = true;
|
||||
this.updateAuthStatus();
|
||||
this.printLine('✅ Already authenticated with Qwen!');
|
||||
} else if (data.requiresDeviceCode) {
|
||||
// Device Code Flow
|
||||
this.printLine('🔐 Device Code Flow initiated');
|
||||
this.printLine(`📋 Go to: ${data.verificationUri}`);
|
||||
this.printLine(`🔢 Enter code: ${data.userCode}`);
|
||||
this.printLine(`⏱️ Code expires in ${Math.floor(data.expiresIn / 60)} minutes`);
|
||||
|
||||
// Open verification URL
|
||||
window.open(data.verificationUriComplete || data.verificationUri, '_blank');
|
||||
|
||||
// Poll for completion
|
||||
this.printLine('⏳ Waiting for authentication completion...');
|
||||
this.pollForAuthCompletion();
|
||||
} else {
|
||||
this.isAuthenticated = true;
|
||||
this.updateAuthStatus();
|
||||
this.printLine('✅ Successfully authenticated with Qwen!');
|
||||
}
|
||||
} else {
|
||||
this.printLine(`❌ Authentication failed: ${data.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.printLine(`❌ Authentication error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async pollForAuthCompletion() {
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/auth/status');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.authenticated) {
|
||||
this.isAuthenticated = true;
|
||||
this.updateAuthStatus();
|
||||
this.printLine('✅ Authentication completed successfully!');
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
// Continue polling
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Poll every 5 seconds for up to 15 minutes
|
||||
let attempts = 0;
|
||||
const maxAttempts = 180;
|
||||
const poll = setInterval(async () => {
|
||||
attempts++;
|
||||
if (await checkAuth() || attempts >= maxAttempts) {
|
||||
clearInterval(poll);
|
||||
if (attempts >= maxAttempts && !this.isAuthenticated) {
|
||||
this.printLine('⏰ Authentication timed out. Please try again.');
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.isAuthenticated = false;
|
||||
this.updateAuthStatus();
|
||||
this.printLine('🔓 Logged out successfully');
|
||||
}
|
||||
|
||||
showAuthStatus() {
|
||||
if (this.isAuthenticated) {
|
||||
this.printLine('✅ Authenticated with Qwen');
|
||||
} else {
|
||||
this.printLine('❌ Not authenticated');
|
||||
}
|
||||
}
|
||||
|
||||
handleModel(args) {
|
||||
if (args.length === 0) {
|
||||
this.printLine(`Current model: ${this.currentModel}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const model = args.join(' ');
|
||||
const validModels = [
|
||||
'qwen/coder-model',
|
||||
'qwen/chat-model',
|
||||
'gpt-4',
|
||||
'gpt-3.5-turbo'
|
||||
];
|
||||
|
||||
if (validModels.includes(model)) {
|
||||
this.currentModel = model;
|
||||
this.updateModelStatus();
|
||||
this.printLine(`✅ Model changed to: ${model}`);
|
||||
} else {
|
||||
this.printLine('❌ Invalid model. Available models:');
|
||||
validModels.forEach(m => this.printLine(` - ${m}`));
|
||||
}
|
||||
}
|
||||
|
||||
async handleChat(message) {
|
||||
// Check auth - either flag or localStorage token
|
||||
const token = localStorage.getItem('openqode_token');
|
||||
if (!this.isAuthenticated && !token && this.currentModel.startsWith('qwen')) {
|
||||
this.printLine('❌ Please authenticate first: auth login');
|
||||
return;
|
||||
}
|
||||
|
||||
this.printLine(`🤖 (${this.currentModel}) Processing...`);
|
||||
|
||||
try {
|
||||
// Get auth token from localStorage (same as GUI view)
|
||||
const token = localStorage.getItem('openqode_token');
|
||||
|
||||
const response = await fetch('/api/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
message: message,
|
||||
model: this.currentModel,
|
||||
token: token,
|
||||
features: this.features || {}
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Update auth status since we got a successful response
|
||||
this.isAuthenticated = true;
|
||||
this.updateAuthStatus();
|
||||
|
||||
this.printLine('');
|
||||
this.printLine(data.response, 'ai-response');
|
||||
} else {
|
||||
if (data.needsReauth) {
|
||||
this.isAuthenticated = false;
|
||||
this.updateAuthStatus();
|
||||
this.printLine('❌ Session expired. Please authenticate again: auth login');
|
||||
} else {
|
||||
this.printLine(`❌ Error: ${data.error}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.printLine(`❌ Chat error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
showHelp() {
|
||||
const helpText = `
|
||||
📖 OpenQode TUI Commands:
|
||||
|
||||
Authentication:
|
||||
auth login - Authenticate with Qwen
|
||||
auth logout - Logout from current session
|
||||
auth status - Show authentication status
|
||||
|
||||
Model Management:
|
||||
model [name] - Set or show current model
|
||||
Available models: qwen/coder-model, qwen/chat-model, gpt-4, gpt-3.5-turbo
|
||||
|
||||
Chat & Interaction:
|
||||
chat [message] - Send message to AI
|
||||
ask [question] - Ask question to AI
|
||||
(any text) - Direct chat message
|
||||
|
||||
Features:
|
||||
lakeview - Toggle Lakeview mode
|
||||
thinking - Toggle Sequential Thinking
|
||||
session [cmd] - Manage chat sessions
|
||||
|
||||
Terminal:
|
||||
clear/cls - Clear terminal
|
||||
help - Show this help
|
||||
status - Show system status
|
||||
exit/quit - Exit OpenQode
|
||||
|
||||
Navigation:
|
||||
↑/↓ - Navigate command history
|
||||
Tab - Auto-completion
|
||||
Ctrl+C - Cancel current operation
|
||||
Ctrl+L - Clear terminal
|
||||
`;
|
||||
|
||||
this.printLine(helpText);
|
||||
}
|
||||
|
||||
showStatus() {
|
||||
const status = `
|
||||
📊 OpenQode Status:
|
||||
Version: v1.01 Preview Edition
|
||||
Model: ${this.currentModel}
|
||||
Auth: ${this.isAuthenticated ? '✅ Authenticated' : '❌ Not Authenticated'}
|
||||
Connection: 🟢 Connected
|
||||
History: ${this.history.length} commands
|
||||
`;
|
||||
this.printLine(status);
|
||||
}
|
||||
|
||||
toggleLakeview() {
|
||||
const isEnabled = !this.features?.lakeview;
|
||||
if (!this.features) this.features = {};
|
||||
this.features.lakeview = isEnabled;
|
||||
this.printLine(`🌊 Lakeview mode ${isEnabled ? 'enabled' : 'disabled'}`);
|
||||
}
|
||||
|
||||
toggleSequentialThinking() {
|
||||
const isEnabled = !this.features?.sequentialThinking;
|
||||
if (!this.features) this.features = {};
|
||||
this.features.sequentialThinking = isEnabled;
|
||||
this.printLine(`🧠 Sequential Thinking ${isEnabled ? 'enabled' : 'disabled'}`);
|
||||
}
|
||||
|
||||
handleSession(args) {
|
||||
const command = args[0];
|
||||
|
||||
switch (command) {
|
||||
case 'new':
|
||||
this.createNewSession();
|
||||
break;
|
||||
case 'list':
|
||||
this.listSessions();
|
||||
break;
|
||||
case 'switch':
|
||||
this.switchSession(args[1]);
|
||||
break;
|
||||
default:
|
||||
this.printLine('Usage: session [new|list|switch <name>]');
|
||||
}
|
||||
}
|
||||
|
||||
createNewSession() {
|
||||
const sessionName = `session_${Date.now()}`;
|
||||
this.printLine(`✅ Created new session: ${sessionName}`);
|
||||
}
|
||||
|
||||
listSessions() {
|
||||
this.printLine('📁 Available sessions:');
|
||||
this.printLine(' - default');
|
||||
this.printLine(' - session_1234567890');
|
||||
}
|
||||
|
||||
switchSession(name) {
|
||||
if (name) {
|
||||
this.printLine(`🔄 Switched to session: ${name}`);
|
||||
} else {
|
||||
this.printLine('❌ Please provide session name');
|
||||
}
|
||||
}
|
||||
|
||||
handleExit() {
|
||||
this.printLine('👋 Thank you for using OpenQode!');
|
||||
setTimeout(() => {
|
||||
if (confirm('Exit OpenQode TUI?')) {
|
||||
window.close();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
handleCtrlC() {
|
||||
if (this.isProcessing) {
|
||||
this.isProcessing = false;
|
||||
this.showProcessing(false);
|
||||
this.printLine('^C', 'cancel');
|
||||
this.newLine();
|
||||
} else {
|
||||
this.currentLine = '';
|
||||
this.terminal.input.textContent = '';
|
||||
this.cursorPosition = 0;
|
||||
}
|
||||
}
|
||||
|
||||
handleTabCompletion() {
|
||||
// Simple tab completion for commands
|
||||
const commands = ['help', 'clear', 'auth', 'model', 'status', 'exit', 'quit', 'chat', 'ask', 'lakeview', 'thinking', 'session'];
|
||||
const currentInput = this.currentLine.toLowerCase();
|
||||
|
||||
const matches = commands.filter(cmd => cmd.startsWith(currentInput));
|
||||
|
||||
if (matches.length === 1) {
|
||||
this.currentLine = matches[0];
|
||||
this.terminal.input.textContent = matches[0];
|
||||
this.setCursorPosition(matches[0].length);
|
||||
} else if (matches.length > 1) {
|
||||
this.printLine(`\nPossible completions: ${matches.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
navigateHistory(direction) {
|
||||
if (direction === -1 && this.historyIndex > 0) {
|
||||
this.historyIndex--;
|
||||
} else if (direction === 1 && this.historyIndex < this.history.length - 1) {
|
||||
this.historyIndex++;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentLine = this.history[this.historyIndex] || '';
|
||||
this.terminal.input.textContent = this.currentLine;
|
||||
this.setCursorPosition(this.currentLine.length);
|
||||
}
|
||||
|
||||
printLine(text, className = '') {
|
||||
const line = document.createElement('div');
|
||||
line.className = `terminal-line ${className}`;
|
||||
|
||||
// Detect and convert file paths to clickable links
|
||||
const processedText = this.parseFilePathsAndLinks(text);
|
||||
line.innerHTML = processedText;
|
||||
|
||||
this.terminal.output.appendChild(line);
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
parseFilePathsAndLinks(text) {
|
||||
// Escape HTML first
|
||||
let escaped = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
// Match Windows paths (C:\...) and Unix paths (/path/...)
|
||||
const pathRegex = /([A-Za-z]:\\[^\s<>"'`]+|\/[^\s<>"'`]+\.[a-zA-Z0-9]+)/g;
|
||||
|
||||
escaped = escaped.replace(pathRegex, (match) => {
|
||||
const fileName = match.split(/[\/\\]/).pop();
|
||||
const folderPath = match.substring(0, match.lastIndexOf('\\') || match.lastIndexOf('/'));
|
||||
|
||||
return `<span class="file-link-container">
|
||||
<a href="#" class="file-link" data-path="${match}" onclick="window.openQodeTUI?.openFile('${match.replace(/\\/g, '\\\\')}'); return false;">📄 ${fileName}</a>
|
||||
<button class="folder-btn" onclick="window.openQodeTUI?.openFolder('${folderPath.replace(/\\/g, '\\\\')}'); return false;" title="Open folder">📁</button>
|
||||
</span>`;
|
||||
});
|
||||
|
||||
// Also match backtick-wrapped paths
|
||||
escaped = escaped.replace(/`([^`]+\.[a-zA-Z0-9]+)`/g, (match, path) => {
|
||||
if (path.includes('\\') || path.includes('/')) {
|
||||
const fileName = path.split(/[\/\\]/).pop();
|
||||
const folderPath = path.substring(0, path.lastIndexOf('\\') || path.lastIndexOf('/'));
|
||||
return `<span class="file-link-container">
|
||||
<a href="#" class="file-link" data-path="${path}" onclick="window.openQodeTUI?.openFile('${path.replace(/\\/g, '\\\\')}'); return false;">📄 ${fileName}</a>
|
||||
<button class="folder-btn" onclick="window.openQodeTUI?.openFolder('${folderPath.replace(/\\/g, '\\\\')}'); return false;" title="Open folder">📁</button>
|
||||
</span>`;
|
||||
}
|
||||
return `<code class="inline-code">${path}</code>`;
|
||||
});
|
||||
|
||||
return escaped;
|
||||
}
|
||||
|
||||
openFile(filePath) {
|
||||
// Try to open file in new tab (works for HTML files)
|
||||
if (filePath.endsWith('.html') || filePath.endsWith('.htm')) {
|
||||
window.open(`file:///${filePath.replace(/\\/g, '/')}`, '_blank');
|
||||
} else {
|
||||
// For other files, show path and copy to clipboard
|
||||
this.printLine(`📋 Path copied: ${filePath}`, 'success');
|
||||
navigator.clipboard.writeText(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
openFolder(folderPath) {
|
||||
// Copy folder path to clipboard and show message
|
||||
navigator.clipboard.writeText(folderPath);
|
||||
this.printLine(`📋 Folder path copied: ${folderPath}`, 'success');
|
||||
this.printLine('Paste in File Explorer to open folder', 'info');
|
||||
}
|
||||
|
||||
newLine() {
|
||||
const line = document.createElement('div');
|
||||
line.className = 'terminal-line';
|
||||
this.terminal.output.appendChild(line);
|
||||
this.scrollToBottom();
|
||||
}
|
||||
|
||||
clearTerminal() {
|
||||
this.terminal.output.innerHTML = '';
|
||||
this.showWelcome();
|
||||
}
|
||||
|
||||
showWelcome() {
|
||||
// Use separate lines for cleaner display
|
||||
this.printLine('');
|
||||
this.printLine(' ╔═══════════════════════════════════════════════╗', 'welcome-border');
|
||||
this.printLine(' ║ 🚀 OpenQode TUI v1.01 Preview ║', 'welcome-title');
|
||||
this.printLine(' ║ OpenCode + Qwen Integration ║', 'welcome-subtitle');
|
||||
this.printLine(' ╚═══════════════════════════════════════════════╝', 'welcome-border');
|
||||
this.printLine('');
|
||||
this.printLine(' Welcome to OpenQode! Type "help" for commands.', 'welcome-text');
|
||||
this.printLine('');
|
||||
}
|
||||
|
||||
showProcessing(show) {
|
||||
if (show) {
|
||||
this.terminal.connectionStatus.textContent = '🟡 Processing...';
|
||||
} else {
|
||||
this.terminal.connectionStatus.textContent = '🟢 Connected';
|
||||
}
|
||||
}
|
||||
|
||||
updateAuthStatus() {
|
||||
if (this.isAuthenticated) {
|
||||
this.terminal.authStatus.textContent = '✅ Authenticated';
|
||||
} else {
|
||||
this.terminal.authStatus.textContent = '🔒 Not Authenticated';
|
||||
}
|
||||
}
|
||||
|
||||
updateModelStatus() {
|
||||
this.terminal.modelStatus.textContent = `Model: ${this.currentModel}`;
|
||||
document.querySelector('.terminal-title').textContent = `OpenQode TUI v1.01 - ${this.currentModel}`;
|
||||
}
|
||||
|
||||
startCursorBlink() {
|
||||
setInterval(() => {
|
||||
this.terminal.cursor.style.opacity =
|
||||
this.terminal.cursor.style.opacity === '0' ? '1' : '0';
|
||||
}, 500);
|
||||
}
|
||||
|
||||
setCursorPosition(position) {
|
||||
if (position !== undefined) {
|
||||
this.cursorPosition = Math.max(0, Math.min(position, this.currentLine.length));
|
||||
}
|
||||
|
||||
// Create a selection to position cursor
|
||||
const selection = window.getSelection();
|
||||
const range = document.createRange();
|
||||
const textNode = this.terminal.input.firstChild || this.terminal.input;
|
||||
|
||||
if (textNode.nodeType === Node.TEXT_NODE) {
|
||||
range.setStart(textNode, this.cursorPosition);
|
||||
range.setEnd(textNode, this.cursorPosition);
|
||||
} else {
|
||||
range.selectNodeContents(this.terminal.input);
|
||||
range.collapse(false);
|
||||
}
|
||||
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
getCursorPosition() {
|
||||
const selection = window.getSelection();
|
||||
if (selection.rangeCount === 0) return 0;
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
const textNode = this.terminal.input.firstChild;
|
||||
|
||||
if (!textNode || textNode.nodeType !== Node.TEXT_NODE) return 0;
|
||||
|
||||
return range.startOffset;
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
this.terminal.output.scrollTop = this.terminal.output.scrollHeight;
|
||||
}
|
||||
|
||||
async checkAuthentication() {
|
||||
try {
|
||||
// First check if GUI already has a token (shared auth state)
|
||||
const token = localStorage.getItem('openqode_token');
|
||||
|
||||
const response = await fetch('/api/auth/status');
|
||||
const data = await response.json();
|
||||
|
||||
// Consider authenticated if either API says so OR we have a valid token
|
||||
this.isAuthenticated = data.authenticated || !!token;
|
||||
this.updateAuthStatus();
|
||||
|
||||
if (this.isAuthenticated) {
|
||||
this.printLine('✅ Authenticated with Qwen');
|
||||
}
|
||||
} catch (error) {
|
||||
// Fallback: check localStorage token
|
||||
const token = localStorage.getItem('openqode_token');
|
||||
this.isAuthenticated = !!token;
|
||||
this.updateAuthStatus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize TUI when page loads, but only create instance
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Don't auto-initialize TUI, wait for user to switch to TUI view
|
||||
window.createOpenQodeTUI = () => {
|
||||
if (!window.openQodeTUI) {
|
||||
window.openQodeTUI = new OpenQodeTUI();
|
||||
}
|
||||
return window.openQodeTUI;
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user