Spaces:
Paused
Paused
| """smolcode — CLI-parity web UI over the Rust engine.""" | |
| from __future__ import annotations | |
| import json | |
| import os | |
| from dataclasses import dataclass, field | |
| from pathlib import Path | |
| import gradio as gr | |
| from engine import Router, load_preset | |
| from engine.config import ( | |
| Preset, | |
| Tier, | |
| is_specialty_model, | |
| parse_size_b, | |
| specialist_sizes, | |
| ) | |
| from engine.branding import SMOLCODE_CSS | |
| from engine.gradio_shell import ( | |
| AppSessionState, | |
| SlashResult, | |
| UiSettings, | |
| dispatch_slash, | |
| parse_input, | |
| ) | |
| from engine.preflight import list_models | |
| from engine.router import RouteResult | |
| from engine.rust_session import ( | |
| RustSession, | |
| apply_settings, | |
| get_session_chat, | |
| git_status, | |
| list_background_jobs, | |
| load_rust_config, | |
| parse_session_label, | |
| session_choices, | |
| workspace_paths, | |
| AUTOCOMPLETE_FILE_LIMIT, | |
| UI_FILE_LIMIT, | |
| ) | |
| from engine.trace import build_trace, save_trace | |
| from engine.themes import theme_at | |
| from engine.web_tui import ( | |
| Transcript, | |
| agent_choices, | |
| cycle_agent, | |
| cycle_mode, | |
| cycle_model, | |
| cycle_think, | |
| header_bar_html, | |
| help_overlay_html, | |
| host_from_url, | |
| ingest_agent_event, | |
| parse_git_header, | |
| render_picker_html, | |
| render_sidebar_html, | |
| shell_theme_html, | |
| slash_commands, | |
| status_bar_html, | |
| theme_picker_items, | |
| whichkey_overlay_html, | |
| ) | |
| PRESET = load_preset() | |
| _JS_HEAD = (Path(__file__).parent / "static" / "web_tui.js").read_text() | |
| class WebUiState: | |
| sidebar_visible: bool = True | |
| sidebar_view: str = "files" | |
| sidebar_sel: int = 0 | |
| theme_idx: int = 0 | |
| overlay: str = "" | |
| picker_kind: str = "" | |
| picker_items: list[str] = field(default_factory=list) | |
| picker_sel: int = 0 | |
| file_total: int = 0 | |
| # Blocking startup model pick: true until the user chooses from the modal. | |
| needs_model_pick: bool = True | |
| def _normalize_paths(files: list[str] | dict[str, str] | None) -> list[str]: | |
| if not files: | |
| return [] | |
| if isinstance(files, dict): | |
| paths = sorted(files.keys()) | |
| else: | |
| paths = sorted(files) | |
| return paths[:UI_FILE_LIMIT] | |
| def _cfg() -> dict: | |
| return load_rust_config() | |
| def _ensure_rust(app_state: AppSessionState, settings: UiSettings) -> RustSession: | |
| if app_state.rust is None: | |
| app_state.rust = RustSession( | |
| workspace=settings.workspace, | |
| agent=settings.agent, | |
| yolo=settings.yolo, | |
| model=_pinned_model(settings.model), # None for Auto -> router sets it | |
| base_url=_cfg().get("base_url"), | |
| approval_handler=app_state.approval.ask, | |
| ) | |
| apply_settings(app_state.rust, settings) | |
| return app_state.rust | |
| # --- curated model picker (Auto-first, <=32B, specialty fine-tunes collapsed) ------- | |
| # Each row is (label, model, think). model "auto"/"auto:<size>" are router pseudo-tags | |
| # interpreted by engine/router.py + rust_session.apply_settings; think "off" means the | |
| # router derives the level. | |
| _AUTO_ENTRIES: list[tuple[str, str, str]] = [ | |
| ("Auto", "auto", "off"), | |
| ("Auto · think low", "auto", "low"), | |
| ("Auto · think high", "auto", "high"), | |
| ("Auto · think xtra", "auto", "xtra"), | |
| ] | |
| def _model_entries() -> list[tuple[str, str, str]]: | |
| """All picker rows: Auto options, one Auto·<size> per served specialist size, then | |
| generic concrete models filtered to <=32B with the per-specialty fine-tunes hidden.""" | |
| entries = list(_AUTO_ENTRIES) | |
| for sz in specialist_sizes(PRESET): | |
| entries.append((f"Auto · {sz.upper()}", f"auto:{sz}", "off")) | |
| seen: set[str] = set() | |
| base = [t.model for t in PRESET.tiers if t.model] | |
| api = list_models(_cfg().get("base_url", PRESET.base_url)) | |
| for m in api + base: | |
| if not m or m in seen or is_specialty_model(m) or parse_size_b(m) > 32: | |
| continue | |
| seen.add(m) | |
| entries.append((m, m, "off")) | |
| return entries | |
| def _model_labels() -> list[str]: | |
| return [lbl for lbl, _m, _t in _model_entries()] | |
| def _label_to_selection(label: str) -> tuple[str, str] | None: | |
| """(model, think) for a picker label, or None if unknown.""" | |
| for lbl, m, t in _model_entries(): | |
| if lbl == label: | |
| return m, t | |
| return None | |
| def _model_sel_index(settings: UiSettings) -> int: | |
| """Row index matching the current (model, think); falls back to 0 (Auto).""" | |
| entries = _model_entries() | |
| cur_m = settings.model or "auto" | |
| cur_t = settings.think or "off" | |
| for i, (_l, m, t) in enumerate(entries): # exact (model, think) wins | |
| if m == cur_m and t == cur_t: | |
| return i | |
| for i, (_l, m, _t) in enumerate(entries): # else first model match | |
| if m == cur_m: | |
| return i | |
| return 0 | |
| def _selection_label(settings: UiSettings) -> str: | |
| """Friendly label for the current selection (model chip in header/status).""" | |
| entries = _model_entries() | |
| return entries[_model_sel_index(settings)][0] if entries else "Auto" | |
| def _pinned_model(model_sel: str | None) -> str | None: | |
| """The concrete model tag to pin, or None for Auto/Auto·size (router-driven).""" | |
| m = model_sel or "" | |
| return None if (not m or m == "auto" or m.startswith("auto:")) else m | |
| def _effective_preset(model_sel: str | None): | |
| """(preset, size_floor) for a picker selection. | |
| 'auto' -> matrix preset (router picks size); 'auto:<size>' -> matrix + start pinned | |
| to that size (still escalates); '<tag>' -> single-tier preset (pinned, no escalation). | |
| """ | |
| sel = model_sel or "auto" | |
| if sel == "auto": | |
| return PRESET, None | |
| if sel.startswith("auto:"): | |
| return PRESET, (sel.split(":", 1)[1] or None) | |
| return ( | |
| Preset(key=PRESET.key, base_url=PRESET.base_url, api_key=PRESET.api_key, | |
| tiers=[Tier("custom", sel)]), | |
| None, | |
| ) | |
| def _picker_items(kind: str, settings: UiSettings) -> list[str]: | |
| if kind == "models": | |
| return _model_labels() | |
| if kind == "themes": | |
| return theme_picker_items() | |
| if kind == "agents": | |
| return agent_choices() | |
| if kind == "sessions": | |
| return session_choices() | |
| return [] | |
| def _picker_sel_for(kind: str, settings: UiSettings, ui: WebUiState, items: list[str]) -> int: | |
| if not items: | |
| return 0 | |
| if kind == "models": | |
| return _model_sel_index(settings) | |
| if kind == "themes": | |
| name = theme_at(ui.theme_idx).name | |
| return items.index(name) if name in items else 0 | |
| if kind == "agents": | |
| cur = settings.agent if settings.mode != "plan" else "plan" | |
| return items.index(cur) if cur in items else 0 | |
| return 0 | |
| def _header(settings: UiSettings, ui: WebUiState) -> str: | |
| git = git_status(settings.workspace) | |
| branch, dirty = parse_git_header(git) | |
| return header_bar_html( | |
| git_branch=branch, | |
| git_dirty=dirty, | |
| model=_selection_label(settings), | |
| host=host_from_url(_cfg().get("base_url", "")), | |
| theme=theme_at(ui.theme_idx).name, | |
| ) | |
| def _status(settings: UiSettings, app_state: AppSessionState, *, running: bool = False) -> str: | |
| title = f"session {app_state.rust.session_id[:8]}" if app_state.rust else "new session" | |
| return status_bar_html( | |
| settings, session_title=title, | |
| model=_selection_label(settings), | |
| running=running, | |
| ) | |
| def _sidebar_html(ui: WebUiState, settings: UiSettings, files: list[str], app_state: AppSessionState) -> str: | |
| sid = app_state.rust.session_id if app_state.rust else "(none)" | |
| return render_sidebar_html( | |
| view=ui.sidebar_view, | |
| files=files, | |
| selected=ui.sidebar_sel, | |
| session_id=sid, | |
| agent=settings.agent, | |
| file_total=ui.file_total or len(files), | |
| ) | |
| def _overlay_html(ui: WebUiState) -> str: | |
| if ui.overlay == "help": | |
| return f'<div class="sc-overlay"><div class="sc-overlay-panel">{help_overlay_html()}</div></div>' | |
| if ui.overlay == "whichkey": | |
| return f'<div class="sc-overlay"><div class="sc-overlay-panel">{whichkey_overlay_html()}</div></div>' | |
| if ui.overlay == "picker" and ui.picker_kind: | |
| panel = render_picker_html( | |
| ui.picker_kind, | |
| ui.picker_items, | |
| ui.picker_sel, | |
| title=ui.picker_kind, | |
| ) | |
| return f'<div class="sc-overlay"><div class="sc-overlay-panel">{panel}</div></div>' | |
| return "" | |
| def _js_boot_lines(settings: UiSettings, files: list[str]) -> str: | |
| cmds = slash_commands(settings.workspace) | |
| paths = sorted(files)[:AUTOCOMPLETE_FILE_LIMIT] | |
| return ( | |
| f"window.__smolcode_workspace={json.dumps(settings.workspace)};" | |
| f"window.__smolcode_commands={json.dumps(cmds)};" | |
| f"window.__smolcode_files={json.dumps(paths)};" | |
| ) | |
| def _embed_js(settings: UiSettings, files: list[str]) -> str: | |
| return f"<script>{_js_boot_lines(settings, files)}</script>" | |
| def _outputs( | |
| transcript: Transcript, | |
| app_state: AppSessionState, | |
| settings: UiSettings, | |
| ui: WebUiState, | |
| files: list[str], | |
| *, | |
| running: bool = False, | |
| trace_path: str | None = None, | |
| ): | |
| overlay_val = _overlay_html(ui) | |
| return ( | |
| transcript.render_html(running=running), | |
| _header(settings, ui), | |
| _status(settings, app_state, running=running), | |
| gr.update(value=_sidebar_html(ui, settings, files, app_state), visible=ui.sidebar_visible), | |
| gr.update(value=overlay_val, visible=bool(overlay_val)), | |
| shell_theme_html(ui.theme_idx), | |
| gr.update(visible=bool(app_state.approval.pending_desc)), | |
| app_state.approval.pending_desc or "", | |
| files, | |
| trace_path, | |
| app_state, | |
| settings, | |
| ui, | |
| transcript, | |
| "", # clear editor | |
| ) | |
| def _apply_slash_ui(sr: SlashResult, settings: UiSettings, ui: WebUiState, transcript: Transcript): | |
| if sr.cycle_mode: | |
| settings.mode = cycle_mode(settings.mode) | |
| transcript.append_info(f"mode → {settings.mode}") | |
| if sr.cycle_think: | |
| settings.think = cycle_think(settings.think) | |
| transcript.append_info(f"think → {settings.think}") | |
| if sr.set_think: | |
| settings.think = sr.set_think | |
| transcript.append_info(f"think → {settings.think}") | |
| if sr.toggle_sidebar: | |
| ui.sidebar_visible = not ui.sidebar_visible | |
| if sr.toggle_sidebar_view: | |
| ui.sidebar_view = "stats" if ui.sidebar_view == "files" else "files" | |
| if sr.show_help: | |
| ui.overlay = "help" | |
| if sr.show_whichkey: | |
| ui.overlay = "whichkey" | |
| if sr.open_picker: | |
| ui.overlay = "picker" | |
| ui.picker_kind = sr.open_picker | |
| ui.picker_items = _picker_items(sr.open_picker, settings) | |
| ui.picker_sel = _picker_sel_for(sr.open_picker, settings, ui, ui.picker_items) | |
| transcript.append_info(f"picker → {sr.open_picker}") | |
| async def _run_agent_turn( | |
| task: str, | |
| transcript: Transcript, | |
| app_state: AppSessionState, | |
| settings: UiSettings, | |
| ui: WebUiState, | |
| files: list[str], | |
| ): | |
| # Blocking model pick: refuse to run until the user has chosen from the modal. | |
| if ui.needs_model_pick: | |
| ui.overlay = "picker" | |
| ui.picker_kind = "models" | |
| ui.picker_items = _model_labels() | |
| ui.picker_sel = _model_sel_index(settings) | |
| transcript.append_info("pick a model to start — Auto is recommended") | |
| yield _outputs(transcript, app_state, settings, ui, files) | |
| return | |
| rust = _ensure_rust(app_state, settings) | |
| rust.clear_cancel() | |
| preset, size_floor = _effective_preset(settings.model) | |
| router = Router( | |
| preset=preset, | |
| approval_handler=app_state.approval.ask, | |
| workspace_dir=settings.workspace, | |
| think=settings.think, | |
| yolo=settings.yolo, | |
| agent=settings.agent, | |
| size_floor=size_floor, | |
| ) | |
| ladder, start, _think = router._route(task) # real routing for the badge | |
| transcript.append_user(task) | |
| transcript.append_info(f"routed to {ladder.tiers[start].name}") | |
| ui.overlay = "" | |
| yield _outputs(transcript, app_state, settings, ui, files, running=True) | |
| result: RouteResult | None = None | |
| async for frame in router.run_live(task, rust_session=rust): | |
| if frame.raw_event: | |
| ingest_agent_event(transcript, frame.raw_event) | |
| if frame.files: | |
| files = _normalize_paths(frame.files) | |
| if frame.done and isinstance(frame.result, RouteResult): | |
| result = frame.result | |
| if rust.cancelled: | |
| transcript.append_error("interrupted") | |
| yield _outputs(transcript, app_state, settings, ui, files, running=not frame.done) | |
| trace_path = None | |
| if result and result.agent and not rust.cancelled: | |
| app_state.bg_jobs = list_background_jobs() | |
| rust.save() | |
| try: | |
| trace_path = str(save_trace(build_trace( | |
| result.agent, task, result.final, | |
| preset=PRESET.key, model=result.tier_model, | |
| ))) | |
| except Exception: | |
| pass | |
| yield _outputs(transcript, app_state, settings, ui, files, trace_path=trace_path) | |
| async def respond( | |
| message: str, | |
| transcript: Transcript, | |
| app_state: AppSessionState, | |
| settings: UiSettings, | |
| ui: WebUiState, | |
| files: list[str], | |
| ): | |
| message = (message or "").strip() | |
| app_state.settings = settings | |
| if not message: | |
| yield _outputs(transcript, app_state, settings, ui, files) | |
| return | |
| _task, slash, shell_cmd = parse_input( | |
| message, | |
| workspace_files=files, | |
| workspace=settings.workspace, | |
| rust=app_state.rust, | |
| ) | |
| if shell_cmd: | |
| rust = _ensure_rust(app_state, settings) | |
| out = rust.run_shell(shell_cmd) | |
| transcript.append_user(f"!{shell_cmd}") | |
| transcript.append_info(out) | |
| yield _outputs(transcript, app_state, settings, ui, files) | |
| return | |
| if slash: | |
| if slash.startswith("/search "): | |
| q = slash.split(maxsplit=1)[1] | |
| hits = transcript.search(q) | |
| transcript.append_user(slash) | |
| transcript.append_info("\n".join(hits) if hits else f"no matches for '{q}'") | |
| yield _outputs(transcript, app_state, settings, ui, files) | |
| return | |
| sr = dispatch_slash(slash, app_state) | |
| _apply_slash_ui(sr, settings, ui, transcript) | |
| if sr.clear_chat: | |
| transcript.clear() | |
| if sr.reply: | |
| transcript.append_user(slash) | |
| plain = sr.reply.replace("**", "").replace("`", "") | |
| transcript.append_info(plain) | |
| if sr.queued_task: | |
| async for out in _run_agent_turn(sr.queued_task, transcript, app_state, settings, ui, files): | |
| yield out | |
| return | |
| yield _outputs(transcript, app_state, settings, ui, files, trace_path=sr.download_path) | |
| return | |
| async for out in _run_agent_turn(_task, transcript, app_state, settings, ui, files): | |
| yield out | |
| def on_interrupt(app_state: AppSessionState): | |
| if app_state.rust: | |
| app_state.rust.request_cancel() | |
| return app_state | |
| def on_clear(transcript: Transcript, ui: WebUiState): | |
| transcript.clear() | |
| ui.overlay = "" | |
| ui.picker_kind = "" | |
| ui.picker_items = [] | |
| ui.picker_sel = 0 | |
| return transcript, ui, "" | |
| def on_close_overlay(ui: WebUiState): | |
| ui.overlay = "" | |
| ui.picker_kind = "" | |
| ui.picker_items = [] | |
| ui.picker_sel = 0 | |
| return ui, gr.update(value="", visible=False) | |
| def on_open_picker(kind: str, ui: WebUiState, settings: UiSettings): | |
| ui.overlay = "picker" | |
| ui.picker_kind = kind | |
| ui.picker_items = _picker_items(kind, settings) | |
| ui.picker_sel = _picker_sel_for(kind, settings, ui, ui.picker_items) | |
| val = _overlay_html(ui) | |
| return ui, gr.update(value=val, visible=True) | |
| def on_picker_nav(delta: int, ui: WebUiState): | |
| if ui.picker_items: | |
| ui.picker_sel = max(0, min(len(ui.picker_items) - 1, ui.picker_sel + delta)) | |
| val = _overlay_html(ui) | |
| return ui, gr.update(value=val, visible=bool(val)) | |
| def on_picker_select( | |
| pick_idx: str, | |
| ui: WebUiState, | |
| settings: UiSettings, | |
| app_state: AppSessionState, | |
| transcript: Transcript, | |
| files: list[str], | |
| ): | |
| try: | |
| idx = int(pick_idx) if pick_idx else ui.picker_sel | |
| except ValueError: | |
| idx = ui.picker_sel | |
| kind = ui.picker_kind | |
| items = ui.picker_items | |
| if items: | |
| idx = max(0, min(len(items) - 1, idx)) | |
| item = items[idx] | |
| if kind == "models": | |
| sel = _label_to_selection(item) | |
| if sel: | |
| settings.model, settings.think = sel | |
| ui.needs_model_pick = False | |
| transcript.append_info(f"model → {item}") | |
| elif kind == "themes": | |
| if item in theme_names(): | |
| ui.theme_idx = theme_names().index(item) | |
| transcript.append_info(f"theme → {item}") | |
| elif kind == "agents": | |
| if settings.mode != "plan": | |
| settings.agent = item | |
| transcript.append_info(f"agent → {item}") | |
| elif kind == "sessions": | |
| sid = parse_session_label(item) | |
| if sid: | |
| rust = RustSession(workspace=settings.workspace, agent=settings.agent, yolo=settings.yolo) | |
| if rust.load_session(sid): | |
| app_state.rust = rust | |
| transcript.clear() | |
| transcript.from_stored_chat(get_session_chat(sid)) | |
| transcript.append_info(f"loaded session {sid[:8]}") | |
| ui.overlay = "" | |
| ui.picker_kind = "" | |
| ui.picker_items = [] | |
| ui.picker_sel = 0 | |
| overlay_val = _overlay_html(ui) | |
| return ( | |
| transcript.render_html(), | |
| _header(settings, ui), | |
| _status(settings, app_state), | |
| gr.update(value=overlay_val, visible=False), | |
| shell_theme_html(ui.theme_idx), | |
| settings, | |
| ui, | |
| transcript, | |
| app_state, | |
| ) | |
| def _cycle_outputs( | |
| settings: UiSettings, | |
| ui: WebUiState, | |
| app_state: AppSessionState, | |
| transcript: Transcript, | |
| ): | |
| return ( | |
| settings, | |
| transcript, | |
| transcript.render_html(), | |
| _header(settings, ui), | |
| _status(settings, app_state), | |
| shell_theme_html(ui.theme_idx), | |
| ) | |
| def on_toggle_sidebar(ui: WebUiState, settings: UiSettings, files: list[str], app_state: AppSessionState): | |
| ui.sidebar_visible = not ui.sidebar_visible | |
| return ui, gr.update( | |
| value=_sidebar_html(ui, settings, files, app_state), | |
| visible=ui.sidebar_visible, | |
| ) | |
| def on_toggle_sidebar_view( | |
| ui: WebUiState, settings: UiSettings, files: list[str], app_state: AppSessionState, | |
| ): | |
| ui.sidebar_view = "stats" if ui.sidebar_view == "files" else "files" | |
| return ui, gr.update(value=_sidebar_html(ui, settings, files, app_state)) | |
| def on_load(settings: UiSettings, app_state: AppSessionState, ui: WebUiState): | |
| paths, total = workspace_paths(settings.workspace) | |
| ui.file_total = total | |
| overlay_val = "" | |
| if ui.needs_model_pick: # blocking startup model picker | |
| ui.overlay = "picker" | |
| ui.picker_kind = "models" | |
| ui.picker_items = _model_labels() | |
| ui.picker_sel = _model_sel_index(settings) | |
| overlay_val = _overlay_html(ui) | |
| return ( | |
| _sidebar_html(ui, settings, paths, app_state), | |
| paths, | |
| _embed_js(settings, paths), | |
| gr.update(choices=session_choices()), | |
| gr.update(value=overlay_val, visible=bool(overlay_val)), | |
| ui, | |
| ) | |
| def on_cycle_mode(settings: UiSettings, ui: WebUiState, app_state: AppSessionState, transcript: Transcript): | |
| settings.mode = cycle_mode(settings.mode) | |
| if settings.mode == "plan": | |
| settings.agent = "plan" | |
| elif settings.agent == "plan": | |
| settings.agent = "build" | |
| settings.yolo = settings.mode == "auto" | |
| transcript.append_info(f"mode → {settings.mode}") | |
| return _cycle_outputs(settings, ui, app_state, transcript) | |
| def on_cycle_agent(settings: UiSettings, ui: WebUiState, app_state: AppSessionState, transcript: Transcript): | |
| if settings.mode != "plan": | |
| settings.agent = cycle_agent(settings.agent) | |
| transcript.append_info(f"agent → {settings.agent}") | |
| return _cycle_outputs(settings, ui, app_state, transcript) | |
| def on_cycle_model(settings: UiSettings, ui: WebUiState, app_state: AppSessionState, transcript: Transcript): | |
| labels = _model_labels() | |
| nxt = cycle_model(labels, _selection_label(settings)) | |
| sel = _label_to_selection(nxt) | |
| if sel: | |
| settings.model, settings.think = sel | |
| ui.needs_model_pick = False | |
| transcript.append_info(f"model → {nxt}") | |
| return _cycle_outputs(settings, ui, app_state, transcript) | |
| def on_cycle_think(settings: UiSettings, ui: WebUiState, app_state: AppSessionState, transcript: Transcript): | |
| settings.think = cycle_think(settings.think) | |
| transcript.append_info(f"think → {settings.think}") | |
| return _cycle_outputs(settings, ui, app_state, transcript) | |
| def on_help(ui: WebUiState): | |
| ui.overlay = "help" | |
| val = _overlay_html(ui) | |
| return ui, gr.update(value=val, visible=True) | |
| def on_whichkey(ui: WebUiState): | |
| ui.overlay = "whichkey" | |
| val = _overlay_html(ui) | |
| return ui, gr.update(value=val, visible=True) | |
| def on_new_session(): | |
| settings = UiSettings(workspace=os.environ.get("SMALLCODE_WORKSPACE", "."), model="auto") | |
| ui = WebUiState() # needs_model_pick defaults True -> reopen the blocking picker | |
| ui.overlay = "picker" | |
| ui.picker_kind = "models" | |
| ui.picker_items = _model_labels() | |
| ui.picker_sel = _model_sel_index(settings) | |
| return ( | |
| Transcript(), AppSessionState(), settings, ui, [], None, | |
| gr.update(value=_overlay_html(ui), visible=True), | |
| ) | |
| def on_approval(yes: bool, app_state: AppSessionState): | |
| if app_state: | |
| app_state.approval.approve(yes) | |
| return gr.update(visible=False), "" | |
| def on_session_pick(label: str, app_state: AppSessionState, settings: UiSettings): | |
| sid = parse_session_label(label or "") | |
| if not sid: | |
| return Transcript(), app_state | |
| rust = RustSession(workspace=settings.workspace, agent=settings.agent, yolo=settings.yolo) | |
| if not rust.load_session(sid): | |
| return Transcript(), app_state | |
| app_state.rust = rust | |
| t = Transcript() | |
| t.from_stored_chat(get_session_chat(sid)) | |
| return t, app_state | |
| def build() -> gr.Blocks: | |
| default_ws = os.environ.get("SMALLCODE_WORKSPACE", ".") | |
| # Default selection is Auto (router-driven); the blocking startup modal lets the | |
| # user confirm or change it before the first task. | |
| settings = UiSettings(workspace=default_ws, model="auto") | |
| with gr.Blocks( | |
| css=SMOLCODE_CSS, | |
| title="smolcode", | |
| theme=gr.themes.Soft(primary_hue="purple", neutral_hue="slate"), | |
| head=f"<script>{_JS_HEAD}\n{_js_boot_lines(settings, [])}</script>", | |
| fill_height=True, | |
| fill_width=True, | |
| ) as demo: | |
| transcript = gr.State(Transcript()) | |
| app_state = gr.State(AppSessionState(settings=settings)) | |
| settings_state = gr.State(settings) | |
| ui_state = gr.State(WebUiState()) | |
| files_state = gr.State([]) | |
| trace_state = gr.State(None) | |
| with gr.Column(elem_classes="sc-tui-shell"): | |
| header = gr.HTML(_header(settings, WebUiState())) | |
| shell_theme = gr.HTML(shell_theme_html(0), visible=False) | |
| with gr.Row(elem_classes="sc-main-row"): | |
| sidebar = gr.HTML( | |
| _sidebar_html(WebUiState(), settings, [], AppSessionState()), | |
| elem_classes="sc-sidebar", | |
| visible=True, | |
| ) | |
| with gr.Column(elem_classes="sc-main-col"): | |
| transcript_view = gr.HTML(Transcript().render_html()) | |
| with gr.Group(elem_classes="sc-editor-wrap"): | |
| gr.HTML( | |
| '<div class="sc-editor-hint">' | |
| "Enter run · Shift+Enter newline · / commands · ctrl+x leader" | |
| "</div>" | |
| ) | |
| editor = gr.Textbox( | |
| placeholder="type a task…", | |
| lines=5, | |
| max_lines=8, | |
| show_label=False, | |
| elem_id="sc-editor", | |
| interactive=True, | |
| autofocus=True, | |
| ) | |
| with gr.Group(visible=False) as approval_box: | |
| approval_desc = gr.Markdown("", elem_classes="sc-approval") | |
| with gr.Row(): | |
| gr.Button("Approve", variant="primary").click( | |
| lambda s: on_approval(True, s), app_state, [approval_box, approval_desc]) | |
| gr.Button("Deny").click( | |
| lambda s: on_approval(False, s), app_state, [approval_box, approval_desc]) | |
| status = gr.HTML(_status(settings, AppSessionState()), elem_classes="sc-status-wrap") | |
| overlay = gr.HTML("", visible=False) | |
| js_boot = gr.HTML(_embed_js(settings, []), elem_classes=["sc-hidden-controls"]) | |
| # Off-screen controls (visible=True so Gradio mounts them for JS shortcuts). | |
| _hid = ["sc-hidden-btn"] | |
| with gr.Row(elem_classes="sc-hidden-controls"): | |
| btn_submit = gr.Button("submit", elem_id="sc-submit", elem_classes=_hid) | |
| btn_clear = gr.Button("clear", elem_id="sc-clear", elem_classes=_hid) | |
| btn_interrupt = gr.Button("interrupt", elem_id="sc-interrupt", elem_classes=_hid) | |
| btn_toggle_sidebar = gr.Button("sidebar", elem_id="sc-toggle-sidebar", elem_classes=_hid) | |
| btn_toggle_view = gr.Button("view", elem_id="sc-toggle-sidebar-view", elem_classes=_hid) | |
| btn_cycle_mode = gr.Button("mode", elem_id="sc-cycle-mode", elem_classes=_hid) | |
| btn_cycle_agent = gr.Button("agent", elem_id="sc-cycle-agent", elem_classes=_hid) | |
| btn_cycle_model = gr.Button("model", elem_id="sc-cycle-model", elem_classes=_hid) | |
| btn_cycle_think = gr.Button("think", elem_id="sc-cycle-think", elem_classes=_hid) | |
| btn_help = gr.Button("help", elem_id="sc-help", elem_classes=_hid) | |
| btn_whichkey = gr.Button("wk", elem_id="sc-whichkey", elem_classes=_hid) | |
| btn_close = gr.Button("close", elem_id="sc-close-overlay", elem_classes=_hid) | |
| btn_new = gr.Button("new", elem_id="sc-new-session", elem_classes=_hid) | |
| btn_open_models = gr.Button("models", elem_id="sc-open-picker-models", elem_classes=_hid) | |
| btn_open_themes = gr.Button("themes", elem_id="sc-open-picker-themes", elem_classes=_hid) | |
| btn_open_agents = gr.Button("agents", elem_id="sc-open-picker-agents", elem_classes=_hid) | |
| btn_open_sessions = gr.Button("sessions", elem_id="sc-open-picker-sessions", elem_classes=_hid) | |
| btn_picker_up = gr.Button("up", elem_id="sc-picker-up", elem_classes=_hid) | |
| btn_picker_down = gr.Button("down", elem_id="sc-picker-down", elem_classes=_hid) | |
| btn_picker_confirm = gr.Button("confirm", elem_id="sc-picker-confirm", elem_classes=_hid) | |
| picker_pick = gr.Textbox("", elem_id="sc-picker-pick", elem_classes=_hid, show_label=False) | |
| session_pick = gr.Dropdown(choices=session_choices(), label="session", elem_id="sc-pick-sessions", elem_classes=_hid) | |
| trace_dl = gr.DownloadButton("trace", elem_classes=_hid) | |
| out = [ | |
| transcript_view, header, status, sidebar, | |
| overlay, shell_theme, approval_box, approval_desc, | |
| files_state, trace_state, app_state, settings_state, ui_state, transcript, editor, | |
| ] | |
| cycle_out = [ | |
| settings_state, transcript, transcript_view, header, status, shell_theme, | |
| ] | |
| picker_out = [ | |
| transcript_view, header, status, overlay, shell_theme, | |
| settings_state, ui_state, transcript, app_state, | |
| ] | |
| respond_in = [editor, transcript, app_state, settings_state, ui_state, files_state] | |
| btn_submit.click(respond, respond_in, out).then(lambda p: p, trace_state, trace_dl) | |
| editor.submit(respond, respond_in, out).then(lambda p: p, trace_state, trace_dl) | |
| btn_clear.click(on_clear, [transcript, ui_state], [transcript, ui_state, editor]) | |
| btn_interrupt.click(on_interrupt, app_state, app_state) | |
| btn_toggle_sidebar.click( | |
| on_toggle_sidebar, [ui_state, settings_state, files_state, app_state], [ui_state, sidebar]) | |
| btn_toggle_view.click( | |
| on_toggle_sidebar_view, | |
| [ui_state, settings_state, files_state, app_state], | |
| [ui_state, sidebar], | |
| ) | |
| btn_cycle_mode.click( | |
| on_cycle_mode, [settings_state, ui_state, app_state, transcript], cycle_out) | |
| btn_cycle_agent.click( | |
| on_cycle_agent, [settings_state, ui_state, app_state, transcript], cycle_out) | |
| btn_cycle_model.click( | |
| on_cycle_model, [settings_state, ui_state, app_state, transcript], cycle_out) | |
| btn_cycle_think.click( | |
| on_cycle_think, [settings_state, ui_state, app_state, transcript], cycle_out) | |
| btn_help.click(on_help, ui_state, [ui_state, overlay]) | |
| btn_whichkey.click(on_whichkey, ui_state, [ui_state, overlay]) | |
| btn_close.click(on_close_overlay, ui_state, [ui_state, overlay]) | |
| btn_new.click(on_new_session, None, [transcript, app_state, settings_state, ui_state, files_state, trace_state, overlay]) | |
| btn_open_models.click(lambda ui, s: on_open_picker("models", ui, s), [ui_state, settings_state], [ui_state, overlay]) | |
| btn_open_themes.click(lambda ui, s: on_open_picker("themes", ui, s), [ui_state, settings_state], [ui_state, overlay]) | |
| btn_open_agents.click(lambda ui, s: on_open_picker("agents", ui, s), [ui_state, settings_state], [ui_state, overlay]) | |
| btn_open_sessions.click(lambda ui, s: on_open_picker("sessions", ui, s), [ui_state, settings_state], [ui_state, overlay]) | |
| btn_picker_up.click(lambda ui: on_picker_nav(-1, ui), ui_state, [ui_state, overlay]) | |
| btn_picker_down.click(lambda ui: on_picker_nav(1, ui), ui_state, [ui_state, overlay]) | |
| btn_picker_confirm.click( | |
| on_picker_select, | |
| [picker_pick, ui_state, settings_state, app_state, transcript, files_state], | |
| picker_out, | |
| ) | |
| session_pick.change(on_session_pick, [session_pick, app_state, settings_state], [transcript, app_state]) | |
| demo.load( | |
| on_load, | |
| [settings_state, app_state, ui_state], | |
| [sidebar, files_state, js_boot, session_pick, overlay, ui_state], | |
| ) | |
| return demo | |
| if __name__ == "__main__": | |
| from engine.preflight import preflight | |
| preflight(PRESET) | |
| host = os.environ.get("SMOLCODE_HOST", "127.0.0.1") | |
| os.environ["GRADIO_SERVER_PORT"] = os.environ.get("SMOLCODE_PORT", "7860") | |
| os.environ["GRADIO_SERVER_NAME"] = host | |
| # server_port=None lets Gradio scan GRADIO_SERVER_PORT..+99 (skips ghost 7860-7862). | |
| # ssr_mode=False: SSR (default on HF when Node is present) renders before the | |
| # custom web_tui.js applies the fixed-height layout, leaving the file sidebar | |
| # uncapped (grows forever, hides the bottom bar/model picker). Client-side render | |
| # applies the layout immediately. | |
| build().queue().launch(server_name=host, server_port=None, show_api=False, | |
| ssr_mode=False) | |