diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b8f6019 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + unit-tests: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Create venv and install dependencies + run: uv venv && uv pip install -e '.[test]' + + - name: Lint with ruff + run: uv run ruff check src/ tests/ + + - name: Run unit tests + run: | + uv run python -m pytest tests/ -x -q \ + --ignore=tests/test_smoke_pipelines.py \ + --ignore=tests/test_smoke_agent.py \ + --ignore=tests/test_integration_extract.py \ + --ignore=tests/test_integration_summarize.py \ + --ignore=tests/test_integration_agent.py \ + --ignore=tests/test_integration_providers.py \ + --ignore=tests/test_e2e_sync.py \ + --ignore=tests/test_e2e_maintain.py \ + --ignore=tests/test_e2e_full_cycle.py \ + -m "not smoke and not integration and not e2e" diff --git a/.gitignore b/.gitignore index 3b4ac5b..6d8e6e9 100644 --- a/.gitignore +++ b/.gitignore @@ -32,15 +32,23 @@ site/ .tmp/ specs/ .lerim/ +.playwright-cli/ +.logfire/ +.pytest_cache/ +.ruff_cache/ +.claude/ + dev-docs/ # Test artifacts snapshot_report.html qa_artifacts/* +.pytest_tmp/ # Local browser/db state orb.db/ AGENTS.md CLAUDE.md + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6c69c5e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,29 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.0] - 2026-02-25 + +### Added + +- Continual learning layer for coding agents and projects. +- Platform adapters for Claude Code, Codex CLI, Cursor, and OpenCode. +- Memory extraction pipeline using DSPy RLM to extract decisions and learnings from coding session traces. +- Trace summarization pipeline using DSPy RLM to produce structured summaries with YAML frontmatter. +- PydanticAI lead agent with a read-only explorer subagent for memory operations. +- Three CLI flows: `sync` (extract, summarize, write memories), `maintain` (merge, archive, decay), and `chat` (query memories). +- Daemon mode for continuous sync and maintain loop. +- Local read-only web dashboard. +- Session catalog with SQLite FTS5 for session search. +- Job queue with stale job reclamation. +- TOML-layered configuration: shipped defaults, global, project, and env var override. +- OpenTelemetry tracing via Logfire with PydanticAI and DSPy instrumentation. +- Multi-provider LLM support: OpenRouter (with Nebius routing), Ollama, ZAI, OpenAI, Anthropic. +- File-first memory model using markdown files with YAML frontmatter. +- Project-first memory scope with global fallback. +- Memory primitives: decisions, learnings, and summaries. +- Comprehensive test suite with 290 tests across unit, smoke, integration, and e2e layers. +- Skills distribution via `npx skills add lerim-dev/lerim-cli`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a466316 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,95 @@ +# Contributing to Lerim + +Lerim is licensed under BSL 1.1. By contributing you agree your changes +fall under the same license (1 user free, 2+ users need a commercial license). + +## Dev environment setup + +Requires Python 3.12+. + +```bash +uv venv && source .venv/bin/activate +uv pip install -e '.[test]' +``` + +## Running tests + +```bash +# Unit tests (no LLM keys needed) +tests/run_tests.sh unit + +# Smoke tests (needs LLM API key in env) +tests/run_tests.sh smoke + +# Everything +tests/run_tests.sh all +``` + +Lint before submitting: + +```bash +ruff check src/ tests/ +``` + +## Coding style + +Full rules live in `docs/simple-coding-rules.md`. The short version: + +- **Minimal code.** Prefer fewer functions, fewer layers, fewer lines. +- **Strict schemas.** Use Pydantic models / enums for inputs and outputs. +- **Fail fast.** No `try/except` fallbacks for missing packages. If something + is required, let it raise. +- **Docstrings everywhere.** Every file gets a top-level docstring explaining + what it contains. Every function gets a docstring. +- **Self-tests.** Add an `if __name__ == "__main__":` block that exercises the + real code path (no mocking or stubbing). Keep it inline, not in a separate + function. +- **No dead code.** When you replace logic, remove the old path. +- **Config from TOML, keys from env.** Runtime config comes from the TOML + layer stack (`default.toml -> global -> project -> env var`). Only API keys + use environment variables. + +## Adding a new platform adapter + +This is the most common contribution. Follow these steps: + +1. **Create `src/lerim/adapters/.py`.** + Start with a top-level docstring. Implement the four functions required by + the `Adapter` protocol in `src/lerim/adapters/base.py`: + + - `default_path() -> Path | None` -- where traces live on disk. + - `count_sessions(path) -> int` + - `iter_sessions(traces_dir, start, end, known_run_ids) -> list[SessionRecord]` + - `find_session_path(session_id, traces_dir) -> Path | None` + - `read_session(session_path, session_id) -> ViewerSession | None` + + See an existing adapter (e.g. `codex.py` or `claude.py`) as a reference. + +2. **Register the adapter** in `src/lerim/adapters/registry.py`: + add an entry to `_ADAPTER_MODULES` and optionally to `_AUTO_SEED_PLATFORMS`. + +3. **Add a self-test** (`if __name__ == "__main__":` block) at the bottom of + the new adapter file that exercises the real code path. + +4. **Add unit tests** in `tests/test__adapter.py`. Look at the + existing adapter test files for the expected patterns. + +5. **Update `tests/README.md`** if you added new fixtures or test infrastructure. + +## Reporting bugs + +Open a GitHub issue with: + +- Steps to reproduce. +- Expected vs actual behavior. +- Lerim version (`python -m lerim --version`), Python version, and OS. +- Relevant config (redact API keys). + +## Pull request checklist + +- [ ] `ruff check src/ tests/` passes with no errors. +- [ ] `tests/run_tests.sh unit` passes. +- [ ] New/changed files have top-level docstrings and function docstrings. +- [ ] New source files include an `if __name__ == "__main__":` self-test when practical. +- [ ] No mocking or stubbing in self-test flows. +- [ ] Related docs updated if behavior changed. diff --git a/README.md b/README.md index f123d0b..a9401a4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Lerim Logo

