CoDA gives you and the AI agents inside it real authority over your Databricks workspace. By continuing, you confirm you understand:
+
+
Full workspace authority. Commands you run and AI agents you invoke act with your Databricks credentials.
+
AI agents take real actions. Claude, Codex, OpenCode, Gemini, and Hermes can read, write, and delete data on your behalf. Review their proposed actions before approving.
+
Actions are irreversible. There is no staging or undo — destructive commands run immediately.
+
You are accountable. Every API call is logged under your identity in the workspace audit log.
+
This app is for you alone. Do not share the URL, your screen, or your session with anyone you wouldn't hand your Databricks credentials to.
+
You are responsible. Code suggested by AI agents, scripts you paste, and commands you execute are your responsibility — including their effects on production data.
+
+
CoDA is provided AS-IS under the Databricks License. See project support notes. Anonymous event pings are sent to Databricks; opt out with CODA_TELEMETRY_DISABLED=true in app.yaml.
+
+
+
+
Keyboard Shortcuts
@@ -1471,6 +1525,58 @@
General
});
}
+ // Live progress helpers shared by both the PAT-bootstrap path and the
+ // already-configured-on-load path. \r\x1b[K rewrites the current line
+ // in place (CR + clear-to-end), so successive calls produce a single
+ // animated status line rather than scrolling output. Without these,
+ // the setup screen can sit silent for 30-60s and look broken.
+ const SPINNER = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
+ let spinFrame = 0;
+ let spinTimer = null;
+ const startSpinner = (label) => {
+ stopSpinner();
+ term.write('\r\x1b[K\x1b[36m ' + SPINNER[0] + '\x1b[0m \x1b[90m' + label + '\x1b[0m');
+ spinTimer = setInterval(() => {
+ spinFrame = (spinFrame + 1) % SPINNER.length;
+ term.write('\r\x1b[K\x1b[36m ' + SPINNER[spinFrame] + '\x1b[0m \x1b[90m' + label + '\x1b[0m');
+ }, 100);
+ };
+ const stopSpinner = () => {
+ if (spinTimer) { clearInterval(spinTimer); spinTimer = null; }
+ };
+ const finishLine = (emoji, color, msg) => {
+ stopSpinner();
+ term.write('\r\x1b[K' + color + ' ' + emoji + '\x1b[0m ' + msg + '\r\n');
+ };
+ // Polls /api/setup-status and rewrites the spinner line with the
+ // currently-running step + completion count. Returns when setup is
+ // complete or errored. Used by both bootstrap paths.
+ const waitForSetup = async () => {
+ startSpinner('Setting up CLI tools…');
+ while (true) {
+ await new Promise(r => setTimeout(r, 1500));
+ const pollResp = await fetch('/api/setup-status');
+ const pollData = await pollResp.json();
+
+ if (pollData.status === 'complete') {
+ finishLine('✓', '\x1b[1;32m', 'Setup complete');
+ term.write('\r\n');
+ return;
+ } else if (pollData.status === 'error') {
+ finishLine('!', '\x1b[1;33m', 'Setup completed with warnings');
+ term.write('\r\n');
+ return;
+ }
+ const steps = pollData.steps || [];
+ const running = steps.find(s => s.status === 'running');
+ const done = steps.filter(s => s.status === 'complete' || s.status === 'error').length;
+ const total = steps.length;
+ startSpinner(
+ (running ? running.label : 'Setting up CLI tools…') + ' (' + done + '/' + total + ')'
+ );
+ }
+ };
+
// Check if PAT is configured and valid before creating session
const patResp = await fetch('/api/pat-status');
const patData = await patResp.json();
@@ -1538,8 +1644,7 @@
// ── Version ──────────────────────────────────────────────────────
let appVersion = '0.0.0';
+ // ── First-run consent gate ─────────────────────────────────────
+ // Blocks init() on first load until the user acknowledges the trust
+ // model. Bump the '-v1' suffix on the key to re-prompt all users when
+ // the modal's content materially changes (new risk disclosures, new
+ // AI agents added, etc.) — old localStorage flags become invalid.
+ const CONSENT_KEY = 'coda:risks-accepted-v1';
+ function ensureConsent() {
+ if (localStorage.getItem(CONSENT_KEY) === 'true') return Promise.resolve();
+ return new Promise(resolve => {
+ const overlay = document.getElementById('consent-overlay');
+ const acceptBtn = document.getElementById('consent-accept');
+ overlay.classList.add('visible');
+ acceptBtn.focus();
+ acceptBtn.addEventListener('click', () => {
+ localStorage.setItem(CONSENT_KEY, 'true');
+ overlay.classList.remove('visible');
+ resolve();
+ }, { once: true });
+ });
+ }
+
// ── Init ───────────────────────────────────────────────────────
async function init() {
try {
+ // Block on first-run consent acceptance before any network calls,
+ // session creation, or PAT prompts. Resolves immediately on
+ // subsequent loads (localStorage flag set).
+ await ensureConsent();
+
// Fetch version inside async init() to avoid top-level await
try {
const vResp = await fetch('/api/version');
@@ -2154,12 +2257,15 @@
General
// Initialize WebSocket connection (falls back to HTTP polling if unavailable)
initWebSocket();
+ // Hide the global loading indicator BEFORE the terminal renders.
+ // createTab() may block for minutes during PAT prompt; leaving the
+ // status visible causes it to overlap the tab bar at top-left.
+ // The element is kept in the DOM for error reporting (see catch below).
+ status.style.display = 'none';
+
await createTab();
updateSessionBadge();
- status.textContent = 'Connected!';
- setTimeout(() => { status.style.display = 'none'; }, 1000);
-
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
@@ -2168,6 +2274,9 @@
General
window.addEventListener('beforeunload', () => cleanupAllPanes());
} catch (e) {
+ // We hid status before createTab() to avoid overlapping the tab bar
+ // during the PAT prompt. Re-show it here so the error is visible.
+ status.style.display = '';
status.textContent = 'Error: ' + e.message;
status.style.color = '#ff5555';
console.error(e);