Compare commits

..
111 changed files with 7429 additions and 8082 deletions

No files matched your search

-4
View File
@@ -7,10 +7,6 @@
[exclude-files] [exclude-files]
# pnpm lockfiles contain lots of high-entropy package integrity blobs. # pnpm lockfiles contain lots of high-entropy package integrity blobs.
pattern = (^|/)pnpm-lock\.yaml$ pattern = (^|/)pnpm-lock\.yaml$
# Generated output and vendored assets.
pattern = (^|/)(dist|vendor)/
# Local config file with allowlist patterns.
pattern = (^|/)\.detect-secrets\.cfg$
[exclude-lines] [exclude-lines]
# Fastlane checks for private key marker; not a real key. # Fastlane checks for private key marker; not a real key.
-17
View File
@@ -1,17 +0,0 @@
# actionlint configuration
# https://github.com/rhysd/actionlint/blob/main/docs/config.md
self-hosted-runner:
labels:
# Blacksmith CI runners
- blacksmith-4vcpu-ubuntu-2404
- blacksmith-4vcpu-windows-2025
# Ignore patterns for known issues
paths:
.github/workflows/**/*.yml:
ignore:
# Ignore shellcheck warnings (we run shellcheck separately)
- 'shellcheck reported issue.+'
# Ignore intentional if: false for disabled jobs
- 'constant expression "false" in condition'
-113
View File
@@ -1,113 +0,0 @@
# Dependabot configuration
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
registries:
npm-npmjs:
type: npm-registry
url: https://registry.npmjs.org
replaces-base: true
updates:
# npm dependencies (root)
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
production:
dependency-type: production
update-types:
- minor
- patch
development:
dependency-type: development
update-types:
- minor
- patch
open-pull-requests-limit: 10
registries:
- npm-npmjs
# GitHub Actions
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
actions:
patterns:
- "*"
update-types:
- minor
- patch
open-pull-requests-limit: 5
# Swift Package Manager - macOS app
- package-ecosystem: swift
directory: /apps/macos
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
swift-deps:
patterns:
- "*"
update-types:
- minor
- patch
open-pull-requests-limit: 5
# Swift Package Manager - shared ClawdbotKit
- package-ecosystem: swift
directory: /apps/shared/ClawdbotKit
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
swift-deps:
patterns:
- "*"
update-types:
- minor
- patch
open-pull-requests-limit: 5
# Swift Package Manager - Swabble
- package-ecosystem: swift
directory: /Swabble
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
swift-deps:
patterns:
- "*"
update-types:
- minor
- patch
open-pull-requests-limit: 5
# Gradle - Android app
- package-ecosystem: gradle
directory: /apps/android
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
android-deps:
patterns:
- "*"
update-types:
- minor
- patch
open-pull-requests-limit: 5
-105
View File
@@ -1,105 +0,0 @@
# Pre-commit hooks for clawdbot
# Install: prek install
# Run manually: prek run --all-files
#
# See https://pre-commit.com for more information
repos:
# Basic file hygiene
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
exclude: '^(docs/|dist/|vendor/|.*\.snap$)'
- id: end-of-file-fixer
exclude: '^(docs/|dist/|vendor/|.*\.snap$)'
- id: check-yaml
args: [--allow-multiple-documents]
- id: check-added-large-files
args: [--maxkb=500]
- id: check-merge-conflict
# Secret detection (same as CI)
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args:
- --baseline
- .secrets.baseline
- --exclude-files
- '(^|/)(dist/|vendor/|pnpm-lock\.yaml$|\.detect-secrets\.cfg$)'
- --exclude-lines
- 'key_content\.include\?\("BEGIN PRIVATE KEY"\)'
- --exclude-lines
- 'case \.apiKeyEnv: "API key \(env var\)"'
- --exclude-lines
- 'case apikey = "apiKey"'
- --exclude-lines
- '"gateway\.remote\.password"'
- --exclude-lines
- '"gateway\.auth\.password"'
- --exclude-lines
- '"talk\.apiKey"'
- --exclude-lines
- '=== "string"'
- --exclude-lines
- 'typeof remote\?\.password === "string"'
# Shell script linting
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.11.0
hooks:
- id: shellcheck
args: [--severity=error] # Only fail on errors, not warnings/info
# Exclude vendor and scripts with embedded code or known issues
exclude: '^(vendor/|scripts/e2e/)'
# GitHub Actions linting
- repo: https://github.com/rhysd/actionlint
rev: v1.7.10
hooks:
- id: actionlint
# GitHub Actions security audit
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.22.0
hooks:
- id: zizmor
args: [--persona=regular, --min-severity=medium, --min-confidence=medium]
exclude: '^(vendor/|Swabble/)'
# Project checks (same commands as CI)
- repo: local
hooks:
# oxlint --type-aware src test
- id: oxlint
name: oxlint
entry: scripts/pre-commit/run-node-tool.sh oxlint --type-aware src test
language: system
pass_filenames: false
types_or: [javascript, jsx, ts, tsx]
# oxfmt --check src test
- id: oxfmt
name: oxfmt
entry: scripts/pre-commit/run-node-tool.sh oxfmt --check src test
language: system
pass_filenames: false
types_or: [javascript, jsx, ts, tsx]
# swiftlint (same as CI)
- id: swiftlint
name: swiftlint
entry: swiftlint --config .swiftlint.yml
language: system
pass_filenames: false
types: [swift]
# swiftformat --lint (same as CI)
- id: swiftformat
name: swiftformat
entry: swiftformat --lint apps/macos/Sources --config .swiftformat
language: system
pass_filenames: false
types: [swift]
+123 -1796
View File
File diff suppressed because it is too large. Load diff
-25
View File
@@ -1,25 +0,0 @@
# ShellCheck configuration
# https://www.shellcheck.net/wiki/
# Disable common false positives and style suggestions
# SC2034: Variable appears unused (often exported or used indirectly)
disable=SC2034
# SC2155: Declare and assign separately (common idiom, rarely causes issues)
disable=SC2155
# SC2295: Expansions inside ${..} need quoting (info-level, rarely causes issues)
disable=SC2295
# SC1012: \r is literal (tr -d '\r' works as intended on most systems)
disable=SC1012
# SC2026: Word outside quotes (info-level, often intentional)
disable=SC2026
# SC2016: Expressions don't expand in single quotes (often intentional in sed/awk)
disable=SC2016
# SC2129: Consider using { cmd1; cmd2; } >> file (style preference)
disable=SC2129
-1
View File
@@ -37,7 +37,6 @@
## Build, Test, and Development Commands ## Build, Test, and Development Commands
- Runtime baseline: Node **22+** (keep Node + Bun paths working). - Runtime baseline: Node **22+** (keep Node + Bun paths working).
- Install deps: `pnpm install` - Install deps: `pnpm install`
- Pre-commit hooks: `prek install` (runs same checks as CI)
- Also supported: `bun install` (keep `pnpm-lock.yaml` + Bun patching in sync when touching deps/patches). - Also supported: `bun install` (keep `pnpm-lock.yaml` + Bun patching in sync when touching deps/patches).
- Prefer Bun for TypeScript execution (scripts, dev, tests): `bun <file.ts>` / `bunx <tool>`. - Prefer Bun for TypeScript execution (scripts, dev, tests): `bun <file.ts>` / `bunx <tool>`.
- Run CLI in dev: `pnpm clawdbot ...` (bun) or `pnpm dev`. - Run CLI in dev: `pnpm clawdbot ...` (bun) or `pnpm dev`.
+1 -6
View File
@@ -12,7 +12,6 @@ Docs: https://docs.clawd.bot
- TTS: add Edge TTS provider fallback, defaulting to keyless Edge with MP3 retry on format failures. (#1668) Thanks @steipete. https://docs.clawd.bot/tts - TTS: add Edge TTS provider fallback, defaulting to keyless Edge with MP3 retry on format failures. (#1668) Thanks @steipete. https://docs.clawd.bot/tts
- Web search: add Brave freshness filter parameter for time-scoped results. (#1688) Thanks @JonUleis. https://docs.clawd.bot/tools/web - Web search: add Brave freshness filter parameter for time-scoped results. (#1688) Thanks @JonUleis. https://docs.clawd.bot/tools/web
- TTS: add auto mode enum (off/always/inbound/tagged) with per-session `/tts` override. (#1667) Thanks @sebslight. https://docs.clawd.bot/tts - TTS: add auto mode enum (off/always/inbound/tagged) with per-session `/tts` override. (#1667) Thanks @sebslight. https://docs.clawd.bot/tts
- Dev: add prek pre-commit hooks + dependabot config for weekly updates. (#1720) Thanks @dguido.
- Docs: expand FAQ (migration, scheduling, concurrency, model recommendations, OpenAI subscription auth, Pi sizing, hackable install, docs SSL workaround). - Docs: expand FAQ (migration, scheduling, concurrency, model recommendations, OpenAI subscription auth, Pi sizing, hackable install, docs SSL workaround).
- Docs: add verbose installer troubleshooting guidance. - Docs: add verbose installer troubleshooting guidance.
- Docs: add macOS VM guide with local/hosted options + VPS/nodes guidance. (#1693) Thanks @f-trycua. - Docs: add macOS VM guide with local/hosted options + VPS/nodes guidance. (#1693) Thanks @f-trycua.
@@ -23,16 +22,13 @@ Docs: https://docs.clawd.bot
- Telegram: add `channels.telegram.linkPreview` to toggle outbound link previews. (#1700) Thanks @zerone0x. https://docs.clawd.bot/channels/telegram - Telegram: add `channels.telegram.linkPreview` to toggle outbound link previews. (#1700) Thanks @zerone0x. https://docs.clawd.bot/channels/telegram
- Telegram: treat DM topics as separate sessions and keep DM history limits stable with thread suffixes. (#1597) Thanks @rohannagpal. - Telegram: treat DM topics as separate sessions and keep DM history limits stable with thread suffixes. (#1597) Thanks @rohannagpal.
- Telegram: add verbose raw-update logging for inbound Telegram updates. (#1597) Thanks @rohannagpal. - Telegram: add verbose raw-update logging for inbound Telegram updates. (#1597) Thanks @rohannagpal.
- Diagnostics: add diagnostic flags for targeted debug logs (config + env override). https://docs.clawd.bot/diagnostics/flags
### Fixes ### Fixes
- Gateway: include inline config env vars in service install environments. (#1735) Thanks @Seredeep.
- BlueBubbles: route phone-number targets to DMs, avoid leaking routing IDs, and auto-create missing DMs (Private API required). (#1751) Thanks @tyler6204. https://docs.clawd.bot/channels/bluebubbles
- BlueBubbles: keep part-index GUIDs in reply tags when short IDs are missing. - BlueBubbles: keep part-index GUIDs in reply tags when short IDs are missing.
- Web UI: hide internal `message_id` hints in chat bubbles. - Web UI: hide internal `message_id` hints in chat bubbles.
- Web UI: show Stop button during active runs, swap back to New session when idle. (#1664) Thanks @ndbroadbent. - Web UI: show Stop button during active runs, swap back to New session when idle. (#1664) Thanks @ndbroadbent.
- Web UI: clear stale disconnect banners on reconnect; allow form saves with unsupported schema paths but block missing schema. (#1707) Thanks @Glucksberg. - Web UI: clear stale disconnect banners on reconnect; allow form saves with unsupported schema paths but block missing schema. (#1707) Thanks @Glucksberg.
- Web UI: enable settings page scrolling on Windows. (#1780) Thanks @robbyczgw-cla. - Auto-reply: don't treat `/models` as a `/model` directive. (#1753) Thanks @uos-status.
- Heartbeat: normalize target identifiers for consistent routing. - Heartbeat: normalize target identifiers for consistent routing.
- TUI: reload history after gateway reconnect to restore session state. (#1663) - TUI: reload history after gateway reconnect to restore session state. (#1663)
- Telegram: use wrapped fetch for long-polling on Node to normalize AbortSignal handling. (#1639) - Telegram: use wrapped fetch for long-polling on Node to normalize AbortSignal handling. (#1639)
@@ -43,7 +39,6 @@ Docs: https://docs.clawd.bot
- Agents: auto-compact on context overflow prompt errors before failing. (#1627) Thanks @rodrigouroz. - Agents: auto-compact on context overflow prompt errors before failing. (#1627) Thanks @rodrigouroz.
- Agents: use the active auth profile for auto-compaction recovery. - Agents: use the active auth profile for auto-compaction recovery.
- Models: default missing custom provider fields so minimal configs are accepted. - Models: default missing custom provider fields so minimal configs are accepted.
- Media understanding: skip image understanding when the primary model already supports vision. (#1747) Thanks @tyler6204.
- Gateway: skip Tailscale DNS probing when tailscale.mode is off. (#1671) - Gateway: skip Tailscale DNS probing when tailscale.mode is off. (#1671)
- Gateway: reduce log noise for late invokes + remote node probes; debounce skills refresh. (#1607) Thanks @petter-b. - Gateway: reduce log noise for late invokes + remote node probes; debounce skills refresh. (#1607) Thanks @petter-b.
- Gateway: clarify Control UI/WebChat auth error hints for missing tokens. (#1690) - Gateway: clarify Control UI/WebChat auth error hints for missing tokens. (#1690)
+1
View File
@@ -12,3 +12,4 @@ If you believe you’ve found a security issue in Clawdbot, please report it pri
For threat model + hardening guidance (including `clawdbot security audit --deep` and `--fix`), see: For threat model + hardening guidance (including `clawdbot security audit --deep` and `--fix`), see:
- `https://docs.clawd.bot/gateway/security` - `https://docs.clawd.bot/gateway/security`
@@ -12,3 +12,4 @@ data class CameraHudState(
val kind: CameraHudKind, val kind: CameraHudKind,
val message: String, val message: String,
) )
@@ -12,3 +12,4 @@ enum class VoiceWakeMode(val rawValue: String) {
} }
} }
} }
@@ -1,3 +1,4 @@
<resources> <resources>
<color name="ic_launcher_background">#0A0A0A</color> <color name="ic_launcher_background">#0A0A0A</color>
</resources> </resources>
@@ -1,3 +1,4 @@
<resources> <resources>
<string name="app_name">Clawdbot Node</string> <string name="app_name">Clawdbot Node</string>
</resources> </resources>
@@ -23,3 +23,4 @@ class VoiceWakeCommandExtractorTest {
assertNull(VoiceWakeCommandExtractor.extractCommand("hey claude!", listOf("claude"))) assertNull(VoiceWakeCommandExtractor.extractCommand("hey claude!", listOf("claude")))
} }
} }
+1
View File
@@ -16,3 +16,4 @@ dependencyResolutionManagement {
rootProject.name = "ClawdbotNodeAndroid" rootProject.name = "ClawdbotNodeAndroid"
include(":app") include(":app")
+1
View File
@@ -3,3 +3,4 @@ parent_config: ../../.swiftlint.yml
included: included:
- Sources - Sources
- ../shared/ClawdisNodeKit/Sources - ../shared/ClawdisNodeKit/Sources
-1
View File
@@ -1 +0,0 @@
@import"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap";:root{--bg: #0c0d12;--bg-accent: #0d0e14;--bg-elevated: #181a21;--bg-hover: #252830;--bg-muted: #252830;--card: #13151c;--card-foreground: #f8fafc;--card-highlight: rgba(255, 255, 255, .04);--popover: #13151c;--popover-foreground: #f8fafc;--panel: #0c0d12;--panel-strong: #181a21;--panel-hover: #252830;--chrome: rgba(12, 13, 18, .95);--chrome-strong: rgba(12, 13, 18, .98);--text: #f8fafc;--text-strong: #ffffff;--chat-text: #f8fafc;--muted: #94a3b8;--muted-strong: #64748b;--muted-foreground: #94a3b8;--border: #333842;--border-strong: #454d5c;--border-hover: #5a6373;--input: #333842;--ring: #ff4d4d;--accent: #ff4d4d;--accent-hover: #ff6666;--accent-muted: #ff4d4d;--accent-subtle: rgba(255, 77, 77, .12);--accent-foreground: #f8fafc;--primary: #ff4d4d;--primary-foreground: #ffffff;--secondary: #252830;--secondary-foreground: #f8fafc;--accent-2: #3b82f6;--accent-2-muted: rgba(59, 130, 246, .7);--ok: #22c55e;--ok-muted: rgba(34, 197, 94, .7);--ok-subtle: rgba(34, 197, 94, .1);--destructive: #ef4444;--destructive-foreground: #fafafa;--warn: #eab308;--warn-muted: rgba(234, 179, 8, .7);--warn-subtle: rgba(234, 179, 8, .1);--danger: #ef4444;--danger-muted: rgba(239, 68, 68, .7);--danger-subtle: rgba(239, 68, 68, .1);--info: #3b82f6;--focus: rgba(255, 77, 77, .2);--focus-ring: 0 0 0 2px var(--bg), 0 0 0 4px var(--ring);--grid-line: rgba(255, 255, 255, .03);--theme-switch-x: 50%;--theme-switch-y: 50%;--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace;--font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;--font-display: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, .05);--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, .1), 0 2px 4px -2px rgba(0, 0, 0, .1);--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, .1), 0 4px 6px -4px rgba(0, 0, 0, .1);--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, .1), 0 8px 10px -6px rgba(0, 0, 0, .1);--radius-sm: 4px;--radius-md: 6px;--radius-lg: 8px;--radius-xl: 12px;--radius-full: 9999px;--radius: 6px;--ease-out: cubic-bezier(.16, 1, .3, 1);--ease-in-out: cubic-bezier(.4, 0, .2, 1);--duration-fast: .15s;--duration-normal: .2s;--duration-slow: .3s;color-scheme:dark}:root[data-theme=light]{--bg: #f8f8f7;--bg-accent: #f3f2f0;--bg-elevated: #ffffff;--bg-hover: #eae8e6;--bg-muted: #eae8e6;--bg-content: #f0efed;--card: #ffffff;--card-foreground: #1c1917;--card-highlight: rgba(0, 0, 0, .04);--popover: #ffffff;--popover-foreground: #1c1917;--panel: #f8f8f7;--panel-strong: #f0efed;--panel-hover: #e5e3e1;--chrome: rgba(248, 248, 247, .95);--chrome-strong: rgba(248, 248, 247, .98);--text: #44403c;--text-strong: #292524;--chat-text: #44403c;--muted: #5c5856;--muted-strong: #44403c;--muted-foreground: #5c5856;--border: #e0dedc;--border-strong: #d6d3d1;--border-hover: #a8a5a0;--input: #e0dedc;--accent: #b91c1c;--accent-hover: #dc2626;--accent-muted: #b91c1c;--accent-subtle: rgba(185, 28, 28, .18);--accent-foreground: #ffffff;--primary: #b91c1c;--primary-foreground: #ffffff;--secondary: #eae8e6;--secondary-foreground: #44403c;--ok: #15803d;--ok-muted: rgba(21, 128, 61, .75);--ok-subtle: rgba(21, 128, 61, .12);--destructive: #b91c1c;--destructive-foreground: #fafafa;--warn: #a16207;--warn-muted: rgba(161, 98, 7, .75);--warn-subtle: rgba(161, 98, 7, .12);--danger: #b91c1c;--danger-muted: rgba(185, 28, 28, .75);--danger-subtle: rgba(185, 28, 28, .12);--info: #1d4ed8;--focus: rgba(185, 28, 28, .25);--grid-line: rgba(0, 0, 0, .06);color-scheme:light}*{box-sizing:border-box}html,body{height:100%}body{margin:0;font:400 14px/1.5 var(--font-body);letter-spacing:-.011em;background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@keyframes theme-circle-transition{0%{clip-path:circle(0% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%))}to{clip-path:circle(150% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%))}}html.theme-transition{view-transition-name:theme}html.theme-transition::view-transition-old(theme){mix-blend-mode:normal;animation:none;z-index:1}html.theme-transition::view-transition-new(theme){mix-blend-mode:normal;z-index:2;animation:theme-circle-transition .4s var(--ease-out) forwards}@media(prefers-reduced-motion:reduce){html.theme-transition::view-transition-old(theme),html.theme-transition::view-transition-new(theme){animation:none!important}}clawdbot-app{display:block;position:relative;z-index:1;min-height:100vh}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}button,input,textarea,select{font:inherit;color:inherit}::selection{background:var(--accent-subtle);color:var(--text-strong)}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border);border-radius:var(--radius-full)}::-webLine truncated
+1
View File
@@ -0,0 +1 @@
@import"https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Unbounded:wght@400;500;600&family=Work+Sans:wght@400;500;600;700&display=swap";:root{--bg: #0a0f14;--bg-accent: #111826;--bg-grad-1: #162031;--bg-grad-2: #1f2a22;--bg-overlay: rgba(255, 255, 255, .05);--bg-glow: rgba(245, 159, 74, .12);--panel: rgba(14, 20, 30, .88);--panel-strong: rgba(18, 26, 38, .96);--chrome: rgba(9, 14, 20, .72);--chrome-strong: rgba(9, 14, 20, .86);--text: rgba(244, 246, 251, .96);--chat-text: rgba(231, 237, 244, .92);--muted: rgba(156, 169, 189, .72);--border: rgba(255, 255, 255, .09);--border-strong: rgba(255, 255, 255, .16);--accent: #f59f4a;--accent-2: #34c7b7;--ok: #2bd97f;--warn: #f2c94c;--danger: #ff6b6b;--focus: rgba(245, 159, 74, .35);--grid-line: rgba(255, 255, 255, .04);--theme-switch-x: 50%;--theme-switch-y: 50%;--mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--font-body: "Work Sans", system-ui, sans-serif;--font-display: "Unbounded", "Times New Roman", serif;color-scheme:dark}:root[data-theme=light]{--bg: #f5f1ea;--bg-accent: #ffffff;--bg-grad-1: #f1e6d6;--bg-grad-2: #e5eef4;--bg-overlay: rgba(28, 32, 46, .05);--bg-glow: rgba(52, 199, 183, .14);--panel: rgba(255, 255, 255, .9);--panel-strong: rgba(255, 255, 255, .97);--chrome: rgba(255, 255, 255, .75);--chrome-strong: rgba(255, 255, 255, .88);--text: rgba(27, 36, 50, .98);--chat-text: rgba(36, 48, 66, .9);--muted: rgba(80, 94, 114, .7);--border: rgba(18, 24, 40, .12);--border-strong: rgba(18, 24, 40, .2);--accent: #e28a3f;--accent-2: #1ba99d;--ok: #1aa86c;--warn: #b3771c;--danger: #d44848;--focus: rgba(226, 138, 63, .35);--grid-line: rgba(18, 24, 40, .06);color-scheme:light}*{box-sizing:border-box}html,body{height:100%}body{margin:0;font:15px/1.5 var(--font-body);background:radial-gradient(1200px 900px at 15% -10%,var(--bg-grad-1) 0%,transparent 55%) fixed,radial-gradient(900px 700px at 80% 10%,var(--bg-grad-2) 0%,transparent 60%) fixed,linear-gradient(160deg,var(--bg) 0%,var(--bg-accent) 100%) fixed;color:var(--text)}body:before{content:"";position:fixed;inset:0;background:linear-gradient(140deg,var(--bg-overlay) 0%,rgba(255,255,255,0) 40%),radial-gradient(620px 420px at 75% 75%,var(--bg-glow),transparent 60%);pointer-events:none;z-index:0}@keyframes theme-circle-transition{0%{clip-path:circle(0% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%))}to{clip-path:circle(150% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%))}}html.theme-transition{view-transition-name:theme}html.theme-transition::view-transition-old(theme){mix-blend-mode:normal;animation:none;z-index:1}html.theme-transition::view-transition-new(theme){mix-blend-mode:normal;z-index:2;animation:theme-circle-transition .45s ease-out forwards}@media(prefers-reduced-motion:reduce){html.theme-transition::view-transition-old(theme),html.theme-transition::view-transition-new(theme){animation:none!important}}clawdbot-app{display:block;position:relative;z-index:1;min-height:100vh}a{color:inherit}button,input,textarea,select{font:inherit;color:inherit}@keyframes rise{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}@keyframes dashboard-enter{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.shell{--shell-pad: 16px;--shell-gap: 16px;--shell-nav-width: 220px;--shell-topbar-height: 56px;--shell-focus-duration: .22s;--shell-focus-ease: cubic-bezier(.2, .85, .25, 1);height:100vh;display:grid;grid-template-columns:var(--shell-nav-width) minmax(0,1fr);grid-template-rows:var(--shell-topbar-height) 1fr;grid-template-areas:"topbar topbar" "nav content";gap:0;animation:dashboard-enter .6s ease-out;transition:grid-template-columns var(--shell-focus-duration) var(--shell-focus-ease);overflow:hidden}@supports (height: 100dvh){.shell{height:100dvh}}.shell--chat{min-height:100vh;height:100vh;overflow:hidden}@supports (height: 100dvh){.shell--chat{height:100dvh}}.shell--nav-collapsed,.shell--chat-focus{grid-template-columns:0px minmax(0,1fr)}.shell--onboarding{grid-template-rows:0 1fr}.shell--onboarding .topbar{display:none}.shell--onboarding .content{padding-top:0}.shell--chat-focus .content{padding-top:0;gap:0}.topbar{grid-area:topbar;position:sticky;top:0;z-index:40;display:flex;justify-content:space-between;align-items:center;gap:16px;padding:0 20px;height:var(--shell-topbar-height);border-bottom:1px solid var(--border);background:var(--panel);-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px)}.topbar-left{display:flex;align-items:center;gap:12px}.topbar .nav-collapse-toggle{width:44px;height:44px;margin-bottom:0}.topbar .nav-collapse-toggle__icon{font-size:22px}.brand{display:flex;flex-direction:column;gap:2px}.brand-title{font-family:var(--font-display);font-size:16px;letter-spacing:1px;text-transform:uppercase;font-weight:600;line-height:1.1}.brand-sub{font-size:10px;color:var(--muted);letter-spacing:.8px;text-transform:uppercase;line-heightLine truncated
File diff suppressed because it is too large. Load diff
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"index-DQcOTEYz.js","sources":["../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/css-tag.js","../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/reactive-element.js","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/lit-html.js","../../../node_modules/.pnpm/lit-element@4.2.2/node_modules/lit-element/lit-element.js","../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/decorators/custom-element.js","../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/decorators/property.js","../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/decorators/state.js","../../../ui/src/ui/assistant-identity.ts","../../../ui/src/ui/storage.ts","../../../src/sessions/session-key-utils.ts","../../../ui/src/ui/navigation.ts","../../../ui/src/ui/icons.ts","../../../src/shared/text/reasoning-tags.ts","../../../ui/src/ui/format.ts","../../../ui/src/ui/chat/message-extract.ts","../../../ui/src/ui/uuid.ts","../../../ui/src/ui/controllers/chat.ts","../../../ui/src/ui/controllers/sessions.ts","../../../ui/src/ui/app-tool-stream.ts","../../../ui/src/ui/app-scroll.ts","../../../ui/src/ui/controllers/config/form-utils.ts","../../../ui/src/ui/controllers/config.ts","../../../ui/src/ui/controllers/cron.ts","../../../ui/src/ui/controllers/channels.ts","../../../ui/src/ui/controllers/debug.ts","../../../ui/src/ui/controllers/logs.ts","../../../node_modules/.pnpm/@noble+ed25519@3.0.0/node_modules/@noble/ed25519/index.js","../../../ui/src/ui/device-identity.ts","../../../ui/src/ui/device-auth.ts","../../../ui/src/ui/controllers/devices.ts","../../../ui/src/ui/controllers/nodes.ts","../../../ui/src/ui/controllers/exec-approvals.ts","../../../ui/src/ui/controllers/presence.ts","../../../ui/src/ui/controllers/skills.ts","../../../ui/src/ui/theme.ts","../../../ui/src/ui/theme-transition.ts","../../../ui/src/ui/app-polling.ts","../../../ui/src/ui/app-settings.ts","../../../ui/src/ui/app-chat.ts","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/directive.js","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/directive-helpers.js","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/directives/repeat.js","../../../ui/src/ui/chat/message-normalizer.ts","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/directives/unsafe-html.js","../../../node_modules/.pnpm/dompurify@3.3.1/node_modules/dompurify/dist/purify.es.mjs","../../../node_modules/.pnpm/marked@17.0.1/node_modules/marked/lib/marked.esm.js","../../../ui/src/ui/markdown.ts","../../../ui/src/ui/chat/copy-as-markdown.ts","../../../ui/src/ui/tool-display.ts","../../../ui/src/ui/chat/constants.ts","../../../ui/src/ui/chat/tool-helpers.ts","../../../ui/src/ui/chat/tool-cards.ts","../../../ui/src/ui/chat/grouped-render.ts","../../../ui/src/ui/views/markdown-sidebar.ts","../../../ui/src/ui/components/resizable-divider.ts","../../../ui/src/ui/views/chat.ts","../../../ui/src/ui/views/config-form.shared.ts","../../../ui/src/ui/views/config-form.node.ts","../../../ui/src/ui/views/config-form.render.ts","../../../ui/src/ui/views/config-form.analyze.ts","../../../ui/src/ui/views/config.ts","../../../ui/src/ui/views/channels.shared.ts","../../../ui/src/ui/views/channels.config.ts","../../../ui/src/ui/views/channels.discord.ts","../../../ui/src/ui/views/channels.googlechat.ts","../../../ui/src/ui/views/channels.imessage.ts","../../../ui/src/ui/views/channels.nostr-profile-form.ts","../../../ui/src/ui/views/channels.nostr.ts","../../../ui/src/ui/views/channels.signal.ts","../../../ui/src/ui/views/channels.slack.ts","../../../ui/src/ui/views/channels.telegram.ts","../../../ui/src/ui/views/channels.whatsapp.ts","../../../ui/src/ui/views/channels.ts","../../../ui/src/ui/presenter.ts","../../../ui/src/ui/views/cron.ts","../../../ui/src/ui/views/debug.ts","../../../ui/src/ui/views/instances.ts","../../../ui/src/ui/views/logs.ts","../../../ui/src/ui/views/nodes.ts","../../../ui/src/ui/views/overview.ts","../../../ui/src/ui/views/sessions.ts","../../../ui/src/ui/views/exec-approval.ts","../../../ui/src/ui/views/skills.ts","../../../ui/src/ui/app-render.helpers.ts","../../../ui/src/ui/app-render.ts","../../../ui/src/ui/app-defaults.ts","../../../ui/src/ui/controllers/agents.ts","../../../src/gateway/protocol/client-info.ts","../../../src/gateway/device-auth.ts","../../../ui/src/ui/gateway.ts","../../../ui/src/ui/controllers/exec-approval.ts","../../../ui/src/ui/controllers/assistant-identity.ts","../../../ui/src/ui/app-gateway.ts","../../../ui/src/ui/app-lifecycle.ts","../../../ui/src/ui/app-channels.ts","../../../ui/src/ui/app.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\nconst t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow)&&\"adoptedStyLine truncated
File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index-DsXRcnEw.js","sources":["../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/css-tag.js","../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/reactive-element.js","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/lit-html.js","../../../node_modules/.pnpm/lit-element@4.2.2/node_modules/lit-element/lit-element.js","../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/decorators/custom-element.js","../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/decorators/property.js","../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/decorators/state.js","../../../ui/src/ui/assistant-identity.ts","../../../ui/src/ui/storage.ts","../../../src/sessions/session-key-utils.ts","../../../ui/src/ui/navigation.ts","../../../ui/src/ui/format.ts","../../../ui/src/ui/chat/message-extract.ts","../../../ui/src/ui/uuid.ts","../../../ui/src/ui/controllers/chat.ts","../../../ui/src/ui/controllers/sessions.ts","../../../ui/src/ui/app-tool-stream.ts","../../../ui/src/ui/app-scroll.ts","../../../ui/src/ui/controllers/config/form-utils.ts","../../../ui/src/ui/controllers/config.ts","../../../ui/src/ui/controllers/cron.ts","../../../ui/src/ui/controllers/channels.ts","../../../ui/src/ui/controllers/debug.ts","../../../ui/src/ui/controllers/logs.ts","../../../node_modules/.pnpm/@noble+ed25519@3.0.0/node_modules/@noble/ed25519/index.js","../../../ui/src/ui/device-identity.ts","../../../ui/src/ui/device-auth.ts","../../../ui/src/ui/controllers/devices.ts","../../../ui/src/ui/controllers/nodes.ts","../../../ui/src/ui/controllers/exec-approvals.ts","../../../ui/src/ui/controllers/presence.ts","../../../ui/src/ui/controllers/skills.ts","../../../ui/src/ui/theme.ts","../../../ui/src/ui/theme-transition.ts","../../../ui/src/ui/app-polling.ts","../../../ui/src/ui/app-settings.ts","../../../ui/src/ui/app-chat.ts","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/directive.js","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/directive-helpers.js","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/directives/repeat.js","../../../ui/src/ui/chat/message-normalizer.ts","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/directives/unsafe-html.js","../../../node_modules/.pnpm/dompurify@3.3.1/node_modules/dompurify/dist/purify.es.mjs","../../../node_modules/.pnpm/marked@17.0.1/node_modules/marked/lib/marked.esm.js","../../../ui/src/ui/markdown.ts","../../../ui/src/ui/icons.ts","../../../ui/src/ui/chat/copy-as-markdown.ts","../../../ui/src/ui/tool-display.ts","../../../ui/src/ui/chat/constants.ts","../../../ui/src/ui/chat/tool-helpers.ts","../../../ui/src/ui/chat/tool-cards.ts","../../../ui/src/ui/chat/grouped-render.ts","../../../ui/src/ui/views/markdown-sidebar.ts","../../../ui/src/ui/components/resizable-divider.ts","../../../ui/src/ui/views/chat.ts","../../../ui/src/ui/views/config-form.shared.ts","../../../ui/src/ui/views/config-form.node.ts","../../../ui/src/ui/views/config-form.render.ts","../../../ui/src/ui/views/config-form.analyze.ts","../../../ui/src/ui/views/config.ts","../../../ui/src/ui/views/channels.shared.ts","../../../ui/src/ui/views/channels.config.ts","../../../ui/src/ui/views/channels.discord.ts","../../../ui/src/ui/views/channels.imessage.ts","../../../ui/src/ui/views/channels.nostr-profile-form.ts","../../../ui/src/ui/views/channels.nostr.ts","../../../ui/src/ui/views/channels.signal.ts","../../../ui/src/ui/views/channels.slack.ts","../../../ui/src/ui/views/channels.telegram.ts","../../../ui/src/ui/views/channels.whatsapp.ts","../../../ui/src/ui/views/channels.ts","../../../ui/src/ui/presenter.ts","../../../ui/src/ui/views/cron.ts","../../../ui/src/ui/views/debug.ts","../../../ui/src/ui/views/instances.ts","../../../ui/src/ui/views/logs.ts","../../../ui/src/ui/views/nodes.ts","../../../ui/src/ui/views/overview.ts","../../../ui/src/ui/views/sessions.ts","../../../ui/src/ui/views/exec-approval.ts","../../../ui/src/ui/views/skills.ts","../../../ui/src/ui/app-render.helpers.ts","../../../ui/src/ui/app-render.ts","../../../ui/src/ui/app-defaults.ts","../../../ui/src/ui/controllers/agents.ts","../../../src/gateway/protocol/client-info.ts","../../../src/gateway/device-auth.ts","../../../ui/src/ui/gateway.ts","../../../ui/src/ui/controllers/exec-approval.ts","../../../ui/src/ui/controllers/assistant-identity.ts","../../../ui/src/ui/app-gateway.ts","../../../ui/src/ui/app-lifecycle.ts","../../../ui/src/ui/app-channels.ts","../../../ui/src/ui/app.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\nconst t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow)&&\"adoptedStyleSheets\"in Document.prototype&&\"replace\"in CSSStyleSheet.prototype,s=Symbol(),o=new WeakMapLine truncated
File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index-bYQnHP3a.js","sources":["../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/css-tag.js","../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/reactive-element.js","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/lit-html.js","../../../node_modules/.pnpm/lit-element@4.2.2/node_modules/lit-element/lit-element.js","../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/decorators/custom-element.js","../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/decorators/property.js","../../../node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/decorators/state.js","../../../ui/src/ui/assistant-identity.ts","../../../ui/src/ui/storage.ts","../../../src/sessions/session-key-utils.ts","../../../ui/src/ui/navigation.ts","../../../ui/src/ui/format.ts","../../../ui/src/ui/chat/message-extract.ts","../../../ui/src/ui/uuid.ts","../../../ui/src/ui/controllers/chat.ts","../../../ui/src/ui/controllers/sessions.ts","../../../ui/src/ui/app-tool-stream.ts","../../../ui/src/ui/app-scroll.ts","../../../ui/src/ui/controllers/config/form-utils.ts","../../../ui/src/ui/controllers/config.ts","../../../ui/src/ui/controllers/cron.ts","../../../ui/src/ui/controllers/channels.ts","../../../ui/src/ui/controllers/debug.ts","../../../ui/src/ui/controllers/logs.ts","../../../node_modules/.pnpm/@noble+ed25519@3.0.0/node_modules/@noble/ed25519/index.js","../../../ui/src/ui/device-identity.ts","../../../ui/src/ui/device-auth.ts","../../../ui/src/ui/controllers/devices.ts","../../../ui/src/ui/controllers/nodes.ts","../../../ui/src/ui/controllers/exec-approvals.ts","../../../ui/src/ui/controllers/presence.ts","../../../ui/src/ui/controllers/skills.ts","../../../ui/src/ui/theme.ts","../../../ui/src/ui/theme-transition.ts","../../../ui/src/ui/app-polling.ts","../../../ui/src/ui/app-settings.ts","../../../ui/src/ui/app-chat.ts","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/directive.js","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/directive-helpers.js","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/directives/repeat.js","../../../ui/src/ui/chat/message-normalizer.ts","../../../node_modules/.pnpm/lit-html@3.3.2/node_modules/lit-html/directives/unsafe-html.js","../../../node_modules/.pnpm/dompurify@3.3.1/node_modules/dompurify/dist/purify.es.mjs","../../../node_modules/.pnpm/marked@17.0.1/node_modules/marked/lib/marked.esm.js","../../../ui/src/ui/markdown.ts","../../../ui/src/ui/icons.ts","../../../ui/src/ui/chat/copy-as-markdown.ts","../../../ui/src/ui/tool-display.ts","../../../ui/src/ui/chat/constants.ts","../../../ui/src/ui/chat/tool-helpers.ts","../../../ui/src/ui/chat/tool-cards.ts","../../../ui/src/ui/chat/grouped-render.ts","../../../ui/src/ui/views/markdown-sidebar.ts","../../../ui/src/ui/components/resizable-divider.ts","../../../ui/src/ui/views/chat.ts","../../../ui/src/ui/views/config-form.shared.ts","../../../ui/src/ui/views/config-form.node.ts","../../../ui/src/ui/views/config-form.render.ts","../../../ui/src/ui/views/config-form.analyze.ts","../../../ui/src/ui/views/config.ts","../../../ui/src/ui/views/channels.shared.ts","../../../ui/src/ui/views/channels.config.ts","../../../ui/src/ui/views/channels.discord.ts","../../../ui/src/ui/views/channels.imessage.ts","../../../ui/src/ui/views/channels.nostr-profile-form.ts","../../../ui/src/ui/views/channels.nostr.ts","../../../ui/src/ui/views/channels.signal.ts","../../../ui/src/ui/views/channels.slack.ts","../../../ui/src/ui/views/channels.telegram.ts","../../../ui/src/ui/views/channels.whatsapp.ts","../../../ui/src/ui/views/channels.ts","../../../ui/src/ui/presenter.ts","../../../ui/src/ui/views/cron.ts","../../../ui/src/ui/views/debug.ts","../../../ui/src/ui/views/instances.ts","../../../ui/src/ui/views/logs.ts","../../../ui/src/ui/views/nodes.ts","../../../ui/src/ui/views/overview.ts","../../../ui/src/ui/views/sessions.ts","../../../ui/src/ui/views/exec-approval.ts","../../../ui/src/ui/views/skills.ts","../../../ui/src/ui/app-render.helpers.ts","../../../ui/src/ui/app-render.ts","../../../ui/src/ui/app-defaults.ts","../../../ui/src/ui/controllers/agents.ts","../../../src/gateway/protocol/client-info.ts","../../../src/gateway/device-auth.ts","../../../ui/src/ui/gateway.ts","../../../ui/src/ui/controllers/exec-approval.ts","../../../ui/src/ui/controllers/assistant-identity.ts","../../../ui/src/ui/app-gateway.ts","../../../ui/src/ui/app-lifecycle.ts","../../../ui/src/ui/app-channels.ts","../../../ui/src/ui/app.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2019 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\nconst t=globalThis,e=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow)&&\"adoptedStyleSheets\"in Document.prototype&&\"replace\"in CSSStyleSheet.prototype,s=Symbol(),o=new WeakMapLine truncated
+2 -2
View File
@@ -6,8 +6,8 @@
<title>Clawdbot Control</title> <title>Clawdbot Control</title>
<meta name="color-scheme" content="dark light" /> <meta name="color-scheme" content="dark light" />
<link rel="icon" href="./favicon.ico" sizes="any" /> <link rel="icon" href="./favicon.ico" sizes="any" />
<script type="module" crossorigin src="./assets/index-DQcOTEYz.js"></script> <script type="module" crossorigin src="./assets/index-DsXRcnEw.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-08nzABV3.css"> <link rel="stylesheet" crossorigin href="./assets/index-BvhR9FCb.css">
</head> </head>
<body> <body>
<clawdbot-app></clawdbot-app> <clawdbot-app></clawdbot-app>
-1
View File
@@ -213,7 +213,6 @@ Prefer `chat_guid` for stable routing:
- `chat_id:123` - `chat_id:123`
- `chat_identifier:...` - `chat_identifier:...`
- Direct handles: `+15555550123`, `user@example.com` - Direct handles: `+15555550123`, `user@example.com`
- If a direct handle does not have an existing DM chat, Clawdbot will create one via `POST /api/v1/chat/new`. This requires the BlueBubbles Private API to be enabled.
## Security ## Security
- Webhook requests are authenticated by comparing `guid`/`password` query params or headers against `channels.bluebubbles.password`. Requests from `localhost` are also accepted. - Webhook requests are authenticated by comparing `guid`/`password` query params or headers against `channels.bluebubbles.password`. Requests from `localhost` are also accepted.
-89
View File
@@ -1,89 +0,0 @@
---
summary: "Diagnostics flags for targeted debug logs"
read_when:
- You need targeted debug logs without raising global logging levels
- You need to capture subsystem-specific logs for support
---
# Diagnostics Flags
Diagnostics flags let you enable targeted debug logs without turning on verbose logging everywhere. Flags are opt-in and have no effect unless a subsystem checks them.
## How it works
- Flags are strings (case-insensitive).
- You can enable flags in config or via an env override.
- Wildcards are supported:
- `telegram.*` matches `telegram.http`
- `*` enables all flags
## Enable via config
```json
{
"diagnostics": {
"flags": ["telegram.http"]
}
}
```
Multiple flags:
```json
{
"diagnostics": {
"flags": ["telegram.http", "gateway.*"]
}
}
```
Restart the gateway after changing flags.
## Env override (one-off)
```bash
CLAWDBOT_DIAGNOSTICS=telegram.http,telegram.payload
```
Disable all flags:
```bash
CLAWDBOT_DIAGNOSTICS=0
```
## Where logs go
Flags emit logs into the standard diagnostics log file. By default:
```
/tmp/clawdbot/clawdbot-YYYY-MM-DD.log
```
If you set `logging.file`, use that path instead. Logs are JSONL (one JSON object per line). Redaction still applies based on `logging.redactSensitive`.
## Extract logs
Pick the latest log file:
```bash
ls -t /tmp/clawdbot/clawdbot-*.log | head -n 1
```
Filter for Telegram HTTP diagnostics:
```bash
rg "telegram http error" /tmp/clawdbot/clawdbot-*.log
```
Or tail while reproducing:
```bash
tail -f /tmp/clawdbot/clawdbot-$(date +%F).log | rg "telegram http error"
```
For remote gateways, you can also use `clawdbot logs --follow` (see [/cli/logs](/cli/logs)).
## Notes
- If `logging.level` is set higher than `warn`, these logs may be suppressed. Default `info` is fine.
- Flags are safe to leave enabled; they only affect log volume for the specific subsystem.
- Use [/logging](/logging) to change log destinations, levels, and redaction.
-11
View File
@@ -138,7 +138,6 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
- [Can I use self-hosted models (llama.cpp, vLLM, Ollama)?](#can-i-use-selfhosted-models-llamacpp-vllm-ollama) - [Can I use self-hosted models (llama.cpp, vLLM, Ollama)?](#can-i-use-selfhosted-models-llamacpp-vllm-ollama)
- [What do Clawd, Flawd, and Krill use for models?](#what-do-clawd-flawd-and-krill-use-for-models) - [What do Clawd, Flawd, and Krill use for models?](#what-do-clawd-flawd-and-krill-use-for-models)
- [How do I switch models on the fly (without restarting)?](#how-do-i-switch-models-on-the-fly-without-restarting) - [How do I switch models on the fly (without restarting)?](#how-do-i-switch-models-on-the-fly-without-restarting)
- [Can I use GPT 5.2 for daily tasks and Codex 5.2 for coding](#can-i-use-gpt-52-for-daily-tasks-and-codex-52-for-coding)
- [Why do I see “Model … is not allowed” and then no reply?](#why-do-i-see-model-is-not-allowed-and-then-no-reply) - [Why do I see “Model … is not allowed” and then no reply?](#why-do-i-see-model-is-not-allowed-and-then-no-reply)
- [Why do I see “Unknown model: minimax/MiniMax-M2.1”?](#why-do-i-see-unknown-model-minimaxminimaxm21) - [Why do I see “Unknown model: minimax/MiniMax-M2.1”?](#why-do-i-see-unknown-model-minimaxminimaxm21)
- [Can I use MiniMax as my default and OpenAI for complex tasks?](#can-i-use-minimax-as-my-default-and-openai-for-complex-tasks) - [Can I use MiniMax as my default and OpenAI for complex tasks?](#can-i-use-minimax-as-my-default-and-openai-for-complex-tasks)
@@ -1948,16 +1947,6 @@ Re-run `/model` **without** the `@profile` suffix:
If you want to return to the default, pick it from `/model` (or send `/model <default provider/model>`). If you want to return to the default, pick it from `/model` (or send `/model <default provider/model>`).
Use `/model status` to confirm which auth profile is active. Use `/model status` to confirm which auth profile is active.
### Can I use GPT 5.2 for daily tasks and Codex 5.2 for coding
Yes. Set one as default and switch as needed:
- **Quick switch (per session):** `/model gpt-5.2` for daily tasks, `/model gpt-5.2-codex` for coding.
- **Default + switch:** set `agents.defaults.model.primary` to `openai-codex/gpt-5.2`, then switch to `openai-codex/gpt-5.2-codex` when coding (or the other way around).
- **Sub-agents:** route coding tasks to sub-agents with a different default model.
See [Models](/concepts/models) and [Slash commands](/tools/slash-commands).
### Why do I see Model is not allowed and then no reply ### Why do I see Model is not allowed and then no reply
If `agents.defaults.models` is set, it becomes the **allowlist** for `/model` and any If `agents.defaults.models` is set, it becomes the **allowlist** for `/model` and any
-24
View File
@@ -192,30 +192,6 @@ Use this if you want diagnostics events available to plugins or custom sinks:
} }
``` ```
### Diagnostics flags (targeted logs)
Use flags to turn on extra, targeted debug logs without raising `logging.level`.
Flags are case-insensitive and support wildcards (e.g. `telegram.*` or `*`).
```json
{
"diagnostics": {
"flags": ["telegram.http"]
}
}
```
Env override (one-off):
```
CLAWDBOT_DIAGNOSTICS=telegram.http,telegram.payload
```
Notes:
- Flag logs go to the standard log file (same as `logging.file`).
- Output is still redacted according to `logging.redactSensitive`.
- Full guide: [/diagnostics/flags](/diagnostics/flags).
### Export to OpenTelemetry ### Export to OpenTelemetry
Diagnostics can be exported via the `diagnostics-otel` plugin (OTLP/HTTP). This Diagnostics can be exported via the `diagnostics-otel` plugin (OTLP/HTTP). This
+5 -19
View File
@@ -80,7 +80,6 @@ primary_region = "iad"
|---------|-----| |---------|-----|
| `--bind lan` | Binds to `0.0.0.0` so Fly's proxy can reach the gateway | | `--bind lan` | Binds to `0.0.0.0` so Fly's proxy can reach the gateway |
| `--allow-unconfigured` | Starts without a config file (you'll create one after) | | `--allow-unconfigured` | Starts without a config file (you'll create one after) |
| `internal_port = 3000` | Must match `--port 3000` (or `CLAWDBOT_GATEWAY_PORT`) for Fly health checks |
| `memory = "2048mb"` | 512MB is too small; 2GB recommended | | `memory = "2048mb"` | 512MB is too small; 2GB recommended |
| `CLAWDBOT_STATE_DIR = "/data"` | Persists state on the volume | | `CLAWDBOT_STATE_DIR = "/data"` | Persists state on the volume |
@@ -236,12 +235,6 @@ The gateway is binding to `127.0.0.1` instead of `0.0.0.0`.
**Fix:** Add `--bind lan` to your process command in `fly.toml`. **Fix:** Add `--bind lan` to your process command in `fly.toml`.
### Health checks failing / connection refused
Fly can't reach the gateway on the configured port.
**Fix:** Ensure `internal_port` matches the gateway port (set `--port 3000` or `CLAWDBOT_GATEWAY_PORT=3000`).
### OOM / Memory Issues ### OOM / Memory Issues
Container keeps restarting or getting killed. Signs: `SIGABRT`, `v8::internal::Runtime_AllocateInYoungGeneration`, or silent restarts. Container keeps restarting or getting killed. Signs: `SIGABRT`, `v8::internal::Runtime_AllocateInYoungGeneration`, or silent restarts.
@@ -275,11 +268,11 @@ The lock file is at `/data/gateway.*.lock` (not in a subdirectory).
### Config Not Being Read ### Config Not Being Read
If using `--allow-unconfigured`, the gateway creates a minimal config. Your custom config at `/data/clawdbot.json` should be read on restart. If using `--allow-unconfigured`, the gateway creates a minimal config. Your custom config at `/data/.clawdbot/clawdbot.json` should be read on restart.
Verify the config exists: Verify the config exists:
```bash ```bash
fly ssh console --command "cat /data/clawdbot.json" fly ssh console --command "cat /data/.clawdbot/clawdbot.json"
``` ```
### Writing Config via SSH ### Writing Config via SSH
@@ -288,24 +281,18 @@ The `fly ssh console -C` command doesn't support shell redirection. To write a c
```bash ```bash
# Use echo + tee (pipe from local to remote) # Use echo + tee (pipe from local to remote)
echo '{"your":"config"}' | fly ssh console -C "tee /data/clawdbot.json" echo '{"your":"config"}' | fly ssh console -C "tee /data/.clawdbot/clawdbot.json"
# Or use sftp # Or use sftp
fly sftp shell fly sftp shell
> put /local/path/config.json /data/clawdbot.json > put /local/path/config.json /data/.clawdbot/clawdbot.json
``` ```
**Note:** `fly sftp` may fail if the file already exists. Delete first: **Note:** `fly sftp` may fail if the file already exists. Delete first:
```bash ```bash
fly ssh console --command "rm /data/clawdbot.json" fly ssh console --command "rm /data/.clawdbot/clawdbot.json"
``` ```
### State Not Persisting
If you lose credentials or sessions after a restart, the state dir is writing to the container filesystem.
**Fix:** Ensure `CLAWDBOT_STATE_DIR=/data` is set in `fly.toml` and redeploy.
## Updates ## Updates
```bash ```bash
@@ -343,7 +330,6 @@ fly machine update <machine-id> --vm-memory 2048 --command "node dist/index.js g
- The Dockerfile is compatible with both architectures - The Dockerfile is compatible with both architectures
- For WhatsApp/Telegram onboarding, use `fly ssh console` - For WhatsApp/Telegram onboarding, use `fly ssh console`
- Persistent data lives on the volume at `/data` - Persistent data lives on the volume at `/data`
- Signal requires Java + signal-cli; use a custom image and keep memory at 2GB+.
## Cost ## Cost
-54
View File
@@ -25,11 +25,9 @@ import { resolveBlueBubblesMessageId } from "./monitor.js";
import { probeBlueBubbles, type BlueBubblesProbe } from "./probe.js"; import { probeBlueBubbles, type BlueBubblesProbe } from "./probe.js";
import { sendMessageBlueBubbles } from "./send.js"; import { sendMessageBlueBubbles } from "./send.js";
import { import {
extractHandleFromChatGuid,
looksLikeBlueBubblesTargetId, looksLikeBlueBubblesTargetId,
normalizeBlueBubblesHandle, normalizeBlueBubblesHandle,
normalizeBlueBubblesMessagingTarget, normalizeBlueBubblesMessagingTarget,
parseBlueBubblesTarget,
} from "./targets.js"; } from "./targets.js";
import { bluebubblesMessageActions } from "./actions.js"; import { bluebubblesMessageActions } from "./actions.js";
import { monitorBlueBubblesProvider, resolveWebhookPathFromConfig } from "./monitor.js"; import { monitorBlueBubblesProvider, resolveWebhookPathFromConfig } from "./monitor.js";
@@ -150,58 +148,6 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
looksLikeId: looksLikeBlueBubblesTargetId, looksLikeId: looksLikeBlueBubblesTargetId,
hint: "<handle|chat_guid:GUID|chat_id:ID|chat_identifier:ID>", hint: "<handle|chat_guid:GUID|chat_id:ID|chat_identifier:ID>",
}, },
formatTargetDisplay: ({ target, display }) => {
const shouldParseDisplay = (value: string): boolean => {
if (looksLikeBlueBubblesTargetId(value)) return true;
return /^(bluebubbles:|chat_guid:|chat_id:|chat_identifier:)/i.test(value);
};
// Helper to extract a clean handle from any BlueBubbles target format
const extractCleanDisplay = (value: string | undefined): string | null => {
const trimmed = value?.trim();
if (!trimmed) return null;
try {
const parsed = parseBlueBubblesTarget(trimmed);
if (parsed.kind === "chat_guid") {
const handle = extractHandleFromChatGuid(parsed.chatGuid);
if (handle) return handle;
}
if (parsed.kind === "handle") {
return normalizeBlueBubblesHandle(parsed.to);
}
} catch {
// Fall through
}
// Strip common prefixes and try raw extraction
const stripped = trimmed
.replace(/^bluebubbles:/i, "")
.replace(/^chat_guid:/i, "")
.replace(/^chat_id:/i, "")
.replace(/^chat_identifier:/i, "");
const handle = extractHandleFromChatGuid(stripped);
if (handle) return handle;
// Don't return raw chat_guid formats - they contain internal routing info
if (stripped.includes(";-;") || stripped.includes(";+;")) return null;
return stripped;
};
// Try to get a clean display from the display parameter first
const trimmedDisplay = display?.trim();
if (trimmedDisplay) {
if (!shouldParseDisplay(trimmedDisplay)) {
return trimmedDisplay;
}
const cleanDisplay = extractCleanDisplay(trimmedDisplay);
if (cleanDisplay) return cleanDisplay;
}
// Fall back to extracting from target
const cleanTarget = extractCleanDisplay(target);
if (cleanTarget) return cleanTarget;
// Last resort: return display or target as-is
return display?.trim() || target?.trim() || "";
},
}, },
setup: { setup: {
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
+2 -94
View File
@@ -187,47 +187,6 @@ describe("send", () => {
expect(result).toBe("iMessage;-;+15551234567"); expect(result).toBe("iMessage;-;+15551234567");
}); });
it("returns null when handle only exists in group chat (not DM)", async () => {
// This is the critical fix: if a phone number only exists as a participant in a group chat
// (no direct DM chat), we should NOT send to that group. Return null instead.
mockFetch
.mockResolvedValueOnce({
ok: true,
json: () =>
Promise.resolve({
data: [
{
guid: "iMessage;+;group-the-council",
participants: [
{ address: "+12622102921" },
{ address: "+15550001111" },
{ address: "+15550002222" },
],
},
],
}),
})
// Empty second page to stop pagination
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ data: [] }),
});
const target: BlueBubblesSendTarget = {
kind: "handle",
address: "+12622102921",
service: "imessage",
};
const result = await resolveChatGuidForTarget({
baseUrl: "http://localhost:1234",
password: "test",
target,
});
// Should return null, NOT the group chat GUID
expect(result).toBeNull();
});
it("returns null when chat not found", async () => { it("returns null when chat not found", async () => {
mockFetch.mockResolvedValueOnce({ mockFetch.mockResolvedValueOnce({
ok: true, ok: true,
@@ -385,14 +344,14 @@ describe("send", () => {
).rejects.toThrow("password is required"); ).rejects.toThrow("password is required");
}); });
it("throws when chatGuid cannot be resolved for non-handle targets", async () => { it("throws when chatGuid cannot be resolved", async () => {
mockFetch.mockResolvedValue({ mockFetch.mockResolvedValue({
ok: true, ok: true,
json: () => Promise.resolve({ data: [] }), json: () => Promise.resolve({ data: [] }),
}); });
await expect( await expect(
sendMessageBlueBubbles("chat_id:999", "Hello", { sendMessageBlueBubbles("+15559999999", "Hello", {
serverUrl: "http://localhost:1234", serverUrl: "http://localhost:1234",
password: "test", password: "test",
}), }),
@@ -439,57 +398,6 @@ describe("send", () => {
expect(body.method).toBeUndefined(); expect(body.method).toBeUndefined();
}); });
it("creates a new chat when handle target is missing", async () => {
mockFetch
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ data: [] }),
})
.mockResolvedValueOnce({
ok: true,
text: () =>
Promise.resolve(
JSON.stringify({
data: { guid: "new-msg-guid" },
}),
),
});
const result = await sendMessageBlueBubbles("+15550009999", "Hello new chat", {
serverUrl: "http://localhost:1234",
password: "test",
});
expect(result.messageId).toBe("new-msg-guid");
expect(mockFetch).toHaveBeenCalledTimes(2);
const createCall = mockFetch.mock.calls[1];
expect(createCall[0]).toContain("/api/v1/chat/new");
const body = JSON.parse(createCall[1].body);
expect(body.addresses).toEqual(["+15550009999"]);
expect(body.message).toBe("Hello new chat");
});
it("throws when creating a new chat requires Private API", async () => {
mockFetch
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ data: [] }),
})
.mockResolvedValueOnce({
ok: false,
status: 403,
text: () => Promise.resolve("Private API not enabled"),
});
await expect(
sendMessageBlueBubbles("+15550008888", "Hello", {
serverUrl: "http://localhost:1234",
password: "test",
}),
).rejects.toThrow("Private API must be enabled");
});
it("uses private-api when reply metadata is present", async () => { it("uses private-api when reply metadata is present", async () => {
mockFetch mockFetch
.mockResolvedValueOnce({ .mockResolvedValueOnce({
+5 -71
View File
@@ -257,17 +257,11 @@ export async function resolveChatGuidForTarget(params: {
return guid; return guid;
} }
if (!participantMatch && guid) { if (!participantMatch && guid) {
// Only consider DM chats (`;-;` separator) as participant matches. const participants = extractParticipantAddresses(chat).map((entry) =>
// Group chats (`;+;` separator) should never match when searching by handle/phone. normalizeBlueBubblesHandle(entry),
// This prevents routing "send to +1234567890" to a group chat that contains that number. );
const isDmChat = guid.includes(";-;"); if (participants.includes(normalizedHandle)) {
if (isDmChat) { participantMatch = guid;
const participants = extractParticipantAddresses(chat).map((entry) =>
normalizeBlueBubblesHandle(entry),
);
if (participants.includes(normalizedHandle)) {
participantMatch = guid;
}
} }
} }
} }
@@ -276,55 +270,6 @@ export async function resolveChatGuidForTarget(params: {
return participantMatch; return participantMatch;
} }
/**
* Creates a new chat (DM) and optionally sends an initial message.
* Requires Private API to be enabled in BlueBubbles.
*/
async function createNewChatWithMessage(params: {
baseUrl: string;
password: string;
address: string;
message: string;
timeoutMs?: number;
}): Promise<BlueBubblesSendResult> {
const url = buildBlueBubblesApiUrl({
baseUrl: params.baseUrl,
path: "/api/v1/chat/new",
password: params.password,
});
const payload = {
addresses: [params.address],
message: params.message,
};
const res = await blueBubblesFetchWithTimeout(
url,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
},
params.timeoutMs,
);
if (!res.ok) {
const errorText = await res.text();
// Check for Private API not enabled error
if (res.status === 400 || res.status === 403 || errorText.toLowerCase().includes("private api")) {
throw new Error(
`BlueBubbles send failed: Cannot create new chat - Private API must be enabled. Original error: ${errorText || res.status}`,
);
}
throw new Error(`BlueBubbles create chat failed (${res.status}): ${errorText || "unknown"}`);
}
const body = await res.text();
if (!body) return { messageId: "ok" };
try {
const parsed = JSON.parse(body) as unknown;
return { messageId: extractMessageId(parsed) };
} catch {
return { messageId: "ok" };
}
}
export async function sendMessageBlueBubbles( export async function sendMessageBlueBubbles(
to: string, to: string,
text: string, text: string,
@@ -352,17 +297,6 @@ export async function sendMessageBlueBubbles(
target, target,
}); });
if (!chatGuid) { if (!chatGuid) {
// If target is a phone number/handle and no existing chat found,
// auto-create a new DM chat using the /api/v1/chat/new endpoint
if (target.kind === "handle") {
return createNewChatWithMessage({
baseUrl,
password,
address: target.address,
message: trimmedText,
timeoutMs: opts.timeoutMs,
});
}
throw new Error( throw new Error(
"BlueBubbles send failed: chatGuid not found for target. Use a chat_guid target or ensure the chat exists.", "BlueBubbles send failed: chatGuid not found for target. Use a chat_guid target or ensure the chat exists.",
); );
@@ -19,3 +19,4 @@ export type CallManagerContext = {
transcriptWaiters: Map<CallId, TranscriptWaiter>; transcriptWaiters: Map<CallId, TranscriptWaiter>;
maxDurationTimers: Map<CallId, NodeJS.Timeout>; maxDurationTimers: Map<CallId, NodeJS.Timeout>;
}; };
@@ -175,3 +175,4 @@ export function processEvent(ctx: CallManagerContext, event: NormalizedEvent): v
persistCallRecord(ctx.storePath, call); persistCallRecord(ctx.storePath, call);
} }
@@ -31,3 +31,4 @@ export function findCall(params: {
providerCallId: params.callIdOrProviderCallId, providerCallId: params.callIdOrProviderCallId,
}); });
} }
@@ -48,3 +48,4 @@ export function addTranscriptEntry(
}; };
call.transcript.push(entry); call.transcript.push(entry);
} }
@@ -86,3 +86,4 @@ export async function getCallHistoryFromStore(
return calls; return calls;
} }
@@ -84,3 +84,4 @@ export function waitForFinalTranscript(
ctx.transcriptWaiters.set(callId, { resolve, reject, timeout }); ctx.transcriptWaiters.set(callId, { resolve, reject, timeout });
}); });
} }
@@ -7,3 +7,4 @@ export function generateNotifyTwiml(message: string, voice: string): string {
<Hangup/> <Hangup/>
</Response>`; </Response>`;
} }
@@ -26,3 +26,4 @@ describe("PlivoProvider", () => {
expect(result.providerResponseBody).toContain('length="300"'); expect(result.providerResponseBody).toContain('length="300"');
}); });
}); });
@@ -27,3 +27,4 @@ export function verifyTwilioProviderWebhook(params: {
reason: result.reason, reason: result.reason,
}; };
} }
+1
View File
@@ -15,3 +15,4 @@ describe("zalouser outbound chunker", () => {
expect(chunks.every((c) => c.length <= limit)).toBe(true); expect(chunks.every((c) => c.length <= limit)).toBe(true);
}); });
}); });
-31
View File
@@ -1,31 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
if [[ $# -lt 1 ]]; then
echo "usage: run-node-tool.sh <tool> [args...]" >&2
exit 2
fi
tool="$1"
shift
if [[ -f "$ROOT_DIR/pnpm-lock.yaml" ]] && command -v pnpm >/dev/null 2>&1; then
exec pnpm exec "$tool" "$@"
fi
if { [[ -f "$ROOT_DIR/bun.lockb" ]] || [[ -f "$ROOT_DIR/bun.lock" ]]; } && command -v bun >/dev/null 2>&1; then
exec bunx --bun "$tool" "$@"
fi
if command -v npm >/dev/null 2>&1; then
exec npm exec -- "$tool" "$@"
fi
if command -v npx >/dev/null 2>&1; then
exec npx "$tool" "$@"
fi
echo "Missing package manager: pnpm, bun, or npm required." >&2
exit 1
+1
View File
@@ -30,3 +30,4 @@ export type Entry = {
avatar_url: string; avatar_url: string;
lines: number; lines: number;
}; };
+1 -30
View File
@@ -8,7 +8,6 @@ export type ModelCatalogEntry = {
provider: string; provider: string;
contextWindow?: number; contextWindow?: number;
reasoning?: boolean; reasoning?: boolean;
input?: Array<"text" | "image">;
}; };
type DiscoveredModel = { type DiscoveredModel = {
@@ -17,7 +16,6 @@ type DiscoveredModel = {
provider: string; provider: string;
contextWindow?: number; contextWindow?: number;
reasoning?: boolean; reasoning?: boolean;
input?: Array<"text" | "image">;
}; };
type PiSdkModule = typeof import("@mariozechner/pi-coding-agent"); type PiSdkModule = typeof import("@mariozechner/pi-coding-agent");
@@ -82,10 +80,7 @@ export async function loadModelCatalog(params?: {
? entry.contextWindow ? entry.contextWindow
: undefined; : undefined;
const reasoning = typeof entry?.reasoning === "boolean" ? entry.reasoning : undefined; const reasoning = typeof entry?.reasoning === "boolean" ? entry.reasoning : undefined;
const input = Array.isArray(entry?.input) models.push({ id, name, provider, contextWindow, reasoning });
? (entry.input as Array<"text" | "image">)
: undefined;
models.push({ id, name, provider, contextWindow, reasoning, input });
} }
if (models.length === 0) { if (models.length === 0) {
@@ -110,27 +105,3 @@ export async function loadModelCatalog(params?: {
return modelCatalogPromise; return modelCatalogPromise;
} }
/**
* Check if a model supports image input based on its catalog entry.
*/
export function modelSupportsVision(entry: ModelCatalogEntry | undefined): boolean {
return entry?.input?.includes("image") ?? false;
}
/**
* Find a model in the catalog by provider and model ID.
*/
export function findModelInCatalog(
catalog: ModelCatalogEntry[],
provider: string,
modelId: string,
): ModelCatalogEntry | undefined {
const normalizedProvider = provider.toLowerCase().trim();
const normalizedModelId = modelId.toLowerCase().trim();
return catalog.find(
(entry) =>
entry.provider.toLowerCase() === normalizedProvider &&
entry.id.toLowerCase() === normalizedModelId,
);
}
@@ -27,14 +27,4 @@ describe("sanitizeUserFacingText", () => {
const raw = '{"type":"error","error":{"message":"Something exploded","type":"server_error"}}'; const raw = '{"type":"error","error":{"message":"Something exploded","type":"server_error"}}';
expect(sanitizeUserFacingText(raw)).toBe("LLM error server_error: Something exploded"); expect(sanitizeUserFacingText(raw)).toBe("LLM error server_error: Something exploded");
}); });
it("collapses consecutive duplicate paragraphs", () => {
const text = "Hello there!\n\nHello there!";
expect(sanitizeUserFacingText(text)).toBe("Hello there!");
});
it("does not collapse distinct paragraphs", () => {
const text = "Hello there!\n\nDifferent line.";
expect(sanitizeUserFacingText(text)).toBe(text);
});
}); });
+1 -24
View File
@@ -77,29 +77,6 @@ function stripFinalTagsFromText(text: string): string {
return text.replace(FINAL_TAG_RE, ""); return text.replace(FINAL_TAG_RE, "");
} }
function collapseConsecutiveDuplicateBlocks(text: string): string {
const trimmed = text.trim();
if (!trimmed) return text;
const blocks = trimmed.split(/\n{2,}/);
if (blocks.length < 2) return text;
const normalizeBlock = (value: string) => value.trim().replace(/\s+/g, " ");
const result: string[] = [];
let lastNormalized: string | null = null;
for (const block of blocks) {
const normalized = normalizeBlock(block);
if (lastNormalized && normalized === lastNormalized) {
continue;
}
result.push(block.trim());
lastNormalized = normalized;
}
if (result.length === blocks.length) return text;
return result.join("\n\n");
}
function isLikelyHttpErrorText(raw: string): boolean { function isLikelyHttpErrorText(raw: string): boolean {
const match = raw.match(HTTP_STATUS_PREFIX_RE); const match = raw.match(HTTP_STATUS_PREFIX_RE);
if (!match) return false; if (!match) return false;
@@ -344,7 +321,7 @@ export function sanitizeUserFacingText(text: string): string {
return formatRawAssistantErrorForUi(trimmed); return formatRawAssistantErrorForUi(trimmed);
} }
return collapseConsecutiveDuplicateBlocks(stripped); return stripped;
} }
export function isRateLimitAssistantError(msg: AssistantMessage | undefined): boolean { export function isRateLimitAssistantError(msg: AssistantMessage | undefined): boolean {
+1 -11
View File
@@ -333,13 +333,7 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
name: "message", name: "message",
description, description,
parameters: schema, parameters: schema,
execute: async (_toolCallId, args, signal) => { execute: async (_toolCallId, args) => {
// Check if already aborted before doing any work
if (signal?.aborted) {
const err = new Error("Message send aborted");
err.name = "AbortError";
throw err;
}
const params = args as Record<string, unknown>; const params = args as Record<string, unknown>;
const cfg = options?.config ?? loadConfig(); const cfg = options?.config ?? loadConfig();
const action = readStringParam(params, "action", { const action = readStringParam(params, "action", {
@@ -372,9 +366,6 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
currentThreadTs: options?.currentThreadTs, currentThreadTs: options?.currentThreadTs,
replyToMode: options?.replyToMode, replyToMode: options?.replyToMode,
hasRepliedRef: options?.hasRepliedRef, hasRepliedRef: options?.hasRepliedRef,
// Direct tool invocations should not add cross-context decoration.
// The agent is composing a message, not forwarding from another chat.
skipCrossContextDecoration: true,
} }
: undefined; : undefined;
@@ -388,7 +379,6 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
agentId: options?.agentSessionKey agentId: options?.agentSessionKey
? resolveSessionAgentId({ sessionKey: options.agentSessionKey, config: cfg }) ? resolveSessionAgentId({ sessionKey: options.agentSessionKey, config: cfg })
: undefined, : undefined,
abortSignal: signal,
}); });
const toolResult = getToolResult(result); const toolResult = getToolResult(result);
-6
View File
@@ -240,12 +240,6 @@ export type ChannelThreadingToolContext = {
currentThreadTs?: string; currentThreadTs?: string;
replyToMode?: "off" | "first" | "all"; replyToMode?: "off" | "first" | "all";
hasRepliedRef?: { value: boolean }; hasRepliedRef?: { value: boolean };
/**
* When true, skip cross-context decoration (e.g., "[from X]" prefix).
* Use this for direct tool invocations where the agent is composing a new message,
* not forwarding/relaying a message from another conversation.
*/
skipCrossContextDecoration?: boolean;
}; };
export type ChannelMessagingAdapter = { export type ChannelMessagingAdapter = {
-1
View File
@@ -100,7 +100,6 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
if (json) warnings.push(message); if (json) warnings.push(message);
else defaultRuntime.log(message); else defaultRuntime.log(message);
}, },
config: cfg,
}); });
try { try {
-3
View File
@@ -11,7 +11,6 @@ import {
} from "./daemon-runtime.js"; } from "./daemon-runtime.js";
import { guardCancel } from "./onboard-helpers.js"; import { guardCancel } from "./onboard-helpers.js";
import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js"; import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js";
import { loadConfig } from "../config/config.js";
export async function maybeInstallDaemon(params: { export async function maybeInstallDaemon(params: {
runtime: RuntimeEnv; runtime: RuntimeEnv;
@@ -82,14 +81,12 @@ export async function maybeInstallDaemon(params: {
progress.setLabel("Preparing Gateway service…"); progress.setLabel("Preparing Gateway service…");
const cfg = loadConfig();
const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({ const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({
env: process.env, env: process.env,
port: params.port, port: params.port,
token: params.gatewayToken, token: params.gatewayToken,
runtime: daemonRuntime, runtime: daemonRuntime,
warn: (message, title) => note(message, title), warn: (message, title) => note(message, title),
config: cfg,
}); });
progress.setLabel("Installing Gateway service…"); progress.setLabel("Installing Gateway service…");
-134
View File
@@ -95,140 +95,6 @@ describe("buildGatewayInstallPlan", () => {
expect(warn).toHaveBeenCalledWith("Node too old", "Gateway runtime"); expect(warn).toHaveBeenCalledWith("Node too old", "Gateway runtime");
expect(mocks.resolvePreferredNodePath).toHaveBeenCalled(); expect(mocks.resolvePreferredNodePath).toHaveBeenCalled();
}); });
it("merges config env vars into the environment", async () => {
mocks.resolvePreferredNodePath.mockResolvedValue("/opt/node");
mocks.resolveGatewayProgramArguments.mockResolvedValue({
programArguments: ["node", "gateway"],
workingDirectory: "/Users/me",
});
mocks.resolveSystemNodeInfo.mockResolvedValue({
path: "/opt/node",
version: "22.0.0",
supported: true,
});
mocks.buildServiceEnvironment.mockReturnValue({
CLAWDBOT_PORT: "3000",
HOME: "/Users/me",
});
const plan = await buildGatewayInstallPlan({
env: {},
port: 3000,
runtime: "node",
config: {
env: {
vars: {
GOOGLE_API_KEY: "test-key",
},
CUSTOM_VAR: "custom-value",
},
},
});
// Config env vars should be present
expect(plan.environment.GOOGLE_API_KEY).toBe("test-key");
expect(plan.environment.CUSTOM_VAR).toBe("custom-value");
// Service environment vars should take precedence
expect(plan.environment.CLAWDBOT_PORT).toBe("3000");
expect(plan.environment.HOME).toBe("/Users/me");
});
it("does not include empty config env values", async () => {
mocks.resolvePreferredNodePath.mockResolvedValue("/opt/node");
mocks.resolveGatewayProgramArguments.mockResolvedValue({
programArguments: ["node", "gateway"],
workingDirectory: "/Users/me",
});
mocks.resolveSystemNodeInfo.mockResolvedValue({
path: "/opt/node",
version: "22.0.0",
supported: true,
});
mocks.buildServiceEnvironment.mockReturnValue({ CLAWDBOT_PORT: "3000" });
const plan = await buildGatewayInstallPlan({
env: {},
port: 3000,
runtime: "node",
config: {
env: {
vars: {
VALID_KEY: "valid",
EMPTY_KEY: "",
},
},
},
});
expect(plan.environment.VALID_KEY).toBe("valid");
expect(plan.environment.EMPTY_KEY).toBeUndefined();
});
it("drops whitespace-only config env values", async () => {
mocks.resolvePreferredNodePath.mockResolvedValue("/opt/node");
mocks.resolveGatewayProgramArguments.mockResolvedValue({
programArguments: ["node", "gateway"],
workingDirectory: "/Users/me",
});
mocks.resolveSystemNodeInfo.mockResolvedValue({
path: "/opt/node",
version: "22.0.0",
supported: true,
});
mocks.buildServiceEnvironment.mockReturnValue({});
const plan = await buildGatewayInstallPlan({
env: {},
port: 3000,
runtime: "node",
config: {
env: {
vars: {
VALID_KEY: "valid",
},
TRIMMED_KEY: " ",
},
},
});
expect(plan.environment.VALID_KEY).toBe("valid");
expect(plan.environment.TRIMMED_KEY).toBeUndefined();
});
it("keeps service env values over config env vars", async () => {
mocks.resolvePreferredNodePath.mockResolvedValue("/opt/node");
mocks.resolveGatewayProgramArguments.mockResolvedValue({
programArguments: ["node", "gateway"],
workingDirectory: "/Users/me",
});
mocks.resolveSystemNodeInfo.mockResolvedValue({
path: "/opt/node",
version: "22.0.0",
supported: true,
});
mocks.buildServiceEnvironment.mockReturnValue({
HOME: "/Users/service",
CLAWDBOT_PORT: "3000",
});
const plan = await buildGatewayInstallPlan({
env: {},
port: 3000,
runtime: "node",
config: {
env: {
HOME: "/Users/config",
vars: {
CLAWDBOT_PORT: "9999",
},
},
},
});
expect(plan.environment.HOME).toBe("/Users/service");
expect(plan.environment.CLAWDBOT_PORT).toBe("3000");
});
}); });
describe("gatewayInstallErrorHint", () => { describe("gatewayInstallErrorHint", () => {
+1 -12
View File
@@ -7,8 +7,6 @@ import {
} from "../daemon/runtime-paths.js"; } from "../daemon/runtime-paths.js";
import { buildServiceEnvironment } from "../daemon/service-env.js"; import { buildServiceEnvironment } from "../daemon/service-env.js";
import { formatCliCommand } from "../cli/command-format.js"; import { formatCliCommand } from "../cli/command-format.js";
import { collectConfigEnvVars } from "../config/env-vars.js";
import type { ClawdbotConfig } from "../config/types.js";
import type { GatewayDaemonRuntime } from "./daemon-runtime.js"; import type { GatewayDaemonRuntime } from "./daemon-runtime.js";
type WarnFn = (message: string, title?: string) => void; type WarnFn = (message: string, title?: string) => void;
@@ -33,8 +31,6 @@ export async function buildGatewayInstallPlan(params: {
devMode?: boolean; devMode?: boolean;
nodePath?: string; nodePath?: string;
warn?: WarnFn; warn?: WarnFn;
/** Full config to extract env vars from (env vars + inline env keys). */
config?: ClawdbotConfig;
}): Promise<GatewayInstallPlan> { }): Promise<GatewayInstallPlan> {
const devMode = params.devMode ?? resolveGatewayDevMode(); const devMode = params.devMode ?? resolveGatewayDevMode();
const nodePath = const nodePath =
@@ -54,7 +50,7 @@ export async function buildGatewayInstallPlan(params: {
const warning = renderSystemNodeWarning(systemNode, programArguments[0]); const warning = renderSystemNodeWarning(systemNode, programArguments[0]);
if (warning) params.warn?.(warning, "Gateway runtime"); if (warning) params.warn?.(warning, "Gateway runtime");
} }
const serviceEnvironment = buildServiceEnvironment({ const environment = buildServiceEnvironment({
env: params.env, env: params.env,
port: params.port, port: params.port,
token: params.token, token: params.token,
@@ -64,13 +60,6 @@ export async function buildGatewayInstallPlan(params: {
: undefined, : undefined,
}); });
// Merge config env vars into the service environment (vars + inline env keys).
// Config env vars are added first so service-specific vars take precedence.
const environment: Record<string, string | undefined> = {
...collectConfigEnvVars(params.config),
};
Object.assign(environment, serviceEnvironment);
return { programArguments, workingDirectory, environment }; return { programArguments, workingDirectory, environment };
} }
@@ -161,7 +161,6 @@ export async function maybeRepairGatewayDaemon(params: {
token: params.cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN, token: params.cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN,
runtime: daemonRuntime, runtime: daemonRuntime,
warn: (message, title) => note(message, title), warn: (message, title) => note(message, title),
config: params.cfg,
}); });
try { try {
await service.install({ await service.install({
-2
View File
@@ -110,7 +110,6 @@ export async function maybeMigrateLegacyGatewayService(
token: cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN, token: cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN,
runtime: daemonRuntime, runtime: daemonRuntime,
warn: (message, title) => note(message, title), warn: (message, title) => note(message, title),
config: cfg,
}); });
try { try {
await service.install({ await service.install({
@@ -178,7 +177,6 @@ export async function maybeRepairGatewayServiceConfig(
runtime: needsNodeRuntime && systemNodePath ? "node" : runtimeChoice, runtime: needsNodeRuntime && systemNodePath ? "node" : runtimeChoice,
nodePath: systemNodePath ?? undefined, nodePath: systemNodePath ?? undefined,
warn: (message, title) => note(message, title), warn: (message, title) => note(message, title),
config: cfg,
}); });
const expectedEntrypoint = findGatewayEntrypoint(programArguments); const expectedEntrypoint = findGatewayEntrypoint(programArguments);
const currentEntrypoint = findGatewayEntrypoint(command.programArguments); const currentEntrypoint = findGatewayEntrypoint(command.programArguments);
@@ -38,7 +38,6 @@ export async function installGatewayDaemonNonInteractive(params: {
token: gatewayToken, token: gatewayToken,
runtime: daemonRuntimeRaw, runtime: daemonRuntimeRaw,
warn: (message) => runtime.log(message), warn: (message) => runtime.log(message),
config: params.nextConfig,
}); });
try { try {
await service.install({ await service.install({
-23
View File
@@ -1,23 +0,0 @@
import type { ClawdbotConfig } from "./types.js";
export function collectConfigEnvVars(cfg?: ClawdbotConfig): Record<string, string> {
const envConfig = cfg?.env;
if (!envConfig) return {};
const entries: Record<string, string> = {};
if (envConfig.vars) {
for (const [key, value] of Object.entries(envConfig.vars)) {
if (!value) continue;
entries[key] = value;
}
}
for (const [key, value] of Object.entries(envConfig)) {
if (key === "shellEnv" || key === "vars") continue;
if (typeof value !== "string" || !value.trim()) continue;
entries[key] = value;
}
return entries;
}
+18 -2
View File
@@ -24,7 +24,6 @@ import {
} from "./defaults.js"; } from "./defaults.js";
import { VERSION } from "../version.js"; import { VERSION } from "../version.js";
import { MissingEnvVarError, resolveConfigEnvVars } from "./env-substitution.js"; import { MissingEnvVarError, resolveConfigEnvVars } from "./env-substitution.js";
import { collectConfigEnvVars } from "./env-vars.js";
import { ConfigIncludeError, resolveConfigIncludes } from "./includes.js"; import { ConfigIncludeError, resolveConfigIncludes } from "./includes.js";
import { findLegacyConfigIssues } from "./legacy.js"; import { findLegacyConfigIssues } from "./legacy.js";
import { normalizeConfigPaths } from "./normalize-paths.js"; import { normalizeConfigPaths } from "./normalize-paths.js";
@@ -150,7 +149,24 @@ function warnIfConfigFromFuture(cfg: ClawdbotConfig, logger: Pick<typeof console
} }
function applyConfigEnv(cfg: ClawdbotConfig, env: NodeJS.ProcessEnv): void { function applyConfigEnv(cfg: ClawdbotConfig, env: NodeJS.ProcessEnv): void {
const entries = collectConfigEnvVars(cfg); const envConfig = cfg.env;
if (!envConfig) return;
const entries: Record<string, string> = {};
if (envConfig.vars) {
for (const [key, value] of Object.entries(envConfig.vars)) {
if (!value) continue;
entries[key] = value;
}
}
for (const [key, value] of Object.entries(envConfig)) {
if (key === "shellEnv" || key === "vars") continue;
if (typeof value !== "string" || !value.trim()) continue;
entries[key] = value;
}
for (const [key, value] of Object.entries(entries)) { for (const [key, value] of Object.entries(entries)) {
if (env[key]?.trim()) continue; if (env[key]?.trim()) continue;
env[key] = value; env[key] = value;
-3
View File
@@ -107,7 +107,6 @@ const FIELD_LABELS: Record<string, string> = {
"update.channel": "Update Channel", "update.channel": "Update Channel",
"update.checkOnStart": "Update Check on Start", "update.checkOnStart": "Update Check on Start",
"diagnostics.enabled": "Diagnostics Enabled", "diagnostics.enabled": "Diagnostics Enabled",
"diagnostics.flags": "Diagnostics Flags",
"diagnostics.otel.enabled": "OpenTelemetry Enabled", "diagnostics.otel.enabled": "OpenTelemetry Enabled",
"diagnostics.otel.endpoint": "OpenTelemetry Endpoint", "diagnostics.otel.endpoint": "OpenTelemetry Endpoint",
"diagnostics.otel.protocol": "OpenTelemetry Protocol", "diagnostics.otel.protocol": "OpenTelemetry Protocol",
@@ -389,8 +388,6 @@ const FIELD_HELP: Record<string, string> = {
"nodeHost.browserProxy.enabled": "Expose the local browser control server via node proxy.", "nodeHost.browserProxy.enabled": "Expose the local browser control server via node proxy.",
"nodeHost.browserProxy.allowProfiles": "nodeHost.browserProxy.allowProfiles":
"Optional allowlist of browser profile names exposed via the node proxy.", "Optional allowlist of browser profile names exposed via the node proxy.",
"diagnostics.flags":
'Enable targeted diagnostics logs by flag (e.g. ["telegram.http"]). Supports wildcards like "telegram.*" or "*".',
"diagnostics.cacheTrace.enabled": "diagnostics.cacheTrace.enabled":
"Log cache trace snapshots for embedded agent runs (default: false).", "Log cache trace snapshots for embedded agent runs (default: false).",
"diagnostics.cacheTrace.filePath": "diagnostics.cacheTrace.filePath":
-2
View File
@@ -135,8 +135,6 @@ export type DiagnosticsCacheTraceConfig = {
export type DiagnosticsConfig = { export type DiagnosticsConfig = {
enabled?: boolean; enabled?: boolean;
/** Optional ad-hoc diagnostics flags (e.g. "telegram.http"). */
flags?: string[];
otel?: DiagnosticsOtelConfig; otel?: DiagnosticsOtelConfig;
cacheTrace?: DiagnosticsCacheTraceConfig; cacheTrace?: DiagnosticsCacheTraceConfig;
}; };
-1
View File
@@ -62,7 +62,6 @@ export const ClawdbotSchema = z
diagnostics: z diagnostics: z
.object({ .object({
enabled: z.boolean().optional(), enabled: z.boolean().optional(),
flags: z.array(z.string()).optional(),
otel: z otel: z
.object({ .object({
enabled: z.boolean().optional(), enabled: z.boolean().optional(),
-9
View File
@@ -14,7 +14,6 @@ import {
writeConfigFile, writeConfigFile,
} from "../config/config.js"; } from "../config/config.js";
import { isDiagnosticsEnabled } from "../infra/diagnostic-events.js"; import { isDiagnosticsEnabled } from "../infra/diagnostic-events.js";
import { logAcceptedEnvOption } from "../infra/env.js";
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
import { clearAgentRunContext, onAgentEvent } from "../infra/agent-events.js"; import { clearAgentRunContext, onAgentEvent } from "../infra/agent-events.js";
import { onHeartbeatEvent } from "../infra/heartbeat-events.js"; import { onHeartbeatEvent } from "../infra/heartbeat-events.js";
@@ -150,14 +149,6 @@ export async function startGatewayServer(
): Promise<GatewayServer> { ): Promise<GatewayServer> {
// Ensure all default port derivations (browser/canvas) see the actual runtime port. // Ensure all default port derivations (browser/canvas) see the actual runtime port.
process.env.CLAWDBOT_GATEWAY_PORT = String(port); process.env.CLAWDBOT_GATEWAY_PORT = String(port);
logAcceptedEnvOption({
key: "CLAWDBOT_RAW_STREAM",
description: "raw stream logging enabled",
});
logAcceptedEnvOption({
key: "CLAWDBOT_RAW_STREAM_PATH",
description: "raw stream log path override",
});
let configSnapshot = await readConfigFileSnapshot(); let configSnapshot = await readConfigFileSnapshot();
if (configSnapshot.legacyIssues.length > 0) { if (configSnapshot.legacyIssues.length > 0) {
-31
View File
@@ -1,31 +0,0 @@
import { describe, expect, it } from "vitest";
import type { ClawdbotConfig } from "../config/config.js";
import { isDiagnosticFlagEnabled, resolveDiagnosticFlags } from "./diagnostic-flags.js";
describe("diagnostic flags", () => {
it("merges config + env flags", () => {
const cfg = {
diagnostics: { flags: ["telegram.http", "cache.*"] },
} as ClawdbotConfig;
const env = {
CLAWDBOT_DIAGNOSTICS: "foo,bar",
} as NodeJS.ProcessEnv;
const flags = resolveDiagnosticFlags(cfg, env);
expect(flags).toEqual(expect.arrayContaining(["telegram.http", "cache.*", "foo", "bar"]));
expect(isDiagnosticFlagEnabled("telegram.http", cfg, env)).toBe(true);
expect(isDiagnosticFlagEnabled("cache.hit", cfg, env)).toBe(true);
expect(isDiagnosticFlagEnabled("foo", cfg, env)).toBe(true);
});
it("treats env true as wildcard", () => {
const env = { CLAWDBOT_DIAGNOSTICS: "1" } as NodeJS.ProcessEnv;
expect(isDiagnosticFlagEnabled("anything.here", undefined, env)).toBe(true);
});
it("treats env false as disabled", () => {
const env = { CLAWDBOT_DIAGNOSTICS: "0" } as NodeJS.ProcessEnv;
expect(isDiagnosticFlagEnabled("telegram.http", undefined, env)).toBe(false);
});
});
-70
View File
@@ -1,70 +0,0 @@
import type { ClawdbotConfig } from "../config/config.js";
const DIAGNOSTICS_ENV = "CLAWDBOT_DIAGNOSTICS";
function normalizeFlag(value: string): string {
return value.trim().toLowerCase();
}
function parseEnvFlags(raw?: string): string[] {
if (!raw) return [];
const trimmed = raw.trim();
if (!trimmed) return [];
const lowered = trimmed.toLowerCase();
if (["0", "false", "off", "none"].includes(lowered)) return [];
if (["1", "true", "all", "*"].includes(lowered)) return ["*"];
return trimmed
.split(/[,\s]+/)
.map(normalizeFlag)
.filter(Boolean);
}
function uniqueFlags(flags: string[]): string[] {
const seen = new Set<string>();
const out: string[] = [];
for (const flag of flags) {
const normalized = normalizeFlag(flag);
if (!normalized || seen.has(normalized)) continue;
seen.add(normalized);
out.push(normalized);
}
return out;
}
export function resolveDiagnosticFlags(
cfg?: ClawdbotConfig,
env: NodeJS.ProcessEnv = process.env,
): string[] {
const configFlags = Array.isArray(cfg?.diagnostics?.flags) ? cfg?.diagnostics?.flags : [];
const envFlags = parseEnvFlags(env[DIAGNOSTICS_ENV]);
return uniqueFlags([...configFlags, ...envFlags]);
}
export function matchesDiagnosticFlag(flag: string, enabledFlags: string[]): boolean {
const target = normalizeFlag(flag);
if (!target) return false;
for (const raw of enabledFlags) {
const enabled = normalizeFlag(raw);
if (!enabled) continue;
if (enabled === "*" || enabled === "all") return true;
if (enabled.endsWith(".*")) {
const prefix = enabled.slice(0, -2);
if (target === prefix || target.startsWith(`${prefix}.`)) return true;
}
if (enabled.endsWith("*")) {
const prefix = enabled.slice(0, -1);
if (target.startsWith(prefix)) return true;
}
if (enabled === target) return true;
}
return false;
}
export function isDiagnosticFlagEnabled(
flag: string,
cfg?: ClawdbotConfig,
env: NodeJS.ProcessEnv = process.env,
): boolean {
const flags = resolveDiagnosticFlags(cfg, env);
return matchesDiagnosticFlag(flag, flags);
}
-27
View File
@@ -1,32 +1,5 @@
import { createSubsystemLogger } from "../logging/subsystem.js";
import { parseBooleanValue } from "../utils/boolean.js"; import { parseBooleanValue } from "../utils/boolean.js";
const log = createSubsystemLogger("env");
const loggedEnv = new Set<string>();
type AcceptedEnvOption = {
key: string;
description: string;
value?: string;
redact?: boolean;
};
function formatEnvValue(value: string, redact?: boolean): string {
if (redact) return "<redacted>";
const singleLine = value.replace(/\s+/g, " ").trim();
if (singleLine.length <= 160) return singleLine;
return `${singleLine.slice(0, 160)}…`;
}
export function logAcceptedEnvOption(option: AcceptedEnvOption): void {
if (process.env.VITEST || process.env.NODE_ENV === "test") return;
if (loggedEnv.has(option.key)) return;
const rawValue = option.value ?? process.env[option.key];
if (!rawValue || !rawValue.trim()) return;
loggedEnv.add(option.key);
log.info(`env: ${option.key}=${formatEnvValue(rawValue, option.redact)} (${option.description})`);
}
export function normalizeZaiEnv(): void { export function normalizeZaiEnv(): void {
if (!process.env.ZAI_API_KEY?.trim() && process.env.Z_AI_API_KEY?.trim()) { if (!process.env.ZAI_API_KEY?.trim() && process.env.Z_AI_API_KEY?.trim()) {
process.env.ZAI_API_KEY = process.env.Z_AI_API_KEY; process.env.ZAI_API_KEY = process.env.Z_AI_API_KEY;
@@ -321,44 +321,6 @@ describe("runMessageAction context isolation", () => {
}), }),
).rejects.toThrow(/Cross-context messaging denied/); ).rejects.toThrow(/Cross-context messaging denied/);
}); });
it("aborts send when abortSignal is already aborted", async () => {
const controller = new AbortController();
controller.abort();
await expect(
runMessageAction({
cfg: slackConfig,
action: "send",
params: {
channel: "slack",
target: "#C12345678",
message: "hi",
},
dryRun: true,
abortSignal: controller.signal,
}),
).rejects.toMatchObject({ name: "AbortError" });
});
it("aborts broadcast when abortSignal is already aborted", async () => {
const controller = new AbortController();
controller.abort();
await expect(
runMessageAction({
cfg: slackConfig,
action: "broadcast",
params: {
targets: ["channel:C12345678"],
channel: "slack",
message: "hi",
},
dryRun: true,
abortSignal: controller.signal,
}),
).rejects.toMatchObject({ name: "AbortError" });
});
}); });
describe("runMessageAction sendAttachment hydration", () => { describe("runMessageAction sendAttachment hydration", () => {
+3 -37
View File
@@ -64,7 +64,6 @@ export type RunMessageActionParams = {
sessionKey?: string; sessionKey?: string;
agentId?: string; agentId?: string;
dryRun?: boolean; dryRun?: boolean;
abortSignal?: AbortSignal;
}; };
export type MessageActionRunResult = export type MessageActionRunResult =
@@ -508,7 +507,6 @@ type ResolvedActionContext = {
input: RunMessageActionParams; input: RunMessageActionParams;
agentId?: string; agentId?: string;
resolvedTarget?: ResolvedMessagingTarget; resolvedTarget?: ResolvedMessagingTarget;
abortSignal?: AbortSignal;
}; };
function resolveGateway(input: RunMessageActionParams): MessageActionRunnerGateway | undefined { function resolveGateway(input: RunMessageActionParams): MessageActionRunnerGateway | undefined {
if (!input.gateway) return undefined; if (!input.gateway) return undefined;
@@ -526,7 +524,6 @@ async function handleBroadcastAction(
input: RunMessageActionParams, input: RunMessageActionParams,
params: Record<string, unknown>, params: Record<string, unknown>,
): Promise<MessageActionRunResult> { ): Promise<MessageActionRunResult> {
throwIfAborted(input.abortSignal);
const broadcastEnabled = input.cfg.tools?.message?.broadcast?.enabled !== false; const broadcastEnabled = input.cfg.tools?.message?.broadcast?.enabled !== false;
if (!broadcastEnabled) { if (!broadcastEnabled) {
throw new Error("Broadcast is disabled. Set tools.message.broadcast.enabled to true."); throw new Error("Broadcast is disabled. Set tools.message.broadcast.enabled to true.");
@@ -551,11 +548,8 @@ async function handleBroadcastAction(
error?: string; error?: string;
result?: MessageSendResult; result?: MessageSendResult;
}> = []; }> = [];
const isAbortError = (err: unknown): boolean => err instanceof Error && err.name === "AbortError";
for (const targetChannel of targetChannels) { for (const targetChannel of targetChannels) {
throwIfAborted(input.abortSignal);
for (const target of rawTargets) { for (const target of rawTargets) {
throwIfAborted(input.abortSignal);
try { try {
const resolved = await resolveChannelTarget({ const resolved = await resolveChannelTarget({
cfg: input.cfg, cfg: input.cfg,
@@ -579,7 +573,6 @@ async function handleBroadcastAction(
result: sendResult.kind === "send" ? sendResult.sendResult : undefined, result: sendResult.kind === "send" ? sendResult.sendResult : undefined,
}); });
} catch (err) { } catch (err) {
if (isAbortError(err)) throw err;
results.push({ results.push({
channel: targetChannel, channel: targetChannel,
to: target, to: target,
@@ -599,28 +592,8 @@ async function handleBroadcastAction(
}; };
} }
function throwIfAborted(abortSignal?: AbortSignal): void {
if (abortSignal?.aborted) {
const err = new Error("Message send aborted");
err.name = "AbortError";
throw err;
}
}
async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActionRunResult> { async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActionRunResult> {
const { const { cfg, params, channel, accountId, dryRun, gateway, input, agentId, resolvedTarget } = ctx;
cfg,
params,
channel,
accountId,
dryRun,
gateway,
input,
agentId,
resolvedTarget,
abortSignal,
} = ctx;
throwIfAborted(abortSignal);
const action: ChannelMessageActionName = "send"; const action: ChannelMessageActionName = "send";
const to = readStringParam(params, "to", { required: true }); const to = readStringParam(params, "to", { required: true });
// Support media, path, and filePath parameters for attachments // Support media, path, and filePath parameters for attachments
@@ -703,7 +676,6 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
} }
const mirrorMediaUrls = const mirrorMediaUrls =
mergedMediaUrls.length > 0 ? mergedMediaUrls : mediaUrl ? [mediaUrl] : undefined; mergedMediaUrls.length > 0 ? mergedMediaUrls : mediaUrl ? [mediaUrl] : undefined;
throwIfAborted(abortSignal);
const send = await executeSendAction({ const send = await executeSendAction({
ctx: { ctx: {
cfg, cfg,
@@ -723,7 +695,6 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
mediaUrls: mirrorMediaUrls, mediaUrls: mirrorMediaUrls,
} }
: undefined, : undefined,
abortSignal,
}, },
to, to,
message, message,
@@ -747,8 +718,7 @@ async function handleSendAction(ctx: ResolvedActionContext): Promise<MessageActi
} }
async function handlePollAction(ctx: ResolvedActionContext): Promise<MessageActionRunResult> { async function handlePollAction(ctx: ResolvedActionContext): Promise<MessageActionRunResult> {
const { cfg, params, channel, accountId, dryRun, gateway, input, abortSignal } = ctx; const { cfg, params, channel, accountId, dryRun, gateway, input } = ctx;
throwIfAborted(abortSignal);
const action: ChannelMessageActionName = "poll"; const action: ChannelMessageActionName = "poll";
const to = readStringParam(params, "to", { required: true }); const to = readStringParam(params, "to", { required: true });
const question = readStringParam(params, "pollQuestion", { const question = readStringParam(params, "pollQuestion", {
@@ -807,8 +777,7 @@ async function handlePollAction(ctx: ResolvedActionContext): Promise<MessageActi
} }
async function handlePluginAction(ctx: ResolvedActionContext): Promise<MessageActionRunResult> { async function handlePluginAction(ctx: ResolvedActionContext): Promise<MessageActionRunResult> {
const { cfg, params, channel, accountId, dryRun, gateway, input, abortSignal } = ctx; const { cfg, params, channel, accountId, dryRun, gateway, input } = ctx;
throwIfAborted(abortSignal);
const action = input.action as Exclude<ChannelMessageActionName, "send" | "poll" | "broadcast">; const action = input.action as Exclude<ChannelMessageActionName, "send" | "poll" | "broadcast">;
if (dryRun) { if (dryRun) {
return { return {
@@ -961,7 +930,6 @@ export async function runMessageAction(
input, input,
agentId: resolvedAgentId, agentId: resolvedAgentId,
resolvedTarget, resolvedTarget,
abortSignal: input.abortSignal,
}); });
} }
@@ -974,7 +942,6 @@ export async function runMessageAction(
dryRun, dryRun,
gateway, gateway,
input, input,
abortSignal: input.abortSignal,
}); });
} }
@@ -986,6 +953,5 @@ export async function runMessageAction(
dryRun, dryRun,
gateway, gateway,
input, input,
abortSignal: input.abortSignal,
}); });
} }
-2
View File
@@ -50,7 +50,6 @@ type MessageSendParams = {
text?: string; text?: string;
mediaUrls?: string[]; mediaUrls?: string[];
}; };
abortSignal?: AbortSignal;
}; };
export type MessageSendResult = { export type MessageSendResult = {
@@ -168,7 +167,6 @@ export async function sendMessage(params: MessageSendParams): Promise<MessageSen
gifPlayback: params.gifPlayback, gifPlayback: params.gifPlayback,
deps: params.deps, deps: params.deps,
bestEffort: params.bestEffort, bestEffort: params.bestEffort,
abortSignal: params.abortSignal,
mirror: params.mirror mirror: params.mirror
? { ? {
...params.mirror, ...params.mirror,
+1 -3
View File
@@ -119,8 +119,6 @@ export async function buildCrossContextDecoration(params: {
accountId?: string | null; accountId?: string | null;
}): Promise<CrossContextDecoration | null> { }): Promise<CrossContextDecoration | null> {
if (!params.toolContext?.currentChannelId) return null; if (!params.toolContext?.currentChannelId) return null;
// Skip decoration for direct tool sends (agent composing, not forwarding)
if (params.toolContext.skipCrossContextDecoration) return null;
if (!isCrossContextTarget(params)) return null; if (!isCrossContextTarget(params)) return null;
const markerConfig = params.cfg.tools?.message?.crossContext?.marker; const markerConfig = params.cfg.tools?.message?.crossContext?.marker;
@@ -133,11 +131,11 @@ export async function buildCrossContextDecoration(params: {
targetId: params.toolContext.currentChannelId, targetId: params.toolContext.currentChannelId,
accountId: params.accountId ?? undefined, accountId: params.accountId ?? undefined,
})) ?? params.toolContext.currentChannelId; })) ?? params.toolContext.currentChannelId;
// Don't force group formatting here; currentChannelId can be a DM or a group.
const originLabel = formatTargetDisplay({ const originLabel = formatTargetDisplay({
channel: params.channel, channel: params.channel,
target: params.toolContext.currentChannelId, target: params.toolContext.currentChannelId,
display: currentName, display: currentName,
kind: "group",
}); });
const prefixTemplate = markerConfig?.prefix ?? "[from {channel}] "; const prefixTemplate = markerConfig?.prefix ?? "[from {channel}] ";
const suffixTemplate = markerConfig?.suffix ?? ""; const suffixTemplate = markerConfig?.suffix ?? "";
@@ -32,7 +32,6 @@ export type OutboundSendContext = {
text?: string; text?: string;
mediaUrls?: string[]; mediaUrls?: string[];
}; };
abortSignal?: AbortSignal;
}; };
function extractToolPayload(result: AgentToolResult<unknown>): unknown { function extractToolPayload(result: AgentToolResult<unknown>): unknown {
@@ -57,14 +56,6 @@ function extractToolPayload(result: AgentToolResult<unknown>): unknown {
return result.content ?? result; return result.content ?? result;
} }
function throwIfAborted(abortSignal?: AbortSignal): void {
if (abortSignal?.aborted) {
const err = new Error("Message send aborted");
err.name = "AbortError";
throw err;
}
}
export async function executeSendAction(params: { export async function executeSendAction(params: {
ctx: OutboundSendContext; ctx: OutboundSendContext;
to: string; to: string;
@@ -79,7 +70,6 @@ export async function executeSendAction(params: {
toolResult?: AgentToolResult<unknown>; toolResult?: AgentToolResult<unknown>;
sendResult?: MessageSendResult; sendResult?: MessageSendResult;
}> { }> {
throwIfAborted(params.ctx.abortSignal);
if (!params.ctx.dryRun) { if (!params.ctx.dryRun) {
const handled = await dispatchChannelMessageAction({ const handled = await dispatchChannelMessageAction({
channel: params.ctx.channel, channel: params.ctx.channel,
@@ -113,7 +103,6 @@ export async function executeSendAction(params: {
} }
} }
throwIfAborted(params.ctx.abortSignal);
const result: MessageSendResult = await sendMessage({ const result: MessageSendResult = await sendMessage({
cfg: params.ctx.cfg, cfg: params.ctx.cfg,
to: params.to, to: params.to,
@@ -128,7 +117,6 @@ export async function executeSendAction(params: {
deps: params.ctx.deps, deps: params.ctx.deps,
gateway: params.ctx.gateway, gateway: params.ctx.gateway,
mirror: params.ctx.mirror, mirror: params.ctx.mirror,
abortSignal: params.ctx.abortSignal,
}); });
return { return {
+17 -70
View File
@@ -100,12 +100,7 @@ export function formatTargetDisplay(params: {
if (!trimmedTarget) return trimmedTarget; if (!trimmedTarget) return trimmedTarget;
if (trimmedTarget.startsWith("#") || trimmedTarget.startsWith("@")) return trimmedTarget; if (trimmedTarget.startsWith("#") || trimmedTarget.startsWith("@")) return trimmedTarget;
const channelPrefix = `${params.channel}:`; const withoutPrefix = trimmedTarget.replace(/^telegram:/i, "");
const withoutProvider = trimmedTarget.toLowerCase().startsWith(channelPrefix)
? trimmedTarget.slice(channelPrefix.length)
: trimmedTarget;
const withoutPrefix = withoutProvider.replace(/^telegram:/i, "");
if (/^channel:/i.test(withoutPrefix)) { if (/^channel:/i.test(withoutPrefix)) {
return `#${withoutPrefix.replace(/^channel:/i, "")}`; return `#${withoutPrefix.replace(/^channel:/i, "")}`;
} }
@@ -124,23 +119,14 @@ function preserveTargetCase(channel: ChannelId, raw: string, normalized: string)
return trimmed; return trimmed;
} }
function detectTargetKind( function detectTargetKind(raw: string, preferred?: TargetResolveKind): TargetResolveKind {
channel: ChannelId,
raw: string,
preferred?: TargetResolveKind,
): TargetResolveKind {
if (preferred) return preferred; if (preferred) return preferred;
const trimmed = raw.trim(); const trimmed = raw.trim();
if (!trimmed) return "group"; if (!trimmed) return "group";
if (trimmed.startsWith("@") || /^<@!?/.test(trimmed) || /^user:/i.test(trimmed)) return "user"; if (trimmed.startsWith("@") || /^<@!?/.test(trimmed) || /^user:/i.test(trimmed)) return "user";
if (trimmed.startsWith("#") || /^channel:/i.test(trimmed)) return "group"; if (trimmed.startsWith("#") || /^channel:/i.test(trimmed)) {
return "group";
// For some channels (e.g., BlueBubbles/iMessage), bare phone numbers are almost always DM targets.
if ((channel === "bluebubbles" || channel === "imessage") && /^\+?\d{6,}$/.test(trimmed)) {
return "user";
} }
return "group"; return "group";
} }
@@ -296,7 +282,7 @@ export async function resolveMessagingTarget(params: {
const plugin = getChannelPlugin(params.channel); const plugin = getChannelPlugin(params.channel);
const providerLabel = plugin?.meta?.label ?? params.channel; const providerLabel = plugin?.meta?.label ?? params.channel;
const hint = plugin?.messaging?.targetResolver?.hint; const hint = plugin?.messaging?.targetResolver?.hint;
const kind = detectTargetKind(params.channel, raw, params.preferredKind); const kind = detectTargetKind(raw, params.preferredKind);
const normalized = normalizeTargetForProvider(params.channel, raw) ?? raw; const normalized = normalizeTargetForProvider(params.channel, raw) ?? raw;
const looksLikeTargetId = (): boolean => { const looksLikeTargetId = (): boolean => {
const trimmed = raw.trim(); const trimmed = raw.trim();
@@ -305,12 +291,7 @@ export async function resolveMessagingTarget(params: {
if (lookup) return lookup(trimmed, normalized); if (lookup) return lookup(trimmed, normalized);
if (/^(channel|group|user):/i.test(trimmed)) return true; if (/^(channel|group|user):/i.test(trimmed)) return true;
if (/^[@#]/.test(trimmed)) return true; if (/^[@#]/.test(trimmed)) return true;
if (/^\+?\d{6,}$/.test(trimmed)) { if (/^\+?\d{6,}$/.test(trimmed)) return true;
// BlueBubbles/iMessage phone numbers should usually resolve via the directory to a DM chat,
// otherwise the provider may pick an existing group containing that handle.
if (params.channel === "bluebubbles" || params.channel === "imessage") return false;
return true;
}
if (trimmed.includes("@thread")) return true; if (trimmed.includes("@thread")) return true;
if (/^(conversation|user):/i.test(trimmed)) return true; if (/^(conversation|user):/i.test(trimmed)) return true;
return false; return false;
@@ -372,24 +353,6 @@ export async function resolveMessagingTarget(params: {
candidates: match.entries, candidates: match.entries,
}; };
} }
// For iMessage-style channels, allow sending directly to the normalized handle
// even if the directory doesn't contain an entry yet.
if (
(params.channel === "bluebubbles" || params.channel === "imessage") &&
/^\+?\d{6,}$/.test(query)
) {
const directTarget = preserveTargetCase(params.channel, raw, normalized);
return {
ok: true,
target: {
to: directTarget,
kind,
display: stripTargetPrefixes(raw),
source: "normalized",
},
};
}
return { return {
ok: false, ok: false,
error: unknownTargetError(providerLabel, raw, hint), error: unknownTargetError(providerLabel, raw, hint),
@@ -404,32 +367,16 @@ export async function lookupDirectoryDisplay(params: {
runtime?: RuntimeEnv; runtime?: RuntimeEnv;
}): Promise<string | undefined> { }): Promise<string | undefined> {
const normalized = normalizeTargetForProvider(params.channel, params.targetId) ?? params.targetId; const normalized = normalizeTargetForProvider(params.channel, params.targetId) ?? params.targetId;
const candidates = await getDirectoryEntries({
// Targets can resolve to either peers (DMs) or groups. Try both. cfg: params.cfg,
const [groups, users] = await Promise.all([ channel: params.channel,
getDirectoryEntries({ accountId: params.accountId,
cfg: params.cfg, kind: "group",
channel: params.channel, runtime: params.runtime,
accountId: params.accountId, preferLiveOnMiss: false,
kind: "group", });
runtime: params.runtime, const entry = candidates.find(
preferLiveOnMiss: false, (candidate) => normalizeDirectoryEntryId(params.channel, candidate) === normalized,
}), );
getDirectoryEntries({
cfg: params.cfg,
channel: params.channel,
accountId: params.accountId,
kind: "user",
runtime: params.runtime,
preferLiveOnMiss: false,
}),
]);
const findMatch = (candidates: ChannelDirectoryEntry[]) =>
candidates.find(
(candidate) => normalizeDirectoryEntryId(params.channel, candidate) === normalized,
);
const entry = findMatch(groups) ?? findMatch(users);
return entry?.name ?? entry?.handle ?? undefined; return entry?.name ?? entry?.handle ?? undefined;
} }
-41
View File
@@ -4,11 +4,6 @@ import os from "node:os";
import path from "node:path"; import path from "node:path";
import type { ClawdbotConfig } from "../config/config.js"; import type { ClawdbotConfig } from "../config/config.js";
import {
findModelInCatalog,
loadModelCatalog,
modelSupportsVision,
} from "../agents/model-catalog.js";
import type { MsgContext } from "../auto-reply/templating.js"; import type { MsgContext } from "../auto-reply/templating.js";
import { applyTemplate } from "../auto-reply/templating.js"; import { applyTemplate } from "../auto-reply/templating.js";
import { requireApiKey, resolveApiKeyForProvider } from "../agents/model-auth.js"; import { requireApiKey, resolveApiKeyForProvider } from "../agents/model-auth.js";
@@ -1019,42 +1014,6 @@ export async function runCapability(params: {
}; };
} }
// Skip image understanding when the primary model supports vision natively.
// The image will be injected directly into the model context instead.
const activeProvider = params.activeModel?.provider?.trim();
if (capability === "image" && activeProvider) {
const catalog = await loadModelCatalog({ config: cfg });
const entry = findModelInCatalog(catalog, activeProvider, params.activeModel?.model ?? "");
if (modelSupportsVision(entry)) {
if (shouldLogVerbose()) {
logVerbose("Skipping image understanding: primary model supports vision natively");
}
const model = params.activeModel?.model?.trim();
const reason = "primary model supports vision natively";
return {
outputs: [],
decision: {
capability,
outcome: "skipped",
attachments: selected.map((item) => {
const attempt = {
type: "provider" as const,
provider: activeProvider,
model: model || undefined,
outcome: "skipped" as const,
reason,
};
return {
attachmentIndex: item.index,
attempts: [attempt],
chosen: attempt,
};
}),
},
};
}
}
const entries = resolveModelEntries({ const entries = resolveModelEntries({
cfg, cfg,
capability, capability,
@@ -1,61 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import type { MsgContext } from "../auto-reply/templating.js";
import type { ClawdbotConfig } from "../config/config.js";
import {
buildProviderRegistry,
createMediaAttachmentCache,
normalizeMediaAttachments,
runCapability,
} from "./runner.js";
const catalog = [
{
id: "gpt-4.1",
name: "GPT-4.1",
provider: "openai",
input: ["text", "image"] as const,
},
];
vi.mock("../agents/model-catalog.js", async () => {
const actual = await vi.importActual<typeof import("../agents/model-catalog.js")>(
"../agents/model-catalog.js",
);
return {
...actual,
loadModelCatalog: vi.fn(async () => catalog),
};
});
describe("runCapability image skip", () => {
it("skips image understanding when the active model supports vision", async () => {
const ctx: MsgContext = { MediaPath: "/tmp/image.png", MediaType: "image/png" };
const media = normalizeMediaAttachments(ctx);
const cache = createMediaAttachmentCache(media);
const cfg = {} as ClawdbotConfig;
try {
const result = await runCapability({
capability: "image",
cfg,
ctx,
attachments: cache,
media,
providerRegistry: buildProviderRegistry(),
activeModel: { provider: "openai", model: "gpt-4.1" },
});
expect(result.outputs).toHaveLength(0);
expect(result.decision.outcome).toBe("skipped");
expect(result.decision.attachments).toHaveLength(1);
expect(result.decision.attachments[0]?.attachmentIndex).toBe(0);
expect(result.decision.attachments[0]?.attempts[0]?.outcome).toBe("skipped");
expect(result.decision.attachments[0]?.attempts[0]?.reason).toBe(
"primary model supports vision natively",
);
} finally {
await cache.cleanup();
}
});
});
+50 -88
View File
@@ -4,16 +4,13 @@ import type {
ReactionType, ReactionType,
ReactionTypeEmoji, ReactionTypeEmoji,
} from "@grammyjs/types"; } from "@grammyjs/types";
import { type ApiClientOptions, Bot, HttpError, InputFile } from "grammy"; import { type ApiClientOptions, Bot, InputFile } from "grammy";
import { loadConfig } from "../config/config.js"; import { loadConfig } from "../config/config.js";
import { logVerbose } from "../globals.js"; import { logVerbose } from "../globals.js";
import { recordChannelActivity } from "../infra/channel-activity.js"; import { recordChannelActivity } from "../infra/channel-activity.js";
import { formatErrorMessage, formatUncaughtError } from "../infra/errors.js"; import { formatErrorMessage } from "../infra/errors.js";
import { isDiagnosticFlagEnabled } from "../infra/diagnostic-flags.js";
import type { RetryConfig } from "../infra/retry.js"; import type { RetryConfig } from "../infra/retry.js";
import { createTelegramRetryRunner } from "../infra/retry-policy.js"; import { createTelegramRetryRunner } from "../infra/retry-policy.js";
import { redactSensitiveText } from "../logging/redact.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { mediaKindFromMime } from "../media/constants.js"; import { mediaKindFromMime } from "../media/constants.js";
import { isGifMedia } from "../media/mime.js"; import { isGifMedia } from "../media/mime.js";
import { loadWebMedia } from "../web/media.js"; import { loadWebMedia } from "../web/media.js";
@@ -62,19 +59,6 @@ type TelegramReactionOpts = {
}; };
const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i; const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i;
const diagLogger = createSubsystemLogger("telegram/diagnostic");
function createTelegramHttpLogger(cfg: ReturnType<typeof loadConfig>) {
const enabled = isDiagnosticFlagEnabled("telegram.http", cfg);
if (!enabled) {
return () => {};
}
return (label: string, err: unknown) => {
if (!(err instanceof HttpError)) return;
const detail = redactSensitiveText(formatUncaughtError(err.error ?? err));
diagLogger.warn(`telegram http error (${label}): ${detail}`);
};
}
function resolveToken(explicit: string | undefined, params: { accountId: string; token: string }) { function resolveToken(explicit: string | undefined, params: { accountId: string; token: string }) {
if (explicit?.trim()) return explicit.trim(); if (explicit?.trim()) return explicit.trim();
@@ -194,12 +178,7 @@ export async function sendMessageTelegram(
configRetry: account.config.retry, configRetry: account.config.retry,
verbose: opts.verbose, verbose: opts.verbose,
}); });
const logHttpError = createTelegramHttpLogger(cfg);
const requestWithDiag = <T>(fn: () => Promise<T>, label?: string) =>
request(fn, label).catch((err) => {
logHttpError(label ?? "request", err);
throw err;
});
const wrapChatNotFound = (err: unknown) => { const wrapChatNotFound = (err: unknown) => {
if (!/400: Bad Request: chat not found/i.test(formatErrorMessage(err))) return err; if (!/400: Bad Request: chat not found/i.test(formatErrorMessage(err))) return err;
return new Error( return new Error(
@@ -238,31 +217,30 @@ export async function sendMessageTelegram(
parse_mode: "HTML" as const, parse_mode: "HTML" as const,
...baseParams, ...baseParams,
}; };
const res = await requestWithDiag( const res = await request(() => api.sendMessage(chatId, htmlText, sendParams), "message").catch(
() => api.sendMessage(chatId, htmlText, sendParams), async (err) => {
"message", // Telegram rejects malformed HTML (e.g., unsupported tags or entities).
).catch(async (err) => { // When that happens, fall back to plain text so the message still delivers.
// Telegram rejects malformed HTML (e.g., unsupported tags or entities). const errText = formatErrorMessage(err);
// When that happens, fall back to plain text so the message still delivers. if (PARSE_ERR_RE.test(errText)) {
const errText = formatErrorMessage(err); if (opts.verbose) {
if (PARSE_ERR_RE.test(errText)) { console.warn(`telegram HTML parse failed, retrying as plain text: ${errText}`);
if (opts.verbose) { }
console.warn(`telegram HTML parse failed, retrying as plain text: ${errText}`); const fallback = fallbackText ?? rawText;
const plainParams = hasBaseParams ? baseParams : undefined;
return await request(
() =>
plainParams
? api.sendMessage(chatId, fallback, plainParams)
: api.sendMessage(chatId, fallback),
"message-plain",
).catch((err2) => {
throw wrapChatNotFound(err2);
});
} }
const fallback = fallbackText ?? rawText; throw wrapChatNotFound(err);
const plainParams = hasBaseParams ? baseParams : undefined; },
return await requestWithDiag( );
() =>
plainParams
? api.sendMessage(chatId, fallback, plainParams)
: api.sendMessage(chatId, fallback),
"message-plain",
).catch((err2) => {
throw wrapChatNotFound(err2);
});
}
throw wrapChatNotFound(err);
});
return res; return res;
}; };
@@ -299,20 +277,19 @@ export async function sendMessageTelegram(
| Awaited<ReturnType<typeof api.sendAnimation>> | Awaited<ReturnType<typeof api.sendAnimation>>
| Awaited<ReturnType<typeof api.sendDocument>>; | Awaited<ReturnType<typeof api.sendDocument>>;
if (isGif) { if (isGif) {
result = await requestWithDiag( result = await request(() => api.sendAnimation(chatId, file, mediaParams), "animation").catch(
() => api.sendAnimation(chatId, file, mediaParams), (err) => {
"animation", throw wrapChatNotFound(err);
).catch((err) => { },
throw wrapChatNotFound(err); );
});
} else if (kind === "image") { } else if (kind === "image") {
result = await requestWithDiag(() => api.sendPhoto(chatId, file, mediaParams), "photo").catch( result = await request(() => api.sendPhoto(chatId, file, mediaParams), "photo").catch(
(err) => { (err) => {
throw wrapChatNotFound(err); throw wrapChatNotFound(err);
}, },
); );
} else if (kind === "video") { } else if (kind === "video") {
result = await requestWithDiag(() => api.sendVideo(chatId, file, mediaParams), "video").catch( result = await request(() => api.sendVideo(chatId, file, mediaParams), "video").catch(
(err) => { (err) => {
throw wrapChatNotFound(err); throw wrapChatNotFound(err);
}, },
@@ -325,27 +302,24 @@ export async function sendMessageTelegram(
logFallback: logVerbose, logFallback: logVerbose,
}); });
if (useVoice) { if (useVoice) {
result = await requestWithDiag( result = await request(() => api.sendVoice(chatId, file, mediaParams), "voice").catch(
() => api.sendVoice(chatId, file, mediaParams), (err) => {
"voice", throw wrapChatNotFound(err);
).catch((err) => { },
throw wrapChatNotFound(err); );
});
} else { } else {
result = await requestWithDiag( result = await request(() => api.sendAudio(chatId, file, mediaParams), "audio").catch(
() => api.sendAudio(chatId, file, mediaParams), (err) => {
"audio", throw wrapChatNotFound(err);
).catch((err) => { },
throw wrapChatNotFound(err); );
});
} }
} else { } else {
result = await requestWithDiag( result = await request(() => api.sendDocument(chatId, file, mediaParams), "document").catch(
() => api.sendDocument(chatId, file, mediaParams), (err) => {
"document", throw wrapChatNotFound(err);
).catch((err) => { },
throw wrapChatNotFound(err); );
});
} }
const mediaMessageId = String(result?.message_id ?? "unknown"); const mediaMessageId = String(result?.message_id ?? "unknown");
const resolvedChatId = String(result?.chat?.id ?? chatId); const resolvedChatId = String(result?.chat?.id ?? chatId);
@@ -426,12 +400,6 @@ export async function reactMessageTelegram(
configRetry: account.config.retry, configRetry: account.config.retry,
verbose: opts.verbose, verbose: opts.verbose,
}); });
const logHttpError = createTelegramHttpLogger(cfg);
const requestWithDiag = <T>(fn: () => Promise<T>, label?: string) =>
request(fn, label).catch((err) => {
logHttpError(label ?? "request", err);
throw err;
});
const remove = opts.remove === true; const remove = opts.remove === true;
const trimmedEmoji = emoji.trim(); const trimmedEmoji = emoji.trim();
// Build the reaction array. We cast emoji to the grammY union type since // Build the reaction array. We cast emoji to the grammY union type since
@@ -443,7 +411,7 @@ export async function reactMessageTelegram(
if (typeof api.setMessageReaction !== "function") { if (typeof api.setMessageReaction !== "function") {
throw new Error("Telegram reactions are unavailable in this bot API."); throw new Error("Telegram reactions are unavailable in this bot API.");
} }
await requestWithDiag(() => api.setMessageReaction(chatId, messageId, reactions), "reaction"); await request(() => api.setMessageReaction(chatId, messageId, reactions), "reaction");
return { ok: true }; return { ok: true };
} }
@@ -478,13 +446,7 @@ export async function deleteMessageTelegram(
configRetry: account.config.retry, configRetry: account.config.retry,
verbose: opts.verbose, verbose: opts.verbose,
}); });
const logHttpError = createTelegramHttpLogger(cfg); await request(() => api.deleteMessage(chatId, messageId), "deleteMessage");
const requestWithDiag = <T>(fn: () => Promise<T>, label?: string) =>
request(fn, label).catch((err) => {
logHttpError(label ?? "request", err);
throw err;
});
await requestWithDiag(() => api.deleteMessage(chatId, messageId), "deleteMessage");
logVerbose(`[telegram] Deleted message ${messageId} from chat ${chatId}`); logVerbose(`[telegram] Deleted message ${messageId} from chat ${chatId}`);
return { ok: true }; return { ok: true };
} }
-1
View File
@@ -169,7 +169,6 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
token: settings.gatewayToken, token: settings.gatewayToken,
runtime: daemonRuntime, runtime: daemonRuntime,
warn: (message, title) => prompter.note(message, title), warn: (message, title) => prompter.note(message, title),
config: nextConfig,
}); });
progress.update("Installing Gateway service…"); progress.update("Installing Gateway service…");
+1
View File
@@ -1,2 +1,3 @@
import "./styles.css"; import "./styles.css";
import "./ui/app.ts"; import "./ui/app.ts";
+84 -238
View File
@@ -1,172 +1,60 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap"); @import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Unbounded:wght@400;500;600&family=Work+Sans:wght@400;500;600;700&display=swap");
:root { :root {
/* Background - Deep Navy Slate */ --bg: #0a0f14;
--bg: #0c0d12; --bg-accent: #111826;
--bg-accent: #0d0e14; --bg-grad-1: #162031;
--bg-elevated: #181a21; --bg-grad-2: #1f2a22;
--bg-hover: #252830; --bg-overlay: rgba(255, 255, 255, 0.05);
--bg-muted: #252830; --bg-glow: rgba(245, 159, 74, 0.12);
--panel: rgba(14, 20, 30, 0.88);
/* Card / Surface */ --panel-strong: rgba(18, 26, 38, 0.96);
--card: #13151c; --chrome: rgba(9, 14, 20, 0.72);
--card-foreground: #f8fafc; --chrome-strong: rgba(9, 14, 20, 0.86);
--card-highlight: rgba(255, 255, 255, 0.04); --text: rgba(244, 246, 251, 0.96);
--popover: #13151c; --chat-text: rgba(231, 237, 244, 0.92);
--popover-foreground: #f8fafc; --muted: rgba(156, 169, 189, 0.72);
--border: rgba(255, 255, 255, 0.09);
/* Panel */ --border-strong: rgba(255, 255, 255, 0.16);
--panel: #0c0d12; --accent: #f59f4a;
--panel-strong: #181a21; --accent-2: #34c7b7;
--panel-hover: #252830; --ok: #2bd97f;
--chrome: rgba(12, 13, 18, 0.95); --warn: #f2c94c;
--chrome-strong: rgba(12, 13, 18, 0.98); --danger: #ff6b6b;
--focus: rgba(245, 159, 74, 0.35);
/* Text */ --grid-line: rgba(255, 255, 255, 0.04);
--text: #f8fafc;
--text-strong: #ffffff;
--chat-text: #f8fafc;
--muted: #94a3b8;
--muted-strong: #64748b;
--muted-foreground: #94a3b8;
/* Border */
--border: #333842;
--border-strong: #454d5c;
--border-hover: #5a6373;
--input: #333842;
--ring: #ff4d4d;
/* Accent - The signature red */
--accent: #ff4d4d;
--accent-hover: #ff6666;
--accent-muted: #ff4d4d;
--accent-subtle: rgba(255, 77, 77, 0.12);
--accent-foreground: #f8fafc;
--primary: #ff4d4d;
--primary-foreground: #ffffff;
/* Secondary */
--secondary: #252830;
--secondary-foreground: #f8fafc;
--accent-2: #3b82f6;
--accent-2-muted: rgba(59, 130, 246, 0.7);
/* Semantic */
--ok: #22c55e;
--ok-muted: rgba(34, 197, 94, 0.7);
--ok-subtle: rgba(34, 197, 94, 0.1);
--destructive: #ef4444;
--destructive-foreground: #fafafa;
--warn: #eab308;
--warn-muted: rgba(234, 179, 8, 0.7);
--warn-subtle: rgba(234, 179, 8, 0.1);
--danger: #ef4444;
--danger-muted: rgba(239, 68, 68, 0.7);
--danger-subtle: rgba(239, 68, 68, 0.1);
--info: #3b82f6;
/* Focus */
--focus: rgba(255, 77, 77, 0.2);
--focus-ring: 0 0 0 2px var(--bg), 0 0 0 4px var(--ring);
/* Grid */
--grid-line: rgba(255, 255, 255, 0.03);
/* Theme transition */
--theme-switch-x: 50%; --theme-switch-x: 50%;
--theme-switch-y: 50%; --theme-switch-y: 50%;
--mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
/* Typography */ "Liberation Mono", "Courier New", monospace;
--mono: "JetBrains Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, monospace; --font-body: "Work Sans", system-ui, sans-serif;
--font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; --font-display: "Unbounded", "Times New Roman", serif;
--font-display: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
/* Shadows - minimal */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
/* Radii - shadcn uses smaller radii */
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
--radius-full: 9999px;
--radius: 6px;
/* Transitions */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--duration-fast: 150ms;
--duration-normal: 200ms;
--duration-slow: 300ms;
color-scheme: dark; color-scheme: dark;
} }
/* Light theme */
:root[data-theme="light"] { :root[data-theme="light"] {
--bg: #f8f8f7; --bg: #f5f1ea;
--bg-accent: #f3f2f0; --bg-accent: #ffffff;
--bg-elevated: #ffffff; --bg-grad-1: #f1e6d6;
--bg-hover: #eae8e6; --bg-grad-2: #e5eef4;
--bg-muted: #eae8e6; --bg-overlay: rgba(28, 32, 46, 0.05);
--bg-content: #f0efed; --bg-glow: rgba(52, 199, 183, 0.14);
--panel: rgba(255, 255, 255, 0.9);
--card: #ffffff; --panel-strong: rgba(255, 255, 255, 0.97);
--card-foreground: #1c1917; --chrome: rgba(255, 255, 255, 0.75);
--card-highlight: rgba(0, 0, 0, 0.04); --chrome-strong: rgba(255, 255, 255, 0.88);
--popover: #ffffff; --text: rgba(27, 36, 50, 0.98);
--popover-foreground: #1c1917; --chat-text: rgba(36, 48, 66, 0.9);
--muted: rgba(80, 94, 114, 0.7);
--panel: #f8f8f7; --border: rgba(18, 24, 40, 0.12);
--panel-strong: #f0efed; --border-strong: rgba(18, 24, 40, 0.2);
--panel-hover: #e5e3e1; --accent: #e28a3f;
--chrome: rgba(248, 248, 247, 0.95); --accent-2: #1ba99d;
--chrome-strong: rgba(248, 248, 247, 0.98); --ok: #1aa86c;
--warn: #b3771c;
--text: #44403c; --danger: #d44848;
--text-strong: #292524; --focus: rgba(226, 138, 63, 0.35);
--chat-text: #44403c; --grid-line: rgba(18, 24, 40, 0.06);
--muted: #5c5856;
--muted-strong: #44403c;
--muted-foreground: #5c5856;
--border: #e0dedc;
--border-strong: #d6d3d1;
--border-hover: #a8a5a0;
--input: #e0dedc;
--accent: #b91c1c;
--accent-hover: #dc2626;
--accent-muted: #b91c1c;
--accent-subtle: rgba(185, 28, 28, 0.18);
--accent-foreground: #ffffff;
--primary: #b91c1c;
--primary-foreground: #ffffff;
--secondary: #eae8e6;
--secondary-foreground: #44403c;
--ok: #15803d;
--ok-muted: rgba(21, 128, 61, 0.75);
--ok-subtle: rgba(21, 128, 61, 0.12);
--destructive: #b91c1c;
--destructive-foreground: #fafafa;
--warn: #a16207;
--warn-muted: rgba(161, 98, 7, 0.75);
--warn-subtle: rgba(161, 98, 7, 0.12);
--danger: #b91c1c;
--danger-muted: rgba(185, 28, 28, 0.75);
--danger-subtle: rgba(185, 28, 28, 0.12);
--info: #1d4ed8;
--focus: rgba(185, 28, 28, 0.25);
--grid-line: rgba(0, 0, 0, 0.06);
color-scheme: light; color-scheme: light;
} }
@@ -181,21 +69,44 @@ body {
body { body {
margin: 0; margin: 0;
font: 400 14px/1.5 var(--font-body); font: 15px/1.5 var(--font-body);
letter-spacing: -0.011em; background:
background: var(--bg); radial-gradient(1200px 900px at 15% -10%, var(--bg-grad-1) 0%, transparent 55%)
fixed,
radial-gradient(900px 700px at 80% 10%, var(--bg-grad-2) 0%, transparent 60%)
fixed,
linear-gradient(160deg, var(--bg) 0%, var(--bg-accent) 100%) fixed;
color: var(--text); color: var(--text);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
} }
/* Theme transition */ body::before {
content: "";
position: fixed;
inset: 0;
background:
linear-gradient(
140deg,
var(--bg-overlay) 0%,
rgba(255, 255, 255, 0) 40%
),
radial-gradient(620px 420px at 75% 75%, var(--bg-glow), transparent 60%);
pointer-events: none;
z-index: 0;
}
/* Grid overlay removed for cleaner look */
@keyframes theme-circle-transition { @keyframes theme-circle-transition {
0% { 0% {
clip-path: circle(0% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%)); clip-path: circle(
0% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%)
);
} }
100% { 100% {
clip-path: circle(150% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%)); clip-path: circle(
150% at var(--theme-switch-x, 50%) var(--theme-switch-y, 50%)
);
} }
} }
@@ -212,7 +123,7 @@ html.theme-transition::view-transition-old(theme) {
html.theme-transition::view-transition-new(theme) { html.theme-transition::view-transition-new(theme) {
mix-blend-mode: normal; mix-blend-mode: normal;
z-index: 2; z-index: 2;
animation: theme-circle-transition 0.4s var(--ease-out) forwards; animation: theme-circle-transition 0.45s ease-out forwards;
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
@@ -230,12 +141,7 @@ clawdbot-app {
} }
a { a {
color: var(--accent); color: inherit;
text-decoration: none;
}
a:hover {
text-decoration: underline;
} }
button, button,
@@ -246,35 +152,10 @@ select {
color: inherit; color: inherit;
} }
::selection {
background: var(--accent-subtle);
color: var(--text-strong);
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: var(--radius-full);
}
::-webkit-scrollbar-thumb:hover {
background: var(--border-strong);
}
/* Animations */
@keyframes rise { @keyframes rise {
from { from {
opacity: 0; opacity: 0;
transform: translateY(4px); transform: translateY(6px);
} }
to { to {
opacity: 1; opacity: 1;
@@ -282,48 +163,13 @@ select {
} }
} }
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: scale(0.96);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes dashboard-enter { @keyframes dashboard-enter {
from { from {
opacity: 0; opacity: 0;
transform: translateY(8px); transform: translateY(12px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
} }
} }
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
/* Focus visible styles */
:focus-visible {
outline: none;
box-shadow: var(--focus-ring);
}
+28 -70
View File
@@ -8,7 +8,7 @@
gap: 12px; gap: 12px;
align-items: flex-start; align-items: flex-start;
margin-bottom: 16px; margin-bottom: 16px;
margin-left: 4px; margin-left: 16px;
margin-right: 16px; margin-right: 16px;
} }
@@ -70,23 +70,23 @@
} }
.chat-avatar.user { .chat-avatar.user {
background: var(--accent-subtle); background: rgba(245, 159, 74, 0.2);
color: var(--accent); color: rgba(245, 159, 74, 1);
} }
.chat-avatar.assistant { .chat-avatar.assistant {
background: var(--secondary); background: rgba(52, 199, 183, 0.2);
color: var(--muted); color: rgba(52, 199, 183, 1);
} }
.chat-avatar.other { .chat-avatar.other {
background: var(--secondary); background: rgba(150, 150, 150, 0.2);
color: var(--muted); color: rgba(150, 150, 150, 1);
} }
.chat-avatar.tool { .chat-avatar.tool {
background: var(--secondary); background: rgba(134, 142, 150, 0.2);
color: var(--muted); color: rgba(134, 142, 150, 1);
} }
/* Image avatar support */ /* Image avatar support */
@@ -100,9 +100,9 @@ img.chat-avatar {
.chat-bubble { .chat-bubble {
position: relative; position: relative;
display: inline-block; display: inline-block;
border: 1px solid transparent; border: 1px solid var(--border);
background: var(--card); background: rgba(0, 0, 0, 0.12);
border-radius: var(--radius-lg); border-radius: 12px;
padding: 10px 14px; padding: 10px 14px;
box-shadow: none; box-shadow: none;
transition: background 150ms ease-out, border-color 150ms ease-out; transition: background 150ms ease-out, border-color 150ms ease-out;
@@ -119,9 +119,9 @@ img.chat-avatar {
top: 6px; top: 6px;
right: 8px; right: 8px;
border: 1px solid var(--border); border: 1px solid var(--border);
background: var(--bg); background: rgba(0, 0, 0, 0.22);
color: var(--muted); color: var(--muted);
border-radius: var(--radius-md); border-radius: 8px;
padding: 4px 6px; padding: 4px 6px;
font-size: 14px; font-size: 14px;
line-height: 1; line-height: 1;
@@ -132,40 +132,9 @@ img.chat-avatar {
} }
.chat-copy-btn__icon { .chat-copy-btn__icon {
display: inline-flex; display: inline-block;
width: 14px; width: 1em;
height: 14px; text-align: center;
position: relative;
}
.chat-copy-btn__icon svg {
width: 14px;
height: 14px;
stroke: currentColor;
fill: none;
stroke-width: 1.5px;
stroke-linecap: round;
stroke-linejoin: round;
}
.chat-copy-btn__icon-copy,
.chat-copy-btn__icon-check {
position: absolute;
top: 0;
left: 0;
transition: opacity 150ms ease;
}
.chat-copy-btn__icon-check {
opacity: 0;
}
.chat-copy-btn[data-copied="1"] .chat-copy-btn__icon-copy {
opacity: 0;
}
.chat-copy-btn[data-copied="1"] .chat-copy-btn__icon-check {
opacity: 1;
} }
.chat-bubble:hover .chat-copy-btn { .chat-bubble:hover .chat-copy-btn {
@@ -174,7 +143,7 @@ img.chat-avatar {
} }
.chat-copy-btn:hover { .chat-copy-btn:hover {
background: var(--bg-hover); background: rgba(0, 0, 0, 0.3);
} }
.chat-copy-btn[data-copying="1"] { .chat-copy-btn[data-copying="1"] {
@@ -185,17 +154,17 @@ img.chat-avatar {
.chat-copy-btn[data-error="1"] { .chat-copy-btn[data-error="1"] {
opacity: 1; opacity: 1;
pointer-events: auto; pointer-events: auto;
border-color: var(--danger-subtle); border-color: rgba(255, 69, 58, 0.8);
background: var(--danger-subtle); background: rgba(255, 69, 58, 0.18);
color: var(--danger); color: rgba(255, 69, 58, 1);
} }
.chat-copy-btn[data-copied="1"] { .chat-copy-btn[data-copied="1"] {
opacity: 1; opacity: 1;
pointer-events: auto; pointer-events: auto;
border-color: var(--ok-subtle); border-color: rgba(52, 199, 183, 0.8);
background: var(--ok-subtle); background: rgba(52, 199, 183, 0.18);
color: var(--ok); color: rgba(52, 199, 183, 1);
} }
.chat-copy-btn:focus-visible { .chat-copy-btn:focus-visible {
@@ -212,29 +181,18 @@ img.chat-avatar {
} }
} }
/* Light mode: restore borders */
:root[data-theme="light"] .chat-bubble {
border-color: var(--border);
box-shadow: inset 0 1px 0 var(--card-highlight);
}
.chat-bubble:hover { .chat-bubble:hover {
background: var(--bg-hover); background: rgba(0, 0, 0, 0.18);
} }
/* User bubbles have different styling */ /* User bubbles have different styling */
.chat-group.user .chat-bubble { .chat-group.user .chat-bubble {
background: var(--accent-subtle); background: rgba(245, 159, 74, 0.15);
border-color: transparent; border-color: rgba(245, 159, 74, 0.3);
}
:root[data-theme="light"] .chat-group.user .chat-bubble {
border-color: rgba(234, 88, 12, 0.2);
background: rgba(251, 146, 60, 0.12);
} }
.chat-group.user .chat-bubble:hover { .chat-group.user .chat-bubble:hover {
background: rgba(255, 77, 77, 0.15); background: rgba(245, 159, 74, 0.22);
} }
/* Streaming animation */ /* Streaming animation */
+16 -44
View File
@@ -52,8 +52,8 @@
flex: 1 1 0; /* Grow, shrink, and use 0 base for proper scrolling */ flex: 1 1 0; /* Grow, shrink, and use 0 base for proper scrolling */
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
padding: 12px 4px; padding: 12px;
margin: 0 -4px; margin: 0 -12px;
min-height: 0; /* Allow shrinking for flex scroll behavior */ min-height: 0; /* Allow shrinking for flex scroll behavior */
border-radius: 12px; border-radius: 12px;
background: transparent; background: transparent;
@@ -87,39 +87,23 @@
border-color: var(--accent); border-color: var(--accent);
} }
.chat-focus-exit svg {
width: 16px;
height: 16px;
stroke: currentColor;
fill: none;
stroke-width: 2px;
stroke-linecap: round;
stroke-linejoin: round;
}
/* Chat compose - sticky at bottom */ /* Chat compose - sticky at bottom */
.chat-compose { .chat-compose {
position: sticky; position: sticky;
bottom: 0; bottom: 0;
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
align-items: stretch; align-items: flex-end;
gap: 12px; gap: 12px;
margin-top: auto; /* Push to bottom of flex container */ margin-top: auto; /* Push to bottom of flex container */
padding: 12px 4px 4px; padding: 16px 0 4px;
background: linear-gradient(to bottom, transparent, var(--bg) 20%); background: linear-gradient(to bottom, transparent, var(--bg) 20%);
z-index: 10; z-index: 10;
} }
:root[data-theme="light"] .chat-compose {
background: linear-gradient(to bottom, transparent, var(--bg-content) 20%);
}
.chat-compose__field { .chat-compose__field {
flex: 1 1 auto; flex: 1 1 auto;
min-width: 0; min-width: 0;
display: flex;
align-items: stretch;
} }
/* Hide the "Message" label - keep textarea only */ /* Hide the "Message" label - keep textarea only */
@@ -130,11 +114,10 @@
/* Override .field textarea min-height (180px) from components.css */ /* Override .field textarea min-height (180px) from components.css */
.chat-compose .chat-compose__field textarea { .chat-compose .chat-compose__field textarea {
width: 100%; width: 100%;
height: 40px; min-height: 36px;
min-height: 40px;
max-height: 150px; max-height: 150px;
padding: 9px 12px; padding: 8px 12px;
border-radius: 8px; border-radius: 10px;
resize: vertical; resize: vertical;
white-space: pre-wrap; white-space: pre-wrap;
font-family: var(--font-body); font-family: var(--font-body);
@@ -151,18 +134,13 @@
flex-shrink: 0; flex-shrink: 0;
display: flex; display: flex;
align-items: stretch; align-items: stretch;
gap: 8px;
} }
.chat-compose .chat-compose__actions .btn { .chat-compose .chat-compose__actions .btn {
padding: 0 16px; padding: 8px 16px;
font-size: 13px; font-size: 13px;
height: 40px; min-height: 36px;
min-height: 40px;
max-height: 40px;
line-height: 1;
white-space: nowrap; white-space: nowrap;
box-sizing: border-box;
} }
/* Chat controls - moved to content-header area, left aligned */ /* Chat controls - moved to content-header area, left aligned */
@@ -216,27 +194,20 @@
/* Light theme icon button overrides */ /* Light theme icon button overrides */
:root[data-theme="light"] .btn--icon { :root[data-theme="light"] .btn--icon {
background: #ffffff; background: rgba(255, 255, 255, 0.9);
border-color: var(--border); border-color: rgba(16, 24, 40, 0.2);
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05); box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05);
color: var(--muted); color: rgba(16, 24, 40, 0.7);
} }
:root[data-theme="light"] .btn--icon:hover { :root[data-theme="light"] .btn--icon:hover {
background: #ffffff; background: rgba(255, 255, 255, 1);
border-color: var(--border-strong); border-color: rgba(16, 24, 40, 0.3);
color: var(--text); color: rgba(16, 24, 40, 0.9);
} }
.btn--icon svg { .btn--icon svg {
display: block; display: block;
width: 18px;
height: 18px;
stroke: currentColor;
fill: none;
stroke-width: 1.5px;
stroke-linecap: round;
stroke-linejoin: round;
} }
.chat-controls__session select { .chat-controls__session select {
@@ -279,3 +250,4 @@
min-width: 120px; min-width: 120px;
} }
} }
+4 -40
View File
@@ -14,8 +14,8 @@
} }
:root[data-theme="light"] .chat-thinking { :root[data-theme="light"] .chat-thinking {
border-color: rgba(16, 24, 40, 0.25); border-color: rgba(16, 24, 40, 0.18);
background: rgba(16, 24, 40, 0.04); background: rgba(16, 24, 40, 0.03);
} }
.chat-text { .chat-text {
@@ -75,46 +75,9 @@
} }
.chat-text :where(blockquote) { .chat-text :where(blockquote) {
border-left: 3px solid var(--border-strong); border-left: 3px solid var(--border);
padding-left: 12px; padding-left: 12px;
margin-left: 0;
color: var(--muted); color: var(--muted);
background: rgba(255, 255, 255, 0.02);
padding: 8px 12px;
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
}
.chat-text :where(blockquote blockquote) {
margin-top: 8px;
border-left-color: var(--border-hover);
background: rgba(255, 255, 255, 0.03);
}
.chat-text :where(blockquote blockquote blockquote) {
border-left-color: var(--muted-strong);
background: rgba(255, 255, 255, 0.04);
}
:root[data-theme="light"] .chat-text :where(blockquote) {
background: rgba(0, 0, 0, 0.03);
}
:root[data-theme="light"] .chat-text :where(blockquote blockquote) {
background: rgba(0, 0, 0, 0.05);
}
:root[data-theme="light"] .chat-text :where(blockquote blockquote blockquote) {
background: rgba(0, 0, 0, 0.04);
}
:root[data-theme="light"] .chat-text :where(:not(pre) > code) {
background: rgba(0, 0, 0, 0.08);
border: 1px solid rgba(0, 0, 0, 0.1);
}
:root[data-theme="light"] .chat-text :where(pre) {
background: rgba(0, 0, 0, 0.05);
border: 1px solid rgba(0, 0, 0, 0.1);
} }
.chat-text :where(hr) { .chat-text :where(hr) {
@@ -122,3 +85,4 @@
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
margin: 1em 0; margin: 1em 0;
} }
+17 -48
View File
@@ -4,8 +4,6 @@
border-radius: 8px; border-radius: 8px;
padding: 12px; padding: 12px;
margin-top: 8px; margin-top: 8px;
background: var(--card);
box-shadow: inset 0 1px 0 var(--card-highlight);
transition: border-color 150ms ease-out, background 150ms ease-out; transition: border-color 150ms ease-out, background 150ms ease-out;
/* Fixed max-height to ensure cards don't expand too much */ /* Fixed max-height to ensure cards don't expand too much */
max-height: 120px; max-height: 120px;
@@ -13,8 +11,8 @@
} }
.chat-tool-card:hover { .chat-tool-card:hover {
border-color: var(--border-strong); border-color: var(--accent);
background: var(--bg-hover); background: rgba(0, 0, 0, 0.06);
} }
/* First tool card in a group - no top margin */ /* First tool card in a group - no top margin */
@@ -52,63 +50,33 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 16px; width: 18px;
height: 16px; height: 18px;
font-size: 14px;
line-height: 1;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
vertical-align: middle;
flex-shrink: 0; flex-shrink: 0;
} }
.chat-tool-card__icon svg {
width: 14px;
height: 14px;
stroke: currentColor;
fill: none;
stroke-width: 1.5px;
stroke-linecap: round;
stroke-linejoin: round;
}
/* "View >" action link */ /* "View >" action link */
.chat-tool-card__action { .chat-tool-card__action {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px; font-size: 12px;
color: var(--accent); color: var(--accent);
opacity: 0.8; opacity: 0.8;
transition: opacity 150ms ease-out; transition: opacity 150ms ease-out;
} }
.chat-tool-card__action svg {
width: 12px;
height: 12px;
stroke: currentColor;
fill: none;
stroke-width: 1.5px;
stroke-linecap: round;
stroke-linejoin: round;
}
.chat-tool-card--clickable:hover .chat-tool-card__action { .chat-tool-card--clickable:hover .chat-tool-card__action {
opacity: 1; opacity: 1;
} }
/* Status indicator for completed/empty results */ /* Status indicator for completed/empty results */
.chat-tool-card__status { .chat-tool-card__status {
display: inline-flex; font-size: 14px;
align-items: center;
color: var(--ok); color: var(--ok);
} }
.chat-tool-card__status svg {
width: 14px;
height: 14px;
stroke: currentColor;
fill: none;
stroke-width: 2px;
stroke-linecap: round;
stroke-linejoin: round;
}
.chat-tool-card__status-text { .chat-tool-card__status-text {
font-size: 11px; font-size: 11px;
margin-top: 4px; margin-top: 4px;
@@ -126,18 +94,18 @@
color: var(--muted); color: var(--muted);
margin-top: 8px; margin-top: 8px;
padding: 8px 10px; padding: 8px 10px;
background: var(--secondary); background: rgba(0, 0, 0, 0.08);
border-radius: var(--radius-md); border-radius: 6px;
white-space: pre-wrap; white-space: pre-wrap;
overflow: hidden; overflow: hidden;
max-height: 44px; max-height: 44px;
line-height: 1.4; line-height: 1.4;
border: 1px solid var(--border); border: 1px solid rgba(255, 255, 255, 0.04);
} }
.chat-tool-card--clickable:hover .chat-tool-card__preview { .chat-tool-card--clickable:hover .chat-tool-card__preview {
background: var(--bg-hover); background: rgba(0, 0, 0, 0.12);
border-color: var(--border-strong); border-color: rgba(255, 255, 255, 0.08);
} }
/* Short inline output */ /* Short inline output */
@@ -146,8 +114,8 @@
color: var(--text); color: var(--text);
margin-top: 6px; margin-top: 6px;
padding: 6px 8px; padding: 6px 8px;
background: var(--secondary); background: rgba(0, 0, 0, 0.06);
border-radius: var(--radius-sm); border-radius: 4px;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
} }
@@ -196,3 +164,4 @@
transform: scale(1); transform: scale(1);
} }
} }
File diff suppressed because it is too large. Load diff
+296 -335
View File
File diff suppressed because it is too large. Load diff
+114 -205
View File
@@ -1,14 +1,10 @@
/* ===========================================
Shell Layout
=========================================== */
.shell { .shell {
--shell-pad: 16px; --shell-pad: 16px;
--shell-gap: 16px; --shell-gap: 16px;
--shell-nav-width: 220px; --shell-nav-width: 220px;
--shell-topbar-height: 56px; --shell-topbar-height: 56px;
--shell-focus-duration: 200ms; --shell-focus-duration: 220ms;
--shell-focus-ease: var(--ease-out); --shell-focus-ease: cubic-bezier(0.2, 0.85, 0.25, 1);
height: 100vh; height: 100vh;
display: grid; display: grid;
grid-template-columns: var(--shell-nav-width) minmax(0, 1fr); grid-template-columns: var(--shell-nav-width) minmax(0, 1fr);
@@ -17,7 +13,7 @@
"topbar topbar" "topbar topbar"
"nav content"; "nav content";
gap: 0; gap: 0;
animation: dashboard-enter 0.4s var(--ease-out); animation: dashboard-enter 0.6s ease-out;
transition: grid-template-columns var(--shell-focus-duration) var(--shell-focus-ease); transition: grid-template-columns var(--shell-focus-duration) var(--shell-focus-ease);
overflow: hidden; overflow: hidden;
} }
@@ -65,10 +61,6 @@
gap: 0; gap: 0;
} }
/* ===========================================
Topbar
=========================================== */
.topbar { .topbar {
grid-area: topbar; grid-area: topbar;
position: sticky; position: sticky;
@@ -81,7 +73,8 @@
padding: 0 20px; padding: 0 20px;
height: var(--shell-topbar-height); height: var(--shell-topbar-height);
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
background: var(--bg); background: var(--panel);
backdrop-filter: blur(18px);
} }
.topbar-left { .topbar-left {
@@ -91,83 +84,49 @@
} }
.topbar .nav-collapse-toggle { .topbar .nav-collapse-toggle {
width: 36px; width: 44px;
height: 36px; height: 44px;
margin-bottom: 0; margin-bottom: 0;
} }
.topbar .nav-collapse-toggle__icon { .topbar .nav-collapse-toggle__icon {
width: 20px; font-size: 22px;
height: 20px;
} }
.topbar .nav-collapse-toggle__icon svg {
width: 20px;
height: 20px;
}
/* Brand */
.brand { .brand {
display: flex;
align-items: center;
gap: 10px;
}
.brand-logo {
width: 28px;
height: 28px;
flex-shrink: 0;
}
.brand-logo img {
width: 100%;
height: 100%;
object-fit: contain;
}
.brand-text {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1px; gap: 2px;
} }
.brand-title { .brand-title {
font-size: 15px; font-family: var(--font-display);
font-size: 16px;
letter-spacing: 1px;
text-transform: uppercase;
font-weight: 600; font-weight: 600;
letter-spacing: -0.02em;
line-height: 1.1; line-height: 1.1;
color: var(--text-strong);
} }
.brand-sub { .brand-sub {
font-size: 11px; font-size: 10px;
font-weight: 500;
color: var(--muted); color: var(--muted);
letter-spacing: 0.02em; letter-spacing: 0.8px;
text-transform: uppercase;
line-height: 1; line-height: 1;
} }
/* Topbar status */
.topbar-status { .topbar-status {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
/* Smaller pill and theme toggle in topbar */
.topbar-status .pill { .topbar-status .pill {
padding: 6px 10px; padding: 4px 10px;
gap: 6px; gap: 6px;
font-size: 12px; font-size: 11px;
font-weight: 500;
height: 32px;
box-sizing: border-box;
}
.topbar-status .pill .mono {
display: flex;
align-items: center;
line-height: 1;
margin-top: 0px;
} }
.topbar-status .statusDot { .topbar-status .statusDot {
@@ -176,9 +135,9 @@
} }
.topbar-status .theme-toggle { .topbar-status .theme-toggle {
--theme-item: 24px; --theme-item: 22px;
--theme-gap: 2px; --theme-gap: 4px;
--theme-pad: 3px; --theme-pad: 4px;
} }
.topbar-status .theme-icon { .topbar-status .theme-icon {
@@ -186,22 +145,17 @@
height: 12px; height: 12px;
} }
/* ===========================================
Navigation Sidebar
=========================================== */
.nav { .nav {
grid-area: nav; grid-area: nav;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
padding: 16px 12px; padding: 16px;
border-right: 1px solid var(--border); border-right: 1px solid var(--border);
background: var(--bg); background: var(--panel);
transition: backdrop-filter: blur(18px);
width var(--shell-focus-duration) var(--shell-focus-ease), transition: width var(--shell-focus-duration) var(--shell-focus-ease),
padding var(--shell-focus-duration) var(--shell-focus-ease), padding var(--shell-focus-duration) var(--shell-focus-ease);
opacity var(--shell-focus-duration) var(--shell-focus-ease); min-height: 0; /* Allow grid item to shrink and enable scrolling */
min-height: 0;
} }
.shell--chat-focus .nav { .shell--chat-focus .nav {
@@ -210,9 +164,9 @@
border-width: 0; border-width: 0;
overflow: hidden; overflow: hidden;
pointer-events: none; pointer-events: none;
opacity: 0;
} }
/* Collapsed nav sidebar - completely hidden */
.nav--collapsed { .nav--collapsed {
width: 0; width: 0;
min-width: 0; min-width: 0;
@@ -223,7 +177,7 @@
pointer-events: none; pointer-events: none;
} }
/* Nav collapse toggle */ /* Nav collapse toggle button */
.nav-collapse-toggle { .nav-collapse-toggle {
width: 32px; width: 32px;
height: 32px; height: 32px;
@@ -232,88 +186,71 @@
justify-content: center; justify-content: center;
background: transparent; background: transparent;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: var(--radius-md); border-radius: 6px;
cursor: pointer; cursor: pointer;
transition: transition: background 150ms ease, border-color 150ms ease;
background var(--duration-fast) ease,
border-color var(--duration-fast) ease;
margin-bottom: 16px; margin-bottom: 16px;
} }
.nav-collapse-toggle:hover { .nav-collapse-toggle:hover {
background: var(--bg-hover); background: rgba(255, 255, 255, 0.08);
border-color: var(--border); border-color: var(--border);
} }
:root[data-theme="light"] .nav-collapse-toggle:hover {
background: rgba(0, 0, 0, 0.06);
}
.nav-collapse-toggle__icon { .nav-collapse-toggle__icon {
display: flex; font-size: 16px;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
color: var(--muted); color: var(--muted);
transition: color var(--duration-fast) ease;
} }
.nav-collapse-toggle__icon svg {
width: 18px;
height: 18px;
stroke: currentColor;
fill: none;
stroke-width: 1.5px;
stroke-linecap: round;
stroke-linejoin: round;
}
.nav-collapse-toggle:hover .nav-collapse-toggle__icon {
color: var(--text);
}
/* Nav groups */
.nav-group { .nav-group {
margin-bottom: 20px; margin-bottom: 18px;
display: grid; display: grid;
gap: 2px; gap: 6px;
padding-bottom: 12px;
border-bottom: 1px dashed rgba(255, 255, 255, 0.08);
} }
.nav-group:last-child { .nav-group:last-child {
margin-bottom: 0; margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
} }
.nav-group__items { .nav-group__items {
display: grid; display: grid;
gap: 1px; gap: 4px;
} }
.nav-group--collapsed .nav-group__items { .nav-group--collapsed .nav-group__items {
display: none; display: none;
} }
/* Nav label */
.nav-label { .nav-label {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 8px; gap: 8px;
width: 100%; width: 100%;
padding: 6px 10px; padding: 4px 0;
font-size: 11px; font-size: 11px;
font-weight: 500; font-weight: 500;
color: var(--muted); text-transform: uppercase;
letter-spacing: 1.4px;
color: var(--text);
opacity: 0.7;
margin-bottom: 4px; margin-bottom: 4px;
background: transparent; background: transparent;
border: none; border: none;
cursor: pointer; cursor: pointer;
text-align: left; text-align: left;
border-radius: var(--radius-sm);
transition:
color var(--duration-fast) ease,
background var(--duration-fast) ease;
} }
.nav-label:hover { .nav-label:hover {
color: var(--text); opacity: 1;
background: var(--bg-hover);
} }
.nav-label--static { .nav-label--static {
@@ -321,8 +258,7 @@
} }
.nav-label--static:hover { .nav-label--static:hover {
color: var(--muted); opacity: 0.7;
background: transparent;
} }
.nav-label__text { .nav-label__text {
@@ -330,152 +266,131 @@
} }
.nav-label__chevron { .nav-label__chevron {
font-size: 10px; font-size: 12px;
opacity: 0.5; opacity: 0.6;
transition: transform var(--duration-fast) ease;
} }
.nav-group--collapsed .nav-label__chevron {
transform: rotate(-90deg);
}
/* Nav items */
.nav-item { .nav-item {
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
gap: 10px; gap: 8px;
padding: 8px 10px; padding: 10px 12px 10px 14px;
border-radius: var(--radius-md); border-radius: 12px;
border: 1px solid transparent; border: 1px solid transparent;
background: transparent; background: transparent;
color: var(--muted); color: var(--muted);
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
transition: transition: border-color 160ms ease, background 160ms ease, color 160ms ease;
border-color var(--duration-fast) ease,
background var(--duration-fast) ease,
color var(--duration-fast) ease;
} }
.nav-item__icon { .nav-item__icon {
width: 16px; font-size: 16px;
height: 16px; width: 18px;
height: 18px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-shrink: 0; flex-shrink: 0;
opacity: 0.7;
transition: opacity var(--duration-fast) ease;
}
.nav-item__icon svg {
width: 16px;
height: 16px;
stroke: currentColor;
fill: none;
stroke-width: 1.5px;
stroke-linecap: round;
stroke-linejoin: round;
} }
.nav-item__text { .nav-item__text {
font-size: 13px; font-size: 13px;
font-weight: 500;
white-space: nowrap; white-space: nowrap;
} }
.nav-item:hover { .nav-item:hover {
color: var(--text); color: var(--text);
background: var(--bg-hover); border-color: rgba(255, 255, 255, 0.12);
text-decoration: none; background: rgba(255, 255, 255, 0.06);
} }
.nav-item:hover .nav-item__icon { .nav-item::before {
opacity: 1; content: "";
position: absolute;
left: 0;
top: 50%;
width: 4px;
height: 60%;
border-radius: 0 999px 999px 0;
transform: translateY(-50%);
background: transparent;
} }
.nav-item.active { .nav-item.active {
color: var(--text-strong); color: var(--text);
background: var(--accent-subtle); border-color: rgba(245, 159, 74, 0.45);
background: rgba(245, 159, 74, 0.12);
} }
.nav-item.active .nav-item__icon { .nav-item.active::before {
opacity: 1; background: var(--accent);
color: var(--accent); box-shadow: 0 0 12px rgba(245, 159, 74, 0.4);
} }
/* ===========================================
Content Area
=========================================== */
.content { .content {
grid-area: content; grid-area: content;
padding: 8px 8px 24px; padding: 8px 6px 20px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
min-height: 0; min-height: 0;
overflow-y: auto; overflow-y: auto; /* Enable vertical scrolling for pages with long content */
overflow-x: hidden; overflow-x: hidden;
} }
:root[data-theme="light"] .content { /* Chat handles its own scrolling (chat-thread); avoid double scrollbars. */
background: var(--bg-content);
}
.content--chat { .content--chat {
overflow: hidden; overflow: hidden;
padding-bottom: 0;
} }
/* Content header */ .shell--chat .content {
/* No-op: keep chat layout consistent with other tabs */
}
.content-header { .content-header {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
justify-content: space-between; justify-content: space-between;
gap: 16px; gap: 12px;
padding: 4px 8px; padding: 0 6px;
overflow: hidden; overflow: hidden;
transform-origin: top center; transform-origin: top center;
transition: transition: opacity var(--shell-focus-duration) var(--shell-focus-ease),
opacity var(--shell-focus-duration) var(--shell-focus-ease),
transform var(--shell-focus-duration) var(--shell-focus-ease), transform var(--shell-focus-duration) var(--shell-focus-ease),
max-height var(--shell-focus-duration) var(--shell-focus-ease), max-height var(--shell-focus-duration) var(--shell-focus-ease),
padding var(--shell-focus-duration) var(--shell-focus-ease); padding var(--shell-focus-duration) var(--shell-focus-ease);
max-height: 80px; max-height: 90px;
} }
.shell--chat-focus .content-header { .shell--chat-focus .content-header {
opacity: 0; opacity: 0;
transform: translateY(-8px); transform: translateY(-10px);
max-height: 0px; max-height: 0px;
padding: 0; padding: 0;
pointer-events: none; pointer-events: none;
} }
.page-title { .page-title {
font-size: 24px; font-family: var(--font-display);
font-weight: 600; font-size: 26px;
letter-spacing: -0.02em; letter-spacing: 0.6px;
line-height: 1.2;
color: var(--text-strong);
} }
.page-sub { .page-sub {
color: var(--muted); color: var(--muted);
font-size: 13px; font-size: 12px;
font-weight: 400; letter-spacing: 0.4px;
margin-top: 4px;
} }
.page-meta { .page-meta {
display: flex; display: flex;
gap: 8px; gap: 10px;
} }
/* Chat view header adjustments */ /* Chat view: header and controls side by side */
.content--chat .content-header { .content--chat .content-header {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
@@ -495,13 +410,9 @@
flex-shrink: 0; flex-shrink: 0;
} }
/* ===========================================
Grid Utilities
=========================================== */
.grid { .grid {
display: grid; display: grid;
gap: 16px; gap: 18px;
} }
.grid-cols-2 { .grid-cols-2 {
@@ -514,42 +425,39 @@
.stat-grid { .stat-grid {
display: grid; display: grid;
gap: 12px; gap: 14px;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
} }
.note-grid { .note-grid {
display: grid; display: grid;
gap: 12px; gap: 14px;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
} }
.row { .row {
display: flex; display: flex;
gap: 10px; gap: 12px;
align-items: center; align-items: center;
} }
.stack { .stack {
display: grid; display: grid;
gap: 12px; gap: 14px;
} }
.filters { .filters {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 10px;
align-items: center; align-items: center;
} }
/* ===========================================
Responsive - Tablet
=========================================== */
@media (max-width: 1100px) { @media (max-width: 1100px) {
.shell { .shell {
--shell-pad: 12px; --shell-pad: 12px;
--shell-gap: 12px; --shell-gap: 12px;
--shell-nav-col: 1fr;
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: auto auto 1fr; grid-template-rows: auto auto 1fr;
grid-template-areas: grid-template-areas:
@@ -562,18 +470,17 @@
position: static; position: static;
max-height: none; max-height: none;
display: flex; display: flex;
gap: 6px; gap: 16px;
overflow-x: auto; overflow-x: auto;
border-right: none; border-right: none;
border-bottom: 1px solid var(--border); padding: 12px;
padding: 10px 14px;
background: var(--bg);
} }
.nav-group { .nav-group {
grid-auto-flow: column; grid-auto-flow: column;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
margin-bottom: 0; border-bottom: none;
padding-bottom: 0;
} }
.grid-cols-2, .grid-cols-2,
@@ -583,11 +490,13 @@
.topbar { .topbar {
position: static; position: static;
padding: 12px 14px; flex-direction: column;
gap: 10px; align-items: flex-start;
gap: 12px;
} }
.topbar-status { .topbar-status {
width: 100%;
flex-wrap: wrap; flex-wrap: wrap;
} }
+61 -121
View File
@@ -1,15 +1,12 @@
/* =========================================== /* Tablet/Mobile nav fix (under 1100px) - single horizontal scroll row */
Mobile Layout
=========================================== */
/* Tablet: Horizontal nav */
@media (max-width: 1100px) { @media (max-width: 1100px) {
/* Flatten nav into single horizontal scroll */
.nav { .nav {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: nowrap; flex-wrap: nowrap;
gap: 4px; gap: 6px;
padding: 10px 14px; padding: 10px 12px;
overflow-x: auto; overflow-x: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
scrollbar-width: none; scrollbar-width: none;
@@ -19,18 +16,21 @@
display: none; display: none;
} }
/* Nav groups should flow inline, not stack */
.nav-group { .nav-group {
display: contents; display: contents; /* Flatten group wrapper - items flow directly into .nav */
} }
.nav-group__items { .nav-group__items {
display: contents; display: contents; /* Flatten items wrapper too */
} }
/* Hide group labels on tablet/mobile */
.nav-label { .nav-label {
display: none; display: none;
} }
/* Don't hide nav items even if group is "collapsed" */
.nav-group--collapsed .nav-group__items { .nav-group--collapsed .nav-group__items {
display: contents; display: contents;
} }
@@ -38,22 +38,27 @@
.nav-item { .nav-item {
padding: 8px 14px; padding: 8px 14px;
font-size: 13px; font-size: 13px;
border-radius: var(--radius-md); border-radius: 10px;
white-space: nowrap; white-space: nowrap;
flex-shrink: 0; flex-shrink: 0;
} }
.nav-item::before {
display: none;
}
} }
/* Mobile-specific styles */ /* Mobile-specific improvements */
@media (max-width: 600px) { @media (max-width: 600px) {
.shell { .shell {
--shell-pad: 8px; --shell-pad: 8px;
--shell-gap: 8px; --shell-gap: 8px;
} }
/* Topbar */ /* Compact topbar for mobile */
.topbar { .topbar {
padding: 10px 12px; padding: 10px 12px;
border-radius: 12px;
gap: 8px; gap: 8px;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
@@ -67,7 +72,8 @@
} }
.brand-title { .brand-title {
font-size: 14px; font-size: 15px;
letter-spacing: 0.3px;
} }
.brand-sub { .brand-sub {
@@ -94,10 +100,11 @@
display: none; display: none;
} }
/* Nav */ /* Horizontal scrollable nav for mobile */
.nav { .nav {
padding: 8px 10px; padding: 8px;
gap: 4px; border-radius: 12px;
gap: 8px;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
scrollbar-width: none; scrollbar-width: none;
} }
@@ -115,14 +122,18 @@
} }
.nav-item { .nav-item {
padding: 6px 10px; padding: 7px 10px;
font-size: 12px; font-size: 12px;
border-radius: var(--radius-md); border-radius: 8px;
white-space: nowrap; white-space: nowrap;
flex-shrink: 0; flex-shrink: 0;
} }
/* Content */ .nav-item::before {
display: none;
}
/* Hide page title on mobile - nav already shows where you are */
.content-header { .content-header {
display: none; display: none;
} }
@@ -132,17 +143,17 @@
gap: 12px; gap: 12px;
} }
/* Cards */ /* Smaller cards on mobile */
.card { .card {
padding: 12px; padding: 12px;
border-radius: var(--radius-md); border-radius: 12px;
} }
.card-title { .card-title {
font-size: 13px; font-size: 14px;
} }
/* Stats */ /* Stat grid adjustments */
.stat-grid { .stat-grid {
gap: 8px; gap: 8px;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
@@ -150,24 +161,24 @@
.stat { .stat {
padding: 10px; padding: 10px;
border-radius: var(--radius-md); border-radius: 10px;
} }
.stat-label { .stat-label {
font-size: 11px; font-size: 10px;
} }
.stat-value { .stat-value {
font-size: 18px; font-size: 16px;
} }
/* Notes */ /* Notes grid */
.note-grid { .note-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 8px; gap: 10px;
} }
/* Forms */ /* Form fields */
.form-grid { .form-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 10px; gap: 10px;
@@ -177,14 +188,14 @@
.field textarea, .field textarea,
.field select { .field select {
padding: 8px 10px; padding: 8px 10px;
border-radius: var(--radius-md); border-radius: 10px;
font-size: 14px; font-size: 14px;
} }
/* Buttons */ /* Buttons */
.btn { .btn {
padding: 8px 12px; padding: 8px 12px;
font-size: 12px; font-size: 13px;
} }
/* Pills */ /* Pills */
@@ -193,7 +204,7 @@
font-size: 12px; font-size: 12px;
} }
/* Chat */ /* Chat-specific mobile improvements */
.chat-header { .chat-header {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
@@ -216,16 +227,17 @@
.chat-thread { .chat-thread {
margin-top: 8px; margin-top: 8px;
padding: 12px 8px; padding: 10px 8px;
border-radius: 12px;
} }
.chat-msg { .chat-msg {
max-width: 90%; max-width: 92%;
} }
.chat-bubble { .chat-bubble {
padding: 8px 12px; padding: 8px 10px;
border-radius: var(--radius-md); border-radius: 12px;
} }
.chat-compose { .chat-compose {
@@ -235,14 +247,14 @@
.chat-compose__field textarea { .chat-compose__field textarea {
min-height: 60px; min-height: 60px;
padding: 8px 10px; padding: 8px 10px;
border-radius: var(--radius-md); border-radius: 12px;
font-size: 14px; font-size: 14px;
} }
/* Log stream */ /* Log stream mobile */
.log-stream { .log-stream {
border-radius: var(--radius-md); border-radius: 10px;
max-height: 380px; max-height: 400px;
} }
.log-row { .log-row {
@@ -267,14 +279,14 @@
font-size: 12px; font-size: 12px;
} }
/* Lists */ /* List items */
.list-item { .list-item {
padding: 10px; padding: 10px;
border-radius: var(--radius-md); border-radius: 10px;
} }
.list-title { .list-title {
font-size: 13px; font-size: 14px;
} }
.list-sub { .list-sub {
@@ -284,91 +296,19 @@
/* Code blocks */ /* Code blocks */
.code-block { .code-block {
padding: 8px; padding: 8px;
border-radius: var(--radius-md); border-radius: 10px;
font-size: 11px; font-size: 11px;
} }
/* Theme toggle */ /* Theme toggle smaller */
.theme-toggle { .theme-toggle {
--theme-item: 24px; --theme-item: 24px;
--theme-gap: 2px; --theme-gap: 4px;
--theme-pad: 3px; --theme-pad: 4px;
} }
.theme-icon { .theme-icon {
width: 12px; width: 14px;
height: 12px; height: 14px;
}
}
/* Small mobile */
@media (max-width: 400px) {
.shell {
--shell-pad: 4px;
}
.topbar {
padding: 8px 10px;
}
.brand-title {
font-size: 13px;
}
.nav {
padding: 6px 8px;
}
.nav-item {
padding: 6px 8px;
font-size: 11px;
}
.content {
padding: 4px 4px 12px;
gap: 10px;
}
.card {
padding: 10px;
}
.stat {
padding: 8px;
}
.stat-value {
font-size: 16px;
}
.chat-bubble {
padding: 8px 10px;
}
.chat-compose__field textarea {
min-height: 52px;
padding: 8px 10px;
font-size: 13px;
}
.btn {
padding: 6px 10px;
font-size: 11px;
}
.topbar-status .pill {
padding: 3px 6px;
font-size: 10px;
}
.theme-toggle {
--theme-item: 22px;
--theme-gap: 2px;
--theme-pad: 2px;
}
.theme-icon {
width: 11px;
height: 11px;
} }
} }
+1
View File
@@ -3,3 +3,4 @@ export type EventLogEntry = {
event: string; event: string;
payload?: unknown; payload?: unknown;
}; };
+2 -3
View File
@@ -3,7 +3,6 @@ import { repeat } from "lit/directives/repeat.js";
import type { AppViewState } from "./app-view-state"; import type { AppViewState } from "./app-view-state";
import { iconForTab, pathForTab, titleForTab, type Tab } from "./navigation"; import { iconForTab, pathForTab, titleForTab, type Tab } from "./navigation";
import { icons } from "./icons";
import { loadChatHistory } from "./controllers/chat"; import { loadChatHistory } from "./controllers/chat";
import { syncUrlWithSessionKey } from "./app-settings"; import { syncUrlWithSessionKey } from "./app-settings";
import type { SessionsListResult } from "./types"; import type { SessionsListResult } from "./types";
@@ -32,7 +31,7 @@ export function renderTab(state: AppViewState, tab: Tab) {
}} }}
title=${titleForTab(tab)} title=${titleForTab(tab)}
> >
<span class="nav-item__icon" aria-hidden="true">${icons[iconForTab(tab)]}</span> <span class="nav-item__icon" aria-hidden="true">${iconForTab(tab)}</span>
<span class="nav-item__text">${titleForTab(tab)}</span> <span class="nav-item__text">${titleForTab(tab)}</span>
</a> </a>
`; `;
@@ -109,7 +108,7 @@ export function renderChatControls(state: AppViewState) {
? "Disabled during onboarding" ? "Disabled during onboarding"
: "Toggle assistant thinking/working output"} : "Toggle assistant thinking/working output"}
> >
${icons.brain} 🧠
</button> </button>
<button <button
class="btn btn--sm btn--icon ${focusActive ? "active" : ""}" class="btn btn--sm btn--icon ${focusActive ? "active" : ""}"
+4 -10
View File
@@ -11,7 +11,6 @@ import {
titleForTab, titleForTab,
type Tab, type Tab,
} from "./navigation"; } from "./navigation";
import { icons } from "./icons";
import type { UiSettings } from "./storage"; import type { UiSettings } from "./storage";
import type { ThemeMode } from "./theme"; import type { ThemeMode } from "./theme";
import type { ThemeTransitionContext } from "./theme-transition"; import type { ThemeTransitionContext } from "./theme-transition";
@@ -125,16 +124,11 @@ export function renderApp(state: AppViewState) {
title="${state.settings.navCollapsed ? "Expand sidebar" : "Collapse sidebar"}" title="${state.settings.navCollapsed ? "Expand sidebar" : "Collapse sidebar"}"
aria-label="${state.settings.navCollapsed ? "Expand sidebar" : "Collapse sidebar"}" aria-label="${state.settings.navCollapsed ? "Expand sidebar" : "Collapse sidebar"}"
> >
<span class="nav-collapse-toggle__icon">${icons.menu}</span> <span class="nav-collapse-toggle__icon">☰</span>
</button> </button>
<div class="brand"> <div class="brand">
<div class="brand-logo"> <div class="brand-title">CLAWDBOT</div>
<img src="https://mintcdn.com/clawdhub/4rYvG-uuZrMK_URE/assets/pixel-lobster.svg?fit=max&auto=format&n=4rYvG-uuZrMK_URE&q=85&s=da2032e9eac3b5d9bfe7eb96ca6a8a26" alt="Clawdbot" /> <div class="brand-sub">Gateway Dashboard</div>
</div>
<div class="brand-text">
<div class="brand-title">CLAWDBOT</div>
<div class="brand-sub">Gateway Dashboard</div>
</div>
</div> </div>
</div> </div>
<div class="topbar-status"> <div class="topbar-status">
@@ -185,7 +179,7 @@ export function renderApp(state: AppViewState) {
rel="noreferrer" rel="noreferrer"
title="Docs (opens in new tab)" title="Docs (opens in new tab)"
> >
<span class="nav-item__icon" aria-hidden="true">${icons.book}</span> <span class="nav-item__icon" aria-hidden="true">📚</span>
<span class="nav-item__text">Docs</span> <span class="nav-item__text">Docs</span>
</a> </a>
</div> </div>
+10 -6
View File
@@ -1,11 +1,14 @@
import { html, type TemplateResult } from "lit"; import { html, type TemplateResult } from "lit";
import { icons } from "../icons"; import { renderEmojiIcon, setEmojiIcon } from "../icons";
const COPIED_FOR_MS = 1500; const COPIED_FOR_MS = 1500;
const ERROR_FOR_MS = 2000; const ERROR_FOR_MS = 2000;
const COPY_LABEL = "Copy as markdown"; const COPY_LABEL = "Copy as markdown";
const COPIED_LABEL = "Copied"; const COPIED_LABEL = "Copied";
const ERROR_LABEL = "Copy failed"; const ERROR_LABEL = "Copy failed";
const COPY_ICON = "📋";
const COPIED_ICON = "✓";
const ERROR_ICON = "!";
type CopyButtonOptions = { type CopyButtonOptions = {
text: () => string; text: () => string;
@@ -38,7 +41,7 @@ function createCopyButton(options: CopyButtonOptions): TemplateResult {
aria-label=${idleLabel} aria-label=${idleLabel}
@click=${async (e: Event) => { @click=${async (e: Event) => {
const btn = e.currentTarget as HTMLButtonElement | null; const btn = e.currentTarget as HTMLButtonElement | null;
const iconContainer = btn?.querySelector( const icon = btn?.querySelector(
".chat-copy-btn__icon", ".chat-copy-btn__icon",
) as HTMLElement | null; ) as HTMLElement | null;
@@ -58,29 +61,30 @@ function createCopyButton(options: CopyButtonOptions): TemplateResult {
if (!copied) { if (!copied) {
btn.dataset.error = "1"; btn.dataset.error = "1";
setButtonLabel(btn, ERROR_LABEL); setButtonLabel(btn, ERROR_LABEL);
setEmojiIcon(icon, ERROR_ICON);
window.setTimeout(() => { window.setTimeout(() => {
if (!btn.isConnected) return; if (!btn.isConnected) return;
delete btn.dataset.error; delete btn.dataset.error;
setButtonLabel(btn, idleLabel); setButtonLabel(btn, idleLabel);
setEmojiIcon(icon, COPY_ICON);
}, ERROR_FOR_MS); }, ERROR_FOR_MS);
return; return;
} }
btn.dataset.copied = "1"; btn.dataset.copied = "1";
setButtonLabel(btn, COPIED_LABEL); setButtonLabel(btn, COPIED_LABEL);
setEmojiIcon(icon, COPIED_ICON);
window.setTimeout(() => { window.setTimeout(() => {
if (!btn.isConnected) return; if (!btn.isConnected) return;
delete btn.dataset.copied; delete btn.dataset.copied;
setButtonLabel(btn, idleLabel); setButtonLabel(btn, idleLabel);
setEmojiIcon(icon, COPY_ICON);
}, COPIED_FOR_MS); }, COPIED_FOR_MS);
}} }}
> >
<span class="chat-copy-btn__icon" aria-hidden="true"> ${renderEmojiIcon(COPY_ICON, "chat-copy-btn__icon")}
<span class="chat-copy-btn__icon-copy">${icons.copy}</span>
<span class="chat-copy-btn__icon-check">${icons.check}</span>
</span>
</button> </button>
`; `;
} }
+3 -4
View File
@@ -1,7 +1,6 @@
import { html, nothing } from "lit"; import { html, nothing } from "lit";
import { formatToolDetail, resolveToolDisplay } from "../tool-display"; import { formatToolDetail, resolveToolDisplay } from "../tool-display";
import { icons } from "../icons";
import type { ToolCard } from "../types/chat-types"; import type { ToolCard } from "../types/chat-types";
import { TOOL_INLINE_THRESHOLD } from "./constants"; import { TOOL_INLINE_THRESHOLD } from "./constants";
import { import {
@@ -96,13 +95,13 @@ export function renderToolCardSidebar(
> >
<div class="chat-tool-card__header"> <div class="chat-tool-card__header">
<div class="chat-tool-card__title"> <div class="chat-tool-card__title">
<span class="chat-tool-card__icon">${icons[display.icon]}</span> <span class="chat-tool-card__icon">${display.emoji}</span>
<span>${display.label}</span> <span>${display.label}</span>
</div> </div>
${canClick ${canClick
? html`<span class="chat-tool-card__action">${hasText ? "View" : ""} ${icons.check}</span>` ? html`<span class="chat-tool-card__action">${hasText ? "View ›" : "›"}</span>`
: nothing} : nothing}
${isEmpty && !canClick ? html`<span class="chat-tool-card__status">${icons.check}</span>` : nothing} ${isEmpty && !canClick ? html`<span class="chat-tool-card__status">✓</span>` : nothing}
</div> </div>
${detail ${detail
? html`<div class="chat-tool-card__detail">${detail}</div>` ? html`<div class="chat-tool-card__detail">${detail}</div>`
@@ -74,3 +74,4 @@ export function removePathValue(
delete (current as Record<string, unknown>)[lastKey]; delete (current as Record<string, unknown>)[lastKey];
} }
} }
+1
View File
@@ -54,3 +54,4 @@ export async function callDebugMethod(state: DebugState) {
state.debugCallError = String(err); state.debugCallError = String(err);
} }
} }
+1
View File
@@ -33,3 +33,4 @@ export async function loadPresence(state: PresenceState) {
state.presenceLoading = false; state.presenceLoading = false;
} }
} }
+1
View File
@@ -39,3 +39,4 @@ describe("stripThinkingTags", () => {
expect(stripThinkingTags("Hello</final>")).toBe("Hello"); expect(stripThinkingTags("Hello</final>")).toBe("Hello");
}); });
}); });
+2 -54
View File
@@ -1,59 +1,7 @@
import { html, type TemplateResult } from "lit"; import { html, type TemplateResult } from "lit";
// Lucide-style SVG icons export function renderEmojiIcon(icon: string, className: string): TemplateResult {
// All icons use currentColor for stroke return html`<span class=${className} aria-hidden="true">${icon}</span>`;
export const icons = {
// Navigation icons
messageSquare: html`<svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`,
barChart: html`<svg viewBox="0 0 24 24"><line x1="12" x2="12" y1="20" y2="10"/><line x1="18" x2="18" y1="20" y2="4"/><line x1="6" x2="6" y1="20" y2="16"/></svg>`,
link: html`<svg viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`,
radio: html`<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49m11.31-2.82a10 10 0 0 1 0 14.14m-14.14 0a10 10 0 0 1 0-14.14"/></svg>`,
fileText: html`<svg viewBox="0 0 24 24"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><line x1="16" x2="8" y1="13" y2="13"/><line x1="16" x2="8" y1="17" y2="17"/><line x1="10" x2="8" y1="9" y2="9"/></svg>`,
zap: html`<svg viewBox="0 0 24 24"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>`,
monitor: html`<svg viewBox="0 0 24 24"><rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>`,
settings: html`<svg viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>`,
bug: html`<svg viewBox="0 0 24 24"><path d="m8 2 1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/></svg>`,
scrollText: html`<svg viewBox="0 0 24 24"><path d="M8 21h12a2 2 0 0 0 2-2v-2H10v2a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v3h4"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M15 8h-5"/><path d="M15 12h-5"/></svg>`,
folder: html`<svg viewBox="0 0 24 24"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>`,
// UI icons
menu: html`<svg viewBox="0 0 24 24"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>`,
x: html`<svg viewBox="0 0 24 24"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>`,
check: html`<svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></svg>`,
copy: html`<svg viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`,
search: html`<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>`,
brain: html`<svg viewBox="0 0 24 24"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M17.599 6.5a3 3 0 0 0 .399-1.375"/><path d="M6.003 5.125A3 3 0 0 0 6.401 6.5"/><path d="M3.477 10.896a4 4 0 0 1 .585-.396"/><path d="M19.938 10.5a4 4 0 0 1 .585.396"/><path d="M6 18a4 4 0 0 1-1.967-.516"/><path d="M19.967 17.484A4 4 0 0 1 18 18"/></svg>`,
book: html`<svg viewBox="0 0 24 24"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg>`,
loader: html`<svg viewBox="0 0 24 24"><path d="M12 2v4"/><path d="m16.2 7.8 2.9-2.9"/><path d="M18 12h4"/><path d="m16.2 16.2 2.9 2.9"/><path d="M12 18v4"/><path d="m4.9 19.1 2.9-2.9"/><path d="M2 12h4"/><path d="m4.9 4.9 2.9 2.9"/></svg>`,
// Tool icons
wrench: html`<svg viewBox="0 0 24 24"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>`,
fileCode: html`<svg viewBox="0 0 24 24"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><path d="m10 13-2 2 2 2"/><path d="m14 17 2-2-2-2"/></svg>`,
edit: html`<svg viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`,
penLine: html`<svg viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>`,
paperclip: html`<svg viewBox="0 0 24 24"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>`,
globe: html`<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>`,
image: html`<svg viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>`,
smartphone: html`<svg viewBox="0 0 24 24"><rect width="14" height="20" x="5" y="2" rx="2" ry="2"/><path d="M12 18h.01"/></svg>`,
plug: html`<svg viewBox="0 0 24 24"><path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z"/></svg>`,
circle: html`<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/></svg>`,
puzzle: html`<svg viewBox="0 0 24 24"><path d="M19.439 7.85c-.049.322.059.648.289.878l1.568 1.568c.47.47.706 1.087.706 1.704s-.235 1.233-.706 1.704l-1.611 1.611a.98.98 0 0 1-.837.276c-.47-.07-.802-.48-.968-.925a2.501 2.501 0 1 0-3.214 3.214c.446.166.855.497.925.968a.979.979 0 0 1-.276.837l-1.61 1.61a2.404 2.404 0 0 1-1.705.707 2.402 2.402 0 0 1-1.704-.706l-1.568-1.568a1.026 1.026 0 0 0-.877-.29c-.493.074-.84.504-1.02.968a2.5 2.5 0 1 1-3.237-3.237c.464-.18.894-.527.967-1.02a1.026 1.026 0 0 0-.289-.877l-1.568-1.568A2.402 2.402 0 0 1 1.998 12c0-.617.236-1.234.706-1.704L4.23 8.77c.24-.24.581-.353.917-.303.515.076.874.54 1.02 1.02a2.5 2.5 0 1 0 3.237-3.237c-.48-.146-.944-.505-1.02-1.02a.98.98 0 0 1 .303-.917l1.526-1.526A2.402 2.402 0 0 1 11.998 2c.617 0 1.234.236 1.704.706l1.568 1.568c.23.23.556.338.877.29.493-.074.84-.504 1.02-.968a2.5 2.5 0 1 1 3.236 3.236c-.464.18-.894.527-.967 1.02Z"/></svg>`,
} as const;
export type IconName = keyof typeof icons;
export function icon(name: IconName): TemplateResult {
return icons[name];
}
export function renderIcon(name: IconName, className = "nav-item__icon"): TemplateResult {
return html`<span class=${className} aria-hidden="true">${icons[name]}</span>`;
}
// Legacy function for compatibility
export function renderEmojiIcon(iconContent: string | TemplateResult, className: string): TemplateResult {
return html`<span class=${className} aria-hidden="true">${iconContent}</span>`;
} }
export function setEmojiIcon(target: HTMLElement | null, icon: string): void { export function setEmojiIcon(target: HTMLElement | null, icon: string): void {
+1
View File
@@ -30,3 +30,4 @@ describe("toSanitizedMarkdownHtml", () => {
expect(html).toContain("console.log(1)"); expect(html).toContain("console.log(1)");
}); });
}); });
+13 -15
View File
@@ -1,5 +1,3 @@
import type { IconName } from "./icons.js";
export const TAB_GROUPS = [ export const TAB_GROUPS = [
{ label: "Chat", tabs: ["chat"] }, { label: "Chat", tabs: ["chat"] },
{ {
@@ -100,32 +98,32 @@ export function inferBasePathFromPathname(pathname: string): string {
return `/${segments.join("/")}`; return `/${segments.join("/")}`;
} }
export function iconForTab(tab: Tab): IconName { export function iconForTab(tab: Tab): string {
switch (tab) { switch (tab) {
case "chat": case "chat":
return "messageSquare"; return "💬";
case "overview": case "overview":
return "barChart"; return "📊";
case "channels": case "channels":
return "link"; return "🔗";
case "instances": case "instances":
return "radio"; return "📡";
case "sessions": case "sessions":
return "fileText"; return "📄";
case "cron": case "cron":
return "loader"; return "⏰";
case "skills": case "skills":
return "zap"; return "⚡️";
case "nodes": case "nodes":
return "monitor"; return "🖥️";
case "config": case "config":
return "settings"; return "⚙️";
case "debug": case "debug":
return "bug"; return "🐞";
case "logs": case "logs":
return "scrollText"; return "🧾";
default: default:
return "folder"; return "📁";
} }
} }
+1
View File
@@ -55,3 +55,4 @@ export function formatCronPayload(job: CronJob) {
if (p.kind === "systemEvent") return `System: ${p.text}`; if (p.kind === "systemEvent") return `System: ${p.text}`;
return `Agent: ${p.message}`; return `Agent: ${p.message}`;
} }
+15 -15
View File
@@ -1,7 +1,7 @@
{ {
"version": 1, "version": 1,
"fallback": { "fallback": {
"icon": "puzzle", "emoji": "🧩",
"detailKeys": [ "detailKeys": [
"command", "command",
"path", "path",
@@ -26,37 +26,37 @@
}, },
"tools": { "tools": {
"bash": { "bash": {
"icon": "wrench", "emoji": "🛠️",
"title": "Bash", "title": "Bash",
"detailKeys": ["command"] "detailKeys": ["command"]
}, },
"process": { "process": {
"icon": "wrench", "emoji": "🧰",
"title": "Process", "title": "Process",
"detailKeys": ["sessionId"] "detailKeys": ["sessionId"]
}, },
"read": { "read": {
"icon": "fileText", "emoji": "📖",
"title": "Read", "title": "Read",
"detailKeys": ["path"] "detailKeys": ["path"]
}, },
"write": { "write": {
"icon": "edit", "emoji": "✍️",
"title": "Write", "title": "Write",
"detailKeys": ["path"] "detailKeys": ["path"]
}, },
"edit": { "edit": {
"icon": "penLine", "emoji": "📝",
"title": "Edit", "title": "Edit",
"detailKeys": ["path"] "detailKeys": ["path"]
}, },
"attach": { "attach": {
"icon": "paperclip", "emoji": "📎",
"title": "Attach", "title": "Attach",
"detailKeys": ["path", "url", "fileName"] "detailKeys": ["path", "url", "fileName"]
}, },
"browser": { "browser": {
"icon": "globe", "emoji": "🌐",
"title": "Browser", "title": "Browser",
"actions": { "actions": {
"status": { "label": "status" }, "status": { "label": "status" },
@@ -95,7 +95,7 @@
} }
}, },
"canvas": { "canvas": {
"icon": "image", "emoji": "🖼️",
"title": "Canvas", "title": "Canvas",
"actions": { "actions": {
"present": { "label": "present", "detailKeys": ["target", "node", "nodeId"] }, "present": { "label": "present", "detailKeys": ["target", "node", "nodeId"] },
@@ -108,7 +108,7 @@
} }
}, },
"nodes": { "nodes": {
"icon": "smartphone", "emoji": "📱",
"title": "Nodes", "title": "Nodes",
"actions": { "actions": {
"status": { "label": "status" }, "status": { "label": "status" },
@@ -127,7 +127,7 @@
} }
}, },
"cron": { "cron": {
"icon": "loader", "emoji": "⏰",
"title": "Cron", "title": "Cron",
"actions": { "actions": {
"status": { "label": "status" }, "status": { "label": "status" },
@@ -144,7 +144,7 @@
} }
}, },
"gateway": { "gateway": {
"icon": "plug", "emoji": "🔌",
"title": "Gateway", "title": "Gateway",
"actions": { "actions": {
"restart": { "label": "restart", "detailKeys": ["reason", "delayMs"] }, "restart": { "label": "restart", "detailKeys": ["reason", "delayMs"] },
@@ -161,7 +161,7 @@
} }
}, },
"whatsapp_login": { "whatsapp_login": {
"icon": "circle", "emoji": "🟢",
"title": "WhatsApp Login", "title": "WhatsApp Login",
"actions": { "actions": {
"start": { "label": "start" }, "start": { "label": "start" },
@@ -169,7 +169,7 @@
} }
}, },
"discord": { "discord": {
"icon": "messageSquare", "emoji": "💬",
"title": "Discord", "title": "Discord",
"actions": { "actions": {
"react": { "label": "react", "detailKeys": ["channelId", "messageId", "emoji"] }, "react": { "label": "react", "detailKeys": ["channelId", "messageId", "emoji"] },
@@ -204,7 +204,7 @@
} }
}, },
"slack": { "slack": {
"icon": "messageSquare", "emoji": "💬",
"title": "Slack", "title": "Slack",
"actions": { "actions": {
"react": { "label": "react", "detailKeys": ["channelId", "messageId", "emoji"] }, "react": { "label": "react", "detailKeys": ["channelId", "messageId", "emoji"] },
Loaded 100 of 111 files, more files were not shown because too many files have changed in this diff. Show more