-

Continual learning layer for coding agents.

+

Continual learning layer for coding agents

lerim.dev

Lerim is a continual learning layer that gives coding agents persistent memory across sessions. It watches your agent conversations (Claude Code, Codex, Cursor, OpenCode, ...), extracts decisions and learnings, and stores them as plain markdown files that both humans and agents can read. Memories are refined offline over time through merging, deduplication, archiving, and decay-based forgetting. You can query stored memories anytime to bring relevant past context into your current session. @@ -16,7 +16,7 @@ Lerim is file-first and primitive-first. - Global fallback memory: `~/.lerim/` - Search default: `files` (no index required) - Orchestration runtime: `pydantic-ai` lead agent + read-only explorer subagent -- Extraction/summarization: `dspy.RLM` role-configured models (default Ollama `qwen3:8b`) +- Extraction/summarization: `dspy.RLM` role-configured models (default OpenRouter `x-ai/grok-4.1-fast`) - Graph source of truth: explicit id/slug references (and `related` when present) This keeps memory readable by humans and easy for agents to traverse. @@ -29,6 +29,22 @@ Lead flow: 4. Lead writes memory only through boundary-enforced runtime write/edit tools. 5. `sync` stays lightweight; `maintain` runs offline memory refinement (merge duplicates, archive low-value entries, consolidate related memories, apply time-based decay). +### Sync path + +

+ Sync path +

+ +The sync path processes new agent sessions: reads transcript archives, extracts decision and learning candidates via DSPy, deduplicates against existing memories, and writes new primitives to the memory folder. + +### Maintain path + +

+ Maintain path +

+ +The maintain path runs offline refinement over stored memories: merges duplicates, archives low-value entries, consolidates related memories, and applies time-based decay to keep the memory store clean and relevant. + ## Quick start ### 1. Install @@ -70,6 +86,39 @@ At the start of a session, tell your agent: Your agent will run `lerim chat` or `lerim memory search` to pull in past decisions and learnings before it starts working. +## Dashboard + +The dashboard gives you a local UI for session analytics, memory browsing, and runtime status. + +

