Compare commits

..
Author SHA1 Message Date
Peter Steinberger 492c37a171 fix: harden message aborts + bluebubbles dm create (#1751) (thanks @tyler6204) 2026-01-25 10:02:41 +00:00
Tyler Yust 738e596c71 fix(bluebubbles): preserve friendly display names in formatTargetDisplay 2026-01-25 09:47:57 +00:00
Tyler Yust bea6b3eb5b fix: prevent cross-context decoration on direct message tool sends
Two fixes:

1. Cross-context decoration (e.g., '[from +19257864429]' prefix) was being
   added to ALL messages sent to a different target, even when the agent
   was just composing a new message via the message tool. This decoration
   should only be applied when forwarding/relaying messages between chats.

   Fix: Added skipCrossContextDecoration flag to ChannelThreadingToolContext.
   The message tool now sets this flag to true, so direct sends don't get
   decorated. The buildCrossContextDecoration function checks this flag
   and returns null when set.

2. Aborted requests were still completing because the abort signal wasn't
   being passed through the message tool execution chain.

   Fix: Added abortSignal propagation from message tool → runMessageAction →
   executeSendAction → sendMessage → deliverOutboundPayloads. Added abort
   checks at key points in the chain to fail fast when aborted.

Files changed:
- src/channels/plugins/types.core.ts: Added skipCrossContextDecoration field
- src/infra/outbound/outbound-policy.ts: Check skip flag before decorating
- src/agents/tools/message-tool.ts: Set skip flag, accept and pass abort signal
- src/infra/outbound/message-action-runner.ts: Pass abort signal through
- src/infra/outbound/outbound-send-service.ts: Check and pass abort signal
- src/infra/outbound/message.ts: Pass abort signal to delivery
2026-01-25 09:47:57 +00:00
Tyler Yust 00e00460c1 fix(bluebubbles): hide internal routing metadata in cross-context markers
When sending cross-context messages via BlueBubbles, the origin marker was
exposing internal chat_guid routing info like '[from bluebubbles:chat_guid:any;-;+19257864429]'.

This adds a formatTargetDisplay() function to the BlueBubbles plugin that:
- Extracts phone numbers from chat_guid formats (iMessage;-;+1234567890 -> +1234567890)
- Normalizes handles for clean display
- Avoids returning raw chat_guid formats containing internal routing metadata

Now cross-context markers show clean identifiers like '[from +19257864429]' instead
of exposing internal routing details to recipients.
2026-01-25 09:47:57 +00:00
Tyler Yust 64c6698ea4 feat(bluebubbles): auto-create new DM chats when sending to unknown phone numbers
When sending to a phone number that doesn't have an existing chat,
instead of failing with 'chatGuid not found', now automatically creates
a new chat using the /api/v1/chat/new endpoint.

- Added createNewChatWithMessage() helper function
- When resolveChatGuidForTarget returns null for a handle target,
  uses the new chat endpoint with addresses array and message
- Includes helpful error message if Private API isn't enabled
- Only applies to handle targets (phone numbers), not group chats
2026-01-25 09:47:57 +00:00
Tyler Yust f997a3f395 fix(bluebubbles): prevent message routing to group chats when targeting phone numbers
When sending a message to a phone number like +12622102921, the
resolveChatGuidForTarget function was finding and returning a GROUP
CHAT containing that phone number instead of a direct DM chat.

The bug was in the participantMatch fallback logic which matched ANY
chat containing the phone number as a participant, including groups.

This fix adds a check to ensure participantMatch only considers DM
chats (identified by ';-;' separator in the chat GUID). Group chats
(identified by ';+;' separator) are now explicitly excluded from
handle-based matching.

If a phone number only exists in a group chat (no direct DM exists),
the function now correctly returns null, which causes the send to
fail with a clear error rather than accidentally messaging a group.

Added test case to verify this behavior.
2026-01-25 09:47:57 +00:00
Tyler Yust 626a37f1ce fix(bluebubbles): prefer DM resolution + hide routing markers 2026-01-25 09:47:57 +00:00
100 changed files with 7404 additions and 7704 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 -1821
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`.
-4
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,10 +22,8 @@ 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: 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.
@@ -42,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>
-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.
-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
@@ -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 {
+4 -10
View File
@@ -10,17 +10,11 @@ describe("extractModelDirective", () => {
expect(result.cleaned).toBe(""); expect(result.cleaned).toBe("");
}); });
it("does not treat /models as a /model directive", () => { it("extracts /models with argument", () => {
const result = extractModelDirective("/models gpt-5"); const result = extractModelDirective("/models gpt-5");
expect(result.hasDirective).toBe(false); expect(result.hasDirective).toBe(true);
expect(result.rawModel).toBeUndefined(); expect(result.rawModel).toBe("gpt-5");
expect(result.cleaned).toBe("/models gpt-5"); expect(result.cleaned).toBe("");
});
it("does not parse /models as a /model directive (no args)", () => {
const result = extractModelDirective("/models");
expect(result.hasDirective).toBe(false);
expect(result.cleaned).toBe("/models");
}); });
it("extracts /model with provider/model format", () => { it("extracts /model with provider/model format", () => {
+1 -1
View File
@@ -14,7 +14,7 @@ export function extractModelDirective(
if (!body) return { cleaned: "", hasDirective: false }; if (!body) return { cleaned: "", hasDirective: false };
const modelMatch = body.match( const modelMatch = body.match(
/(?:^|\s)\/model(?=$|\s|:)\s*:?\s*([A-Za-z0-9_.:@-]+(?:\/[A-Za-z0-9_.:@-]+)*)?/i, /(?:^|\s)\/models?(?=$|\s|:)\s*:?\s*([A-Za-z0-9_.:@-]+(?:\/[A-Za-z0-9_.:@-]+)*)?/i,
); );
const aliases = (options?.aliases ?? []).map((alias) => alias.trim()).filter(Boolean); const aliases = (options?.aliases ?? []).map((alias) => alias.trim()).filter(Boolean);
-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;
-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 -333
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"] },
+8 -7
View File
@@ -1,5 +1,4 @@
import rawConfig from "./tool-display.json"; import rawConfig from "./tool-display.json";
import type { IconName } from "./icons";
type ToolDisplayActionSpec = { type ToolDisplayActionSpec = {
label?: string; label?: string;
@@ -7,7 +6,7 @@ type ToolDisplayActionSpec = {
}; };
type ToolDisplaySpec = { type ToolDisplaySpec = {
icon?: string; emoji?: string;
title?: string; title?: string;
label?: string; label?: string;
detailKeys?: string[]; detailKeys?: string[];
@@ -22,7 +21,7 @@ type ToolDisplayConfig = {
export type ToolDisplay = { export type ToolDisplay = {
name: string; name: string;
icon: IconName; emoji: string;
title: string; title: string;
label: string; label: string;
verb?: string; verb?: string;
@@ -30,7 +29,7 @@ export type ToolDisplay = {
}; };
const TOOL_DISPLAY_CONFIG = rawConfig as ToolDisplayConfig; const TOOL_DISPLAY_CONFIG = rawConfig as ToolDisplayConfig;
const FALLBACK = TOOL_DISPLAY_CONFIG.fallback ?? { icon: "puzzle" }; const FALLBACK = TOOL_DISPLAY_CONFIG.fallback ?? { emoji: "🧩" };
const TOOL_MAP = TOOL_DISPLAY_CONFIG.tools ?? {}; const TOOL_MAP = TOOL_DISPLAY_CONFIG.tools ?? {};
function normalizeToolName(name?: string): string { function normalizeToolName(name?: string): string {
@@ -136,7 +135,7 @@ export function resolveToolDisplay(params: {
const name = normalizeToolName(params.name); const name = normalizeToolName(params.name);
const key = name.toLowerCase(); const key = name.toLowerCase();
const spec = TOOL_MAP[key]; const spec = TOOL_MAP[key];
const icon = (spec?.icon ?? FALLBACK.icon ?? "puzzle") as IconName; const emoji = spec?.emoji ?? FALLBACK.emoji ?? "🧩";
const title = spec?.title ?? defaultTitle(name); const title = spec?.title ?? defaultTitle(name);
const label = spec?.label ?? name; const label = spec?.label ?? name;
const actionRaw = const actionRaw =
@@ -169,7 +168,7 @@ export function resolveToolDisplay(params: {
return { return {
name, name,
icon, emoji,
title, title,
label, label,
verb, verb,
@@ -187,7 +186,9 @@ export function formatToolDetail(display: ToolDisplay): string | undefined {
export function formatToolSummary(display: ToolDisplay): string { export function formatToolSummary(display: ToolDisplay): string {
const detail = formatToolDetail(display); const detail = formatToolDetail(display);
return detail ? `${display.label}: ${detail}` : display.label; return detail
? `${display.emoji} ${display.label}: ${detail}`
: `${display.emoji} ${display.label}`;
} }
function shortenHomeInString(input: string): string { function shortenHomeInString(input: string): string {
+1
View File
@@ -30,3 +30,4 @@ describe("generateUUID", () => {
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
}); });
}); });
+1
View File
@@ -40,3 +40,4 @@ export function generateUUID(cryptoLike: CryptoLike | null = globalThis.crypto):
return uuidFromBytes(weakRandomBytes()); return uuidFromBytes(weakRandomBytes());
} }
+1
View File
@@ -43,3 +43,4 @@ export function renderChannelAccountCount(
if (count < 2) return nothing; if (count < 2) return nothing;
return html`<div class="account-count">Accounts (${count})</div>`; return html`<div class="account-count">Accounts (${count})</div>`;
} }
+1
View File
@@ -116,3 +116,4 @@ export function renderWhatsAppCard(params: {
</div> </div>
`; `;
} }
+5 -6
View File
@@ -3,7 +3,6 @@ import { repeat } from "lit/directives/repeat.js";
import type { SessionsListResult } from "../types"; import type { SessionsListResult } from "../types";
import type { ChatQueueItem } from "../ui-types"; import type { ChatQueueItem } from "../ui-types";
import type { ChatItem, MessageGroup } from "../types/chat-types"; import type { ChatItem, MessageGroup } from "../types/chat-types";
import { icons } from "../icons";
import { import {
normalizeMessage, normalizeMessage,
normalizeRoleForGrouping, normalizeRoleForGrouping,
@@ -75,7 +74,7 @@ function renderCompactionIndicator(status: CompactionIndicatorStatus | null | un
if (status.active) { if (status.active) {
return html` return html`
<div class="callout info compaction-indicator compaction-indicator--active"> <div class="callout info compaction-indicator compaction-indicator--active">
${icons.loader} Compacting context... 🧹 Compacting context...
</div> </div>
`; `;
} }
@@ -86,7 +85,7 @@ function renderCompactionIndicator(status: CompactionIndicatorStatus | null | un
if (elapsed < COMPACTION_TOAST_DURATION_MS) { if (elapsed < COMPACTION_TOAST_DURATION_MS) {
return html` return html`
<div class="callout success compaction-indicator compaction-indicator--complete"> <div class="callout success compaction-indicator compaction-indicator--complete">
${icons.check} Context compacted 🧹 Context compacted
</div> </div>
`; `;
} }
@@ -172,7 +171,7 @@ export function renderChat(props: ChatProps) {
aria-label="Exit focus mode" aria-label="Exit focus mode"
title="Exit focus mode" title="Exit focus mode"
> >
${icons.x} ✕
</button> </button>
` `
: nothing} : nothing}
@@ -224,7 +223,7 @@ export function renderChat(props: ChatProps) {
aria-label="Remove queued message" aria-label="Remove queued message"
@click=${() => props.onQueueRemove(item.id)} @click=${() => props.onQueueRemove(item.id)}
> >
${icons.x} ✕
</button> </button>
</div> </div>
`, `,
@@ -266,7 +265,7 @@ export function renderChat(props: ChatProps) {
?disabled=${!props.connected} ?disabled=${!props.connected}
@click=${props.onSend} @click=${props.onSend}
> >
${isBusy ? "Queue" : "Send"}<kbd class="btn-kbd">↵</kbd> ${isBusy ? "Queue" : "Send"}
</button> </button>
</div> </div>
</div> </div>
+1 -2
View File
@@ -1,6 +1,5 @@
import { html, nothing } from "lit"; import { html, nothing } from "lit";
import type { ConfigUiHints } from "../types"; import type { ConfigUiHints } from "../types";
import { icons } from "../icons";
import { import {
hintForPath, hintForPath,
humanize, humanize,
@@ -190,7 +189,7 @@ export function renderConfigForm(props: ConfigFormProps) {
if (filteredEntries.length === 0) { if (filteredEntries.length === 0) {
return html` return html`
<div class="config-empty"> <div class="config-empty">
<div class="config-empty__icon">${icons.search}</div> <div class="config-empty__icon">🔍</div>
<div class="config-empty__text"> <div class="config-empty__text">
${searchQuery ${searchQuery
? `No settings match "${searchQuery}"` ? `No settings match "${searchQuery}"`
+1
View File
@@ -89,3 +89,4 @@ export function isSensitivePath(path: Array<string | number>): boolean {
key.endsWith("key") key.endsWith("key")
); );
} }
+1
View File
@@ -5,3 +5,4 @@ export {
} from "./config-form.analyze"; } from "./config-form.analyze";
export { renderNode } from "./config-form.node"; export { renderNode } from "./config-form.node";
export { schemaType, type JsonSchema } from "./config-form.shared"; export { schemaType, type JsonSchema } from "./config-form.shared";
+1 -2
View File
@@ -1,7 +1,6 @@
import { html, nothing } from "lit"; import { html, nothing } from "lit";
import { unsafeHTML } from "lit/directives/unsafe-html.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { icons } from "../icons";
import { toSanitizedMarkdownHtml } from "../markdown"; import { toSanitizedMarkdownHtml } from "../markdown";
export type MarkdownSidebarProps = { export type MarkdownSidebarProps = {
@@ -17,7 +16,7 @@ export function renderMarkdownSidebar(props: MarkdownSidebarProps) {
<div class="sidebar-header"> <div class="sidebar-header">
<div class="sidebar-title">Tool Output</div> <div class="sidebar-title">Tool Output</div>
<button @click=${props.onClose} class="btn" title="Close sidebar"> <button @click=${props.onClose} class="btn" title="Close sidebar">
${icons.x} ✕
</button> </button>
</div> </div>
<div class="sidebar-content"> <div class="sidebar-content">
-17
View File
@@ -1,17 +0,0 @@
# zizmor configuration
# https://docs.zizmor.sh/configuration/
rules:
# Disable unpinned-uses - pinning to SHA hashes is a significant change
# that should be done deliberately, not enforced by pre-commit
unpinned-uses:
disable: true
# Disable excessive-permissions for now - adding explicit permissions
# blocks requires careful review of each workflow's needs
excessive-permissions:
disable: true
# Disable artipacked (persist-credentials) - low confidence finding
artipacked:
disable: true