feat: Tab-based terminal UI with optional tmux#38
feat: Tab-based terminal UI with optional tmux#38dgokeeffe wants to merge 40 commits intodatasciencemonkey:mainfrom
Conversation
Databricks Apps auto-provisions service principal credentials (DATABRICKS_CLIENT_ID/SECRET). This change adds dual-mode auth: if DATABRICKS_TOKEN is set, use PAT (existing behavior); otherwise, use the SP credentials to generate OAuth Bearer tokens on-the-fly. A background TokenRefresher thread refreshes OAuth tokens every 30 minutes and updates all agent config files (Claude, Gemini, Codex, OpenCode, Databricks CLI) with fresh tokens. Key changes: - utils.py: AuthMode enum, AuthState dataclass, resolve_auth(), TokenRefresher class, _update_all_token_files() - app.py: Wire up resolve_auth() in initialize_app(), remove OAuth credential stripping, inject fresh tokens into sessions - setup_databricks.py, sync_to_workspace.py: Remove PAT-only hardcoding, use SDK auto-detect - setup_claude/codex/gemini/opencode.py: Use resolve_databricks_host_and_token() instead of raw env vars - app.yaml: Make DATABRICKS_TOKEN optional with explanatory comment Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Multi-terminal UI with 4 layouts (single, hsplit, vsplit, quad) - Toolbar with layout buttons, pane indicators, focus management - Batch /api/output-batch endpoint for efficient multi-session polling - Git credential helper (~/.local/bin/git-credential-databricks) for HTTPS git auth using DATABRICKS_TOKEN - Ctrl+Shift+N to cycle focus, debounced resize, close/add pane buttons - 46 tests covering backend endpoints, credential helper, and frontend Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Install from dgokeeffe/opencode#feat/databricks-ai-sdk-provider which has built-in Databricks model serving support via @databricks/ai-sdk-provider. The native provider auto-discovers models from serving endpoints and handles auth through the full Databricks SDK credential chain (PAT, OAuth M2M, CLI). Replaces ~140 lines of hardcoded model configs and manual auth.json with a minimal 5-line config that just enables the databricks provider. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Credential helper now supports GIT_TOKEN + GIT_TOKEN_HOST for enterprise git providers (GitHub, Azure DevOps, GitLab), with DATABRICKS_TOKEN fallback - GIT_REPOS env var auto-clones repos into ~/projects/ at startup - Workspace sync is now opt-in via WORKSPACE_SYNC env var (default: off) - Fix pre-existing test issue: mock WorkspaceClient to avoid network calls Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Users can run `gh auth login` in the terminal to authenticate with GitHub via device flow — no PATs needed. gh is installed during setup alongside micro editor. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The arrow-key selection menu in `gh auth login` sends escape sequences that break in the xterm.js PTY. Wrapper script auto-adds `-h github.com -p https -w` to go straight to device flow. Also updates welcome message to reference gh auth login. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The xterm.js PTY sends OSC escape sequences that corrupt gh's interactive prompt library. Pipe "Y" to the git-credential prompt and add --skip-ssh-key to make auth fully non-interactive. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
xterm.js responds to shell/readline OSC queries (e.g. background color \e]11;?\e\) and sends the response through onData, which gets piped to the PTY as input, corrupting commands. Filter these out. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Write .bashrc with colored PS1 prompt, ls/grep color aliases - Write .bash_profile to source .bashrc for login shells - Start bash with --login so .bash_profile is sourced Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Install tmux during setup - Spawn tmux sessions (pane-0, pane-1, etc.) instead of bare bash - On page refresh, tmux reattaches to existing session preserving state - Write .tmux.conf with 256-color, mouse support, login shell - Frontend passes pane_id to /api/session for session mapping - Falls back to plain bash if tmux is unavailable Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Bump compute to Large (4 vCPU, 12GB) and threads to 32 - Add tmux session recovery across browser refreshes via /api/tmux-sessions - Implement adaptive polling (100ms focused, 500ms unfocused, 2s hidden) - Persist layout state in localStorage with automatic restore - Increase session timeout to 120s for better idle tolerance Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add workspace-backed state sync that saves Claude Code auto-memory
(~/.claude/projects/*/memory/) and shell history (~/.bash_history)
to /Workspace/Users/{email}/.state/. State is restored on startup,
saved every 5 minutes via background thread, and flushed on shutdown
via atexit handler. Enabled by default (STATE_SYNC=true).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Container runs as non-root so apt-get fails with permission denied. Use tmux-appimage release which can be extracted without root. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The extracted tmux binary depends on libevent/ncurses from the AppDir. Extract full AppDir and use AppRun wrapper which sets LD_LIBRARY_PATH. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
AppRun uses $APPDIR to locate bundled terminfo database. Without it, tmux fails with "missing or unsuitable terminal: xterm-256color". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add grid layouts for 6, 9, 12, 16, and 20 terminals. Auto-detect layout from tmux session count on reconnect. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
shutil.which("tmux") in list_tmux_sessions() and create_session()
used the server process PATH which didn't include ~/.local/bin.
Terminals were silently falling back to plain bash instead of tmux,
breaking session persistence across browser refreshes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
P0: Pin dependencies (requirements.txt), add GitHub Actions CI with ruff lint, bandit security scan, and dependency pin validation. Harden app.py: fix auth bypass (dev-mode gating), add rate limiter (token bucket), security headers (CSP, X-Frame-Options, etc.), session limits, input validation, resize bounds checking. Replace curl|bash in setup_claude.py with download-verify-execute. P1: Convert all print() to structured logging across setup scripts. Add timeout wrappers (30s) and file size guards (10MB) to state_sync. Set 0o600 permissions on token files in utils.py. Fix silent fetch failures and add ARIA accessibility to index.html. Fix DOM XSS (innerHTML→textContent) in loading.html. P2: Add VERSION file (0.1.0). Add Gunicorn access log format and graceful shutdown hook for state persistence. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove unused imports (os, pytest, tempfile, textwrap), fix f-strings without placeholders, remove unused variable, auto-format. Add ruff.toml to exclude .claude/ vendor skills from linting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Single-user app behind Databricks auth proxy — the token-bucket rate limiter (10 req/s) was throttling legitimate terminal I/O (output polling at 100ms + keystrokes exceeds 10 req/s per terminal). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add bashrc check that detects deleted CWD and resets to ~/projects. On tmux reattach, send cd command to refresh stale directory reference. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…upport # Conflicts: # README.md # app.py # app.yaml # requirements.txt # static/index.html
Databricks Apps injects both DATABRICKS_TOKEN and CLIENT_ID/SECRET. The Databricks SDK rejects ambiguous auth, breaking OpenCode's provider initialization. Strip OAuth vars when PAT is present. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The env var stripping in create_session() shell_env wasn't sufficient — tmux server preserves the original process env. Adding unset to .bashrc ensures every interactive shell strips DATABRICKS_CLIENT_ID/SECRET when PAT is configured. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Install OpenCode binary as _opencode_real with a shell wrapper that unsets DATABRICKS_CLIENT_ID/SECRET before exec. This fixes "No provider selected" caused by SDK rejecting dual auth config. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add cd $HOME to wrapper script to prevent getcwd() errors when opencode is launched from a deleted directory. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Hey David! Thanks for the work on PR #38 : the enterprise hardening stuff (tmux persistence, auth, security headers, state sync, git credential helper) is really solid. We've been working in parallel on a frontend overhaul on feat/multi-tab-terminals (PR #46) — tab-based UI, 8 themes, font picker, search, voice dictation, keyboard shortcuts modal, splash screen, etc. The two PRs conflict heavily on static/index.html since both rewrote the frontend. The backends are mostly compatible though. Your additions are additive to the existing Flask+PTY core. Would you be open to splitting #38 into two PRs?
That way we can merge your backend first, then rebase our frontend on top and get the best of both — your enterprise backend with our UX layer. Let me know what you think! |
datasciencemonkey
left a comment
There was a problem hiding this comment.
Auth: OAuth M2M concern
The M2M token refresh path worries me from a multi-user perspective. The typical failure mode we've seen:
- Team shares a single service principal + token across all users
- All terminal actions audit as the SP identity, not the individual user
- SP gets granted broad permissions to cover everyone's needs
- Token gets copied into
.envfiles, Slack, notebooks — token sprawl - Can't revoke one user's access without revoking everyone's
For CoDA specifically, the better pattern is OAuth on-behalf-of-user — Databricks Apps injects a short-lived DATABRICKS_TOKEN scoped to the logged-in user automatically. The databricks-sdk picks this up with zero config. Each user's actions are audited under their own identity and bounded by their existing UC permissions.
M2M is the right choice for headless/automated workloads (jobs, pipelines, CI/CD) — but for an interactive app with named users, delegated user identity is strictly better.
Suggestion: Can we gate the M2M path behind an explicit opt-in env var (e.g. CODA_USE_M2M=true) with a warning in the docs that shared SPs break per-user auditing? That way it's available for edge cases but not the default path users reach for.
- Agent setup scripts run in parallel via ThreadPoolExecutor (~5x faster) - WebSocket transport for terminal I/O with HTTP polling fallback - Socket.IO integration into enterprise LayoutManager/TerminalPane - Gunicorn timeout increased to 120s for WebSocket connections Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Install ai-dev-kit databricks-mcp-server (sparse clone + venv) as a parallel setup step. Configure all 3 MCP servers (deepwiki, exa, databricks) for both Claude Code and OpenCode. Add new config files to TokenRefresher so OAuth tokens stay fresh in MCP configs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Databricks CLI is already available in the terminal — agents can use it directly. Remove the ai-dev-kit MCP server to avoid unnecessary clone+venv overhead. Keep the two zero-install HTTP MCPs (deepwiki, exa) and add them to OpenCode alongside Claude Code. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When Databricks Apps proxy doesn't support WebSocket, SocketIO falls back to HTTP long-polling which has more overhead than simple POST. Now tracks real WebSocket vs long-polling transport and only uses SocketIO when true WebSocket is active. HTTP batch polling (50ms) handles output otherwise. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The /api/input endpoint now writes to PTY, waits 5ms for echo, drains the output buffer, and returns output in the response. Client renders the echo immediately instead of waiting for the next batch poll cycle, halving perceived keystroke latency. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Serve socket.io.min.js from /static/ instead of CDN to bypass Databricks Apps CSP (script-src 'self') that was blocking it - Batch keystrokes within one animation frame (~16ms) on HTTP fallback path to reduce round trips through Azure proxy Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both WebSocket and HTTP batch polling were delivering terminal output simultaneously, causing every keystroke to echo twice. Only start HTTP polling when WebSocket isn't active, and guard batchPoll() to skip when WebSocket is delivering output. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
TabManagerclass replaces the grid layout as the default experience. Clean tab bar with add (+), close (X), and keyboard cycling (Ctrl+Shift+N).TERMINAL_MODEenv var.TMUX_ENABLED=falsecreates raw PTY sessions without tmux. NewGET /api/active-sessionsendpoint enables session reconnection without tmux.Changes
app.py/api/active-sessionsendpoint,TMUX_ENABLEDgating in session creation,pane_idtrackingstatic/index.htmlTabManagerclass, tab bar CSS, mode toggle button (position:fixed),batchPollfiltering,restoreOrCreatefor non-tmux modeapp.yaml.templateTERMINAL_MODEandTMUX_ENABLEDenv varstests/test_tab_ui.pytests/test_active_sessions.pyTest plan
uv run pytest tests/gates/ -x)test_mlflow_tracing.py)🤖 Generated with Claude Code