+ Lerim dashboard +

+ +### Run it locally + +```bash +# simple +lerim dashboard + +# explicit host/port +python -m lerim dashboard --host 127.0.0.1 --port 8765 +``` + +Then open `http://127.0.0.1:8765`. + +### Tabs + +- **Overview**: high-level metrics and charts (sessions, messages, tools, errors, tokens, activity by day/hour, model usage). +- **Runs**: searchable session list (50/page) with status and metadata; open any run in a full-screen chat viewer. +- **Memories**: library + editor for memory records (filter, inspect, edit title/body/kind/confidence/tags). +- **Pipeline**: sync/maintain status, extraction queue state, and latest extraction report. +- **Settings**: dashboard-editable config for server, model roles, and tracing; saves to `~/.lerim/config.toml`. + +### Notes + +- Top bar filters (`Agent`, `Scope`) update dashboard metrics and run listings. +- Graph Explorer code is kept in the project but currently hidden in the UI. + ## CLI reference Full command reference: [`skills/lerim/cli-reference.md`](skills/lerim/cli-reference.md) @@ -93,8 +142,8 @@ lerim status # runtime state ```bash uv venv && source .venv/bin/activate uv pip install -e . -scripts/run_tests.sh unit -scripts/run_tests.sh all +tests/run_tests.sh unit +tests/run_tests.sh all ``` ### Configuration @@ -108,6 +157,13 @@ TOML-layered config (low to high priority): API keys come from environment variables only (`ZAI_API_KEY`, `OPENROUTER_API_KEY`, `OPENAI_API_KEY`, optional `ANTHROPIC_API_KEY`). +Default role model config (from `src/lerim/config/default.toml`): + +- `lead`: `provider=openrouter`, `model=x-ai/grok-4.1-fast` +- `explorer`: `provider=openrouter`, `model=x-ai/grok-4.1-fast` +- `extract`: `provider=openrouter`, `model=x-ai/grok-4.1-fast`, `sub_model=x-ai/grok-4.1-fast` +- `summarize`: `provider=openrouter`, `model=x-ai/grok-4.1-fast`, `sub_model=x-ai/grok-4.1-fast` + ### Tracing (OpenTelemetry) Lerim uses PydanticAI's built-in OpenTelemetry instrumentation for agent observability. @@ -144,12 +200,58 @@ Config options (`[tracing]` in TOML): | `include_httpx` | `false` | Capture raw HTTP request/response bodies | | `include_content` | `true` | Include prompt/completion text in spans | -### Supported platforms +### Connecting coding agents -- `claude` — reads from `~/.claude/projects/` (JSONL files) -- `codex` — reads from `~/.codex/sessions/` (JSONL files) -- `cursor` — reads from Cursor's `state.vscdb` SQLite DB, exports sessions as JSONL to `~/.lerim/cache/cursor/` -- `opencode` — reads from `~/.local/share/opencode/` +Lerim ingests session transcripts from your coding agents to extract decisions and learnings. The `lerim connect` command registers an agent platform so Lerim knows where to find its sessions. + +#### Supported agents + +| Platform | Session store | Format | +|----------|--------------|--------| +| `claude` | `~/.claude/projects/` | JSONL files | +| `codex` | `~/.codex/sessions/` | JSONL files | +| `cursor` | `~/Library/Application Support/Cursor/User/globalStorage/` (macOS) | SQLite `state.vscdb`, exported to JSONL cache | +| `opencode` | `~/.local/share/opencode/` | SQLite `opencode.db`, exported to JSONL cache | + +#### How to connect + +Auto-detect and connect all supported platforms at once: + +```bash +lerim connect auto +``` + +Or connect a specific platform: + +```bash +lerim connect claude +lerim connect codex +lerim connect cursor +lerim connect opencode +``` + +List currently connected platforms: + +```bash +lerim connect list +``` + +Disconnect a platform: + +```bash +lerim connect remove claude +``` + +#### Custom session path + +If your agent stores sessions in a non-default location, use `--path` to point Lerim to the correct folder: + +```bash +lerim connect claude --path /custom/path/to/claude/sessions +lerim connect cursor --path ~/my-cursor-data/globalStorage +``` + +The path is expanded (`~` is resolved) and must exist on disk. This overrides the auto-detected default for that platform. ### Search @@ -217,10 +319,6 @@ lerim memory reset --yes # wipe everything lerim sync --max-sessions 5 # re-sync newest conversations ``` -## Migration from Acreta - -If you previously used Acreta, the data directories have moved from `~/.acreta/` to `~/.lerim/` and from `/.acreta/` to `/.lerim/`. Existing data is not migrated automatically. Run `lerim memory reset --yes && lerim sync` to start fresh. - ## Docs - Runtime architecture: `docs/architecture.md` diff --git a/assets/dashboard.png b/assets/dashboard.png new file mode 100644 index 0000000..4afdaaa Binary files /dev/null and b/assets/dashboard.png differ diff --git a/assets/maintain.png b/assets/maintain.png new file mode 100644 index 0000000..86e8426 Binary files /dev/null and b/assets/maintain.png differ diff --git a/assets/screenshots/codex-analysis.png b/assets/screenshots/codex-analysis.png deleted file mode 100644 index 1df2b60..0000000 Binary files a/assets/screenshots/codex-analysis.png and /dev/null differ diff --git a/assets/screenshots/codex-overview.png b/assets/screenshots/codex-overview.png deleted file mode 100644 index e4fd8e3..0000000 Binary files a/assets/screenshots/codex-overview.png and /dev/null differ diff --git a/assets/screenshots/readiness-report.png b/assets/screenshots/readiness-report.png deleted file mode 100644 index 2146be4..0000000 Binary files a/assets/screenshots/readiness-report.png and /dev/null differ diff --git a/assets/screenshots/session-viewer.png b/assets/screenshots/session-viewer.png deleted file mode 100644 index 5c82eb8..0000000 Binary files a/assets/screenshots/session-viewer.png and /dev/null differ diff --git a/assets/sync.png b/assets/sync.png new file mode 100644 index 0000000..fbacc8e Binary files /dev/null and b/assets/sync.png differ diff --git a/dashboard/README.md b/dashboard/README.md index 30be839..671294f 100644 --- a/dashboard/README.md +++ b/dashboard/README.md @@ -4,6 +4,7 @@ Unified dashboard frontend lives here. - `index.html`: main dashboard page (tabs, Alpine stores, UI actions). - `assets/graph-explorer/`: built graph explorer bundle used by Memories tab. +- `assets/lerim.png`: dashboard brand logo used in the top navbar. - `frontend/graph-explorer/`: Vite + TypeScript source for graph explorer bundle. Served by `src/lerim/app/dashboard.py` from this folder as the static root. diff --git a/dashboard/assets/lerim.png b/dashboard/assets/lerim.png new file mode 100644 index 0000000..aca4a97 Binary files /dev/null and b/dashboard/assets/lerim.png differ diff --git a/dashboard/index.html b/dashboard/index.html index ddd3cbc..9a0d1df 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -69,8 +69,9 @@ width: 36px; height: 36px; border-radius: 10px; - background: linear-gradient(140deg, var(--lerim-accent-2), var(--lerim-accent)); + object-fit: cover; box-shadow: 0 12px 24px rgba(15, 23, 42, 0.45); + flex-shrink: 0; } .lerim-title { @@ -264,6 +265,8 @@ .agent-badge.claude { background: #f59e0b; } .agent-badge.codex { background: #22d3ee; } .agent-badge.opencode { background: #34d399; } + .agent-badge.cursor { background: #e879f9; } + .agent-badge.cline { background: #fb923c; } .status-pill { padding: 0.25rem 0.6rem; @@ -277,15 +280,6 @@ .status-pill.error { background: rgba(248, 113, 113, 0.15); color: #f87171; border-color: rgba(248, 113, 113, 0.4); } .status-pill.running { background: rgba(56, 189, 248, 0.15); color: #38bdf8; border-color: rgba(56, 189, 248, 0.4); } - .runs-grid { - display: grid; - gap: 1rem; - } - - @media (min-width: 1200px) { - .runs-grid { grid-template-columns: 2fr 1.1fr; } - } - .memory-grid { display: grid; gap: 1rem; @@ -342,49 +336,281 @@ color: var(--lerim-muted); } - .chat-message { - background: rgba(15, 23, 42, 0.35); - border: 1px solid rgba(34, 211, 238, 0.1); - border-radius: 12px; - padding: 0.75rem; - margin-bottom: 0.75rem; + .chat-block { + font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; + font-size: 0.78rem; + white-space: pre-wrap; + background: rgba(15, 23, 42, 0.7); + padding: 0.5rem; + border-radius: 8px; + color: #cbd5f5; + } + + .run-viewer-overlay { + position: fixed; + inset: 0; + z-index: 5000; + display: flex; + align-items: stretch; + justify-content: center; + padding: 14px; + background: rgba(6, 10, 18, 0.86); + backdrop-filter: blur(7px); + } + + .run-viewer-shell { + width: min(1260px, 100%); + max-height: 100%; + border-radius: 16px; + border: 1px solid rgba(148, 163, 184, 0.25); + background: linear-gradient(165deg, rgba(14, 22, 33, 0.98), rgba(10, 15, 24, 0.96)); + box-shadow: 0 34px 70px rgba(0, 0, 0, 0.45); + display: grid; + grid-template-rows: auto auto minmax(0, 1fr); + overflow: hidden; + } + + .run-viewer-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.8rem; + padding: 0.9rem 1rem; + border-bottom: 1px solid rgba(148, 163, 184, 0.2); + background: rgba(15, 23, 42, 0.55); + } + + .run-viewer-title { + font-size: 0.95rem; + font-weight: 700; + letter-spacing: 0.03em; + text-transform: uppercase; + color: #dbe7fb; + } + + .run-viewer-subtitle { + margin-top: 0.18rem; + font-size: 0.8rem; + color: var(--lerim-muted); + } + + .run-viewer-meta { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + padding: 0.72rem 1rem; + border-bottom: 1px solid rgba(148, 163, 184, 0.14); + background: rgba(8, 12, 19, 0.48); + } + + .run-viewer-chip { + display: inline-flex; + align-items: center; + gap: 0.28rem; + border-radius: 999px; + border: 1px solid rgba(148, 163, 184, 0.3); + padding: 0.23rem 0.58rem; + font-size: 0.72rem; + color: #c7d4e8; + background: rgba(15, 23, 42, 0.66); + } + + .run-viewer-chat { + overflow: auto; + padding: 1rem; + display: grid; + gap: 0.66rem; + background: + radial-gradient(1000px 600px at 16% -26%, rgba(34, 211, 238, 0.06), transparent 68%), + radial-gradient(900px 540px at 92% -34%, rgba(245, 158, 11, 0.08), transparent 70%), + rgba(8, 12, 19, 0.75); } - .chat-message.user { - border-color: rgba(245, 158, 11, 0.3); + .run-chat-row { + display: flex; + } + + .run-chat-row.user { + justify-content: flex-end; + } + + .run-chat-row.tool, + .run-chat-row.system, + .run-chat-row.assistant { + justify-content: flex-start; + } + + .run-chat-bubble { + width: min(86%, 860px); + border-radius: 14px; + border: 1px solid rgba(148, 163, 184, 0.26); + background: rgba(12, 18, 28, 0.85); + padding: 0.72rem 0.8rem; + box-shadow: 0 10px 22px rgba(0, 0, 0, 0.22); } - .chat-message.tool { - border-color: rgba(148, 163, 184, 0.25); + .run-chat-row.user .run-chat-bubble { + border-color: rgba(245, 158, 11, 0.38); + background: rgba(59, 35, 6, 0.65); } - .chat-meta { + .run-chat-row.tool .run-chat-bubble { + border-color: rgba(34, 211, 238, 0.34); + background: rgba(7, 30, 36, 0.6); + } + + .run-chat-row.system .run-chat-bubble { + border-color: rgba(148, 163, 184, 0.35); + background: rgba(30, 41, 59, 0.5); + } + + .run-chat-meta { display: flex; justify-content: space-between; + gap: 0.8rem; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em; - color: var(--lerim-muted); - margin-bottom: 0.4rem; + color: #9fb2cd; + margin-bottom: 0.34rem; } - .chat-block { + .run-chat-content { + white-space: normal; + word-break: break-word; + font-size: 0.86rem; + line-height: 1.45; + color: #dde7f6; + } + + .run-chat-markdown > *:first-child { + margin-top: 0; + } + + .run-chat-markdown > *:last-child { + margin-bottom: 0; + } + + .run-chat-markdown h1, + .run-chat-markdown h2, + .run-chat-markdown h3, + .run-chat-markdown h4 { + margin: 0.7rem 0 0.45rem; + font-size: 0.95rem; + line-height: 1.35; + color: #f1f6ff; + letter-spacing: 0.01em; + } + + .run-chat-markdown h1 { font-size: 1.08rem; } + .run-chat-markdown h2 { font-size: 1rem; } + .run-chat-markdown h3 { font-size: 0.93rem; } + + .run-chat-markdown p, + .run-chat-markdown ul, + .run-chat-markdown ol, + .run-chat-markdown blockquote, + .run-chat-markdown pre { + margin: 0.45rem 0; + } + + .run-chat-markdown ul, + .run-chat-markdown ol { + padding-left: 1.2rem; + } + + .run-chat-markdown li { + margin: 0.16rem 0; + } + + .run-chat-markdown hr { + border: none; + border-top: 1px solid rgba(148, 163, 184, 0.3); + margin: 0.7rem 0; + } + + .run-chat-markdown blockquote { + border-left: 3px solid rgba(56, 189, 248, 0.45); + padding-left: 0.7rem; + color: #bed1eb; + opacity: 0.95; + } + + .run-chat-markdown code { font-family: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; font-size: 0.78rem; - white-space: pre-wrap; - background: rgba(15, 23, 42, 0.7); - padding: 0.5rem; + background: rgba(15, 23, 42, 0.72); + border: 1px solid rgba(148, 163, 184, 0.22); + border-radius: 6px; + padding: 0.08rem 0.32rem; + color: #d4e3fb; + } + + .run-chat-markdown pre { + background: rgba(9, 16, 28, 0.78); + border: 1px solid rgba(148, 163, 184, 0.22); border-radius: 8px; - color: #cbd5f5; + padding: 0.62rem; + overflow: auto; } - .analysis-output { - background: rgba(15, 23, 42, 0.6); - border: 1px solid var(--lerim-border); - border-radius: 12px; - padding: 1rem; + .run-chat-markdown pre code { + background: transparent; + border: none; + border-radius: 0; + padding: 0; + color: #dbe9ff; + display: block; + white-space: pre; + line-height: 1.4; + } + + .run-chat-markdown a { + color: #67d7ff; + text-decoration: underline; + text-underline-offset: 2px; + } + + .run-chat-tool-toggle { + margin-top: 0.5rem; + border: 1px solid rgba(148, 163, 184, 0.25); + border-radius: 8px; + background: rgba(15, 23, 42, 0.35); + overflow: hidden; + } + + .run-chat-tool-toggle summary { + cursor: pointer; + padding: 0.45rem 0.58rem; + color: #9fb2cd; + font-size: 0.74rem; + letter-spacing: 0.06em; + text-transform: uppercase; + } + + .run-chat-tool-toggle pre { + margin: 0; + border-top: 1px solid rgba(148, 163, 184, 0.18); + padding: 0.62rem; white-space: pre-wrap; - min-height: 120px; + max-height: 260px; + overflow: auto; + } + + @media (max-width: 991.98px) { + .run-viewer-overlay { + padding: 0; + } + + .run-viewer-shell { + width: 100%; + max-height: 100vh; + border-radius: 0; + } + + .run-chat-bubble { + width: 96%; + } } .toast-container { @@ -422,10 +648,10 @@