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
127 changed files with 7496 additions and 8009 deletions

No files matched your search

-4
View File
@@ -7,10 +7,6 @@
[exclude-files]
# pnpm lockfiles contain lots of high-entropy package integrity blobs.
pattern = (^|/)pnpm-lock\.yaml$
# Generated output and vendored assets.
pattern = (^|/)(dist|vendor)/
# Local config file with allowlist patterns.
pattern = (^|/)\.detect-secrets\.cfg$
[exclude-lines]
# Fastlane checks for private key marker; not a real key.
-17
View File
@@ -1,17 +0,0 @@
# actionlint configuration
# https://github.com/rhysd/actionlint/blob/main/docs/config.md
self-hosted-runner:
labels:
# Blacksmith CI runners
- blacksmith-4vcpu-ubuntu-2404
- blacksmith-4vcpu-windows-2025
# Ignore patterns for known issues
paths:
.github/workflows/**/*.yml:
ignore:
# Ignore shellcheck warnings (we run shellcheck separately)
- 'shellcheck reported issue.+'
# Ignore intentional if: false for disabled jobs
- 'constant expression "false" in condition'
-113
View File
@@ -1,113 +0,0 @@
# Dependabot configuration
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
registries:
npm-npmjs:
type: npm-registry
url: https://registry.npmjs.org
replaces-base: true
updates:
# npm dependencies (root)
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
production:
dependency-type: production
update-types:
- minor
- patch
development:
dependency-type: development
update-types:
- minor
- patch
open-pull-requests-limit: 10
registries:
- npm-npmjs
# GitHub Actions
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
actions:
patterns:
- "*"
update-types:
- minor
- patch
open-pull-requests-limit: 5
# Swift Package Manager - macOS app
- package-ecosystem: swift
directory: /apps/macos
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
swift-deps:
patterns:
- "*"
update-types:
- minor
- patch
open-pull-requests-limit: 5
# Swift Package Manager - shared ClawdbotKit
- package-ecosystem: swift
directory: /apps/shared/ClawdbotKit
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
swift-deps:
patterns:
- "*"
update-types:
- minor
- patch
open-pull-requests-limit: 5
# Swift Package Manager - Swabble
- package-ecosystem: swift
directory: /Swabble
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
swift-deps:
patterns:
- "*"
update-types:
- minor
- patch
open-pull-requests-limit: 5
# Gradle - Android app
- package-ecosystem: gradle
directory: /apps/android
schedule:
interval: weekly
cooldown:
default-days: 7
groups:
android-deps:
patterns:
- "*"
update-types:
- minor
- patch
open-pull-requests-limit: 5
-105
View File
@@ -1,105 +0,0 @@
# Pre-commit hooks for clawdbot
# Install: prek install
# Run manually: prek run --all-files
#
# See https://pre-commit.com for more information
repos:
# Basic file hygiene
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
exclude: '^(docs/|dist/|vendor/|.*\.snap$)'
- id: end-of-file-fixer
exclude: '^(docs/|dist/|vendor/|.*\.snap$)'
- id: check-yaml
args: [--allow-multiple-documents]
- id: check-added-large-files
args: [--maxkb=500]
- id: check-merge-conflict
# Secret detection (same as CI)
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args:
- --baseline
- .secrets.baseline
- --exclude-files
- '(^|/)(dist/|vendor/|pnpm-lock\.yaml$|\.detect-secrets\.cfg$)'
- --exclude-lines
- 'key_content\.include\?\("BEGIN PRIVATE KEY"\)'
- --exclude-lines
- 'case \.apiKeyEnv: "API key \(env var\)"'
- --exclude-lines
- 'case apikey = "apiKey"'
- --exclude-lines
- '"gateway\.remote\.password"'
- --exclude-lines
- '"gateway\.auth\.password"'
- --exclude-lines
- '"talk\.apiKey"'
- --exclude-lines
- '=== "string"'
- --exclude-lines
- 'typeof remote\?\.password === "string"'
# Shell script linting
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.11.0
hooks:
- id: shellcheck
args: [--severity=error] # Only fail on errors, not warnings/info
# Exclude vendor and scripts with embedded code or known issues
exclude: '^(vendor/|scripts/e2e/)'
# GitHub Actions linting
- repo: https://github.com/rhysd/actionlint
rev: v1.7.10
hooks:
- id: actionlint
# GitHub Actions security audit
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.22.0
hooks:
- id: zizmor
args: [--persona=regular, --min-severity=medium, --min-confidence=medium]
exclude: '^(vendor/|Swabble/)'
# Project checks (same commands as CI)
- repo: local
hooks:
# oxlint --type-aware src test
- id: oxlint
name: oxlint
entry: scripts/pre-commit/run-node-tool.sh oxlint --type-aware src test
language: system
pass_filenames: false
types_or: [javascript, jsx, ts, tsx]
# oxfmt --check src test
- id: oxfmt
name: oxfmt
entry: scripts/pre-commit/run-node-tool.sh oxfmt --check src test
language: system
pass_filenames: false
types_or: [javascript, jsx, ts, tsx]
# swiftlint (same as CI)
- id: swiftlint
name: swiftlint
entry: swiftlint --config .swiftlint.yml
language: system
pass_filenames: false
types: [swift]
# swiftformat --lint (same as CI)
- id: swiftformat
name: swiftformat
entry: swiftformat --lint apps/macos/Sources --config .swiftformat
language: system
pass_filenames: false
types: [swift]
+123 -1796
View File
File diff suppressed because it is too large. Load diff
-25
View File
@@ -1,25 +0,0 @@
# ShellCheck configuration
# https://www.shellcheck.net/wiki/
# Disable common false positives and style suggestions
# SC2034: Variable appears unused (often exported or used indirectly)
disable=SC2034
# SC2155: Declare and assign separately (common idiom, rarely causes issues)
disable=SC2155
# SC2295: Expansions inside ${..} need quoting (info-level, rarely causes issues)
disable=SC2295
# SC1012: \r is literal (tr -d '\r' works as intended on most systems)
disable=SC1012
# SC2026: Word outside quotes (info-level, often intentional)
disable=SC2026
# SC2016: Expressions don't expand in single quotes (often intentional in sed/awk)
disable=SC2016
# SC2129: Consider using { cmd1; cmd2; } >> file (style preference)
disable=SC2129
+1 -1
View File
@@ -23,7 +23,7 @@
# Whitespace
--trimwhitespace always
--emptybraces no-space
--nospaceoperators ...,..<
--nospaceoperators ...,..<
--ranges no-space
--someAny true
--voidtype void
-1
View File
@@ -37,7 +37,6 @@
## Build, Test, and Development Commands
- Runtime baseline: Node **22+** (keep Node + Bun paths working).
- 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).
- Prefer Bun for TypeScript execution (scripts, dev, tests): `bun <file.ts>` / `bunx <tool>`.
- Run CLI in dev: `pnpm clawdbot ...` (bun) or `pnpm dev`.
-5
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
- 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
- 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: add verbose installer troubleshooting guidance.
- Docs: add macOS VM guide with local/hosted options + VPS/nodes guidance. (#1693) Thanks @f-trycua.
@@ -23,11 +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: 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.
- Diagnostics: add diagnostic flags for targeted debug logs (config + env override). https://docs.clawd.bot/diagnostics/flags
### Fixes
- macOS: rearm gateway receive loop before push handling to avoid node invoke stalls. (#1752) Thanks @ngutman.
- Gateway: include inline config env vars in service install environments. (#1735) Thanks @Seredeep.
- BlueBubbles: route phone-number targets to DMs, avoid leaking routing IDs, and auto-create missing DMs (Private API required). (#1751) Thanks @tyler6204. https://docs.clawd.bot/channels/bluebubbles
- BlueBubbles: keep part-index GUIDs in reply tags when short IDs are missing.
- Web UI: hide internal `message_id` hints in chat bubbles.
@@ -43,7 +39,6 @@ Docs: https://docs.clawd.bot
- Agents: auto-compact on context overflow prompt errors before failing. (#1627) Thanks @rodrigouroz.
- Agents: use the active auth profile for auto-compaction recovery.
- 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: 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)
+2 -2
View File
@@ -459,7 +459,7 @@ Use these when you’re past the onboarding flow and want the deeper reference.
## Clawd
Clawdbot was built for **Clawd**, a space lobster AI assistant. 🦞
Clawdbot was built for **Clawd**, a space lobster AI assistant. 🦞
by Peter Steinberger and the community.
- [clawd.me](https://clawd.me)
@@ -468,7 +468,7 @@ by Peter Steinberger and the community.
## Community
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines, maintainers, and how to submit PRs.
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines, maintainers, and how to submit PRs.
AI/vibe-coded PRs welcome! 🤖
Special thanks to [Mario Zechner](https://mariozechner.at/) for his support and for
+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:
- `https://docs.clawd.bot/gateway/security`
+1 -1
View File
@@ -212,4 +212,4 @@
<enclosure url="https://github.com/clawdbot/clawdbot/releases/download/v2026.1.21/Clawdbot-2026.1.21.zip" length="22284796" type="application/octet-stream" sparkle:edSignature="pXji4NMA/cu35iMxln385d6LnsT4yIZtFtFiR7sIimKeSC2CsyeWzzSD0EhJsN98PdSoy69iEFZt4I2ZtNCECg=="/>
</item>
</channel>
</rss>
</rss>
@@ -12,3 +12,4 @@ data class CameraHudState(
val kind: CameraHudKind,
val message: String,
)
@@ -12,3 +12,4 @@ enum class VoiceWakeMode(val rawValue: String) {
}
}
}
@@ -135,7 +135,7 @@ class SmsManager(private val context: Context) {
/**
* Send an SMS message.
*
*
* @param paramsJson JSON with "to" (phone number) and "message" (text) fields
* @return SendResult indicating success or failure
*/
@@ -1,3 +1,4 @@
<resources>
<color name="ic_launcher_background">#0A0A0A</color>
</resources>
@@ -1,3 +1,4 @@
<resources>
<string name="app_name">Clawdbot Node</string>
</resources>
@@ -23,3 +23,4 @@ class VoiceWakeCommandExtractorTest {
assertNull(VoiceWakeCommandExtractor.extractCommand("hey claude!", listOf("claude")))
}
}
+1
View File
@@ -16,3 +16,4 @@ dependencyResolutionManagement {
rootProject.name = "ClawdbotNodeAndroid"
include(":app")
+1
View File
@@ -3,3 +3,4 @@ parent_config: ../../.swiftlint.yml
included:
- Sources
- ../shared/ClawdisNodeKit/Sources
+1 -1
View File
@@ -33,4 +33,4 @@
],
"squares" : "shared"
}
}
}
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
SOFTWARE.
@@ -173,4 +173,4 @@
"iPod5,1": "iPod touch (5th generation)",
"iPod7,1": "iPod touch (6th generation)",
"iPod9,1": "iPod touch (7th generation)"
}
}
@@ -211,4 +211,4 @@
"Mac Pro (2019)",
"Mac Pro (Rack, 2019)"
]
}
}
@@ -427,8 +427,8 @@ public actor GatewayChannelActor {
Task { await self.handleReceiveFailure(err) }
case let .success(msg):
Task {
await self.listen()
await self.handle(msg)
await self.listen()
}
}
}
@@ -574,22 +574,46 @@ public actor GatewayChannelActor {
params: [String: AnyCodable]?,
timeoutMs: Double? = nil) async throws -> Data
{
try await self.connectOrThrow(context: "gateway connect")
do {
try await self.connect()
} catch {
throw self.wrap(error, context: "gateway connect")
}
let id = UUID().uuidString
let effectiveTimeout = timeoutMs ?? self.defaultRequestTimeoutMs
let payload = try self.encodeRequest(method: method, params: params, kind: "request")
// Encode request using the generated models to avoid JSONSerialization/ObjC bridging pitfalls.
let paramsObject: ProtoAnyCodable? = params.map { entries in
let dict = entries.reduce(into: [String: ProtoAnyCodable]()) { dict, entry in
dict[entry.key] = ProtoAnyCodable(entry.value.value)
}
return ProtoAnyCodable(dict)
}
let frame = RequestFrame(
type: "req",
id: id,
method: method,
params: paramsObject)
let data: Data
do {
data = try self.encoder.encode(frame)
} catch {
self.logger.error(
"gateway request encode failed \(method, privacy: .public) error=\(error.localizedDescription, privacy: .public)")
throw error
}
let response = try await withCheckedThrowingContinuation { (cont: CheckedContinuation<GatewayFrame, Error>) in
self.pending[payload.id] = cont
self.pending[id] = cont
Task { [weak self] in
guard let self else { return }
try? await Task.sleep(nanoseconds: UInt64(effectiveTimeout * 1_000_000))
await self.timeoutRequest(id: payload.id, timeoutMs: effectiveTimeout)
await self.timeoutRequest(id: id, timeoutMs: effectiveTimeout)
}
Task {
do {
try await self.task?.send(.data(payload.data))
try await self.task?.send(.data(data))
} catch {
let wrapped = self.wrap(error, context: "gateway send \(method)")
let waiter = self.pending.removeValue(forKey: payload.id)
let waiter = self.pending.removeValue(forKey: id)
// Treat send failures as a broken socket: mark disconnected and trigger reconnect.
self.connected = false
self.task?.cancel(with: .goingAway, reason: nil)
@@ -633,42 +657,6 @@ public actor GatewayChannelActor {
return NSError(domain: ns.domain, code: ns.code, userInfo: [NSLocalizedDescriptionKey: "\(context): \(desc)"])
}
private func connectOrThrow(context: String) async throws {
do {
try await self.connect()
} catch {
throw self.wrap(error, context: context)
}
}
private func encodeRequest(
method: String,
params: [String: AnyCodable]?,
kind: String) throws -> (id: String, data: Data)
{
let id = UUID().uuidString
// Encode request using the generated models to avoid JSONSerialization/ObjC bridging pitfalls.
let paramsObject: ProtoAnyCodable? = params.map { entries in
let dict = entries.reduce(into: [String: ProtoAnyCodable]()) { dict, entry in
dict[entry.key] = ProtoAnyCodable(entry.value.value)
}
return ProtoAnyCodable(dict)
}
let frame = RequestFrame(
type: "req",
id: id,
method: method,
params: paramsObject)
do {
let data = try self.encoder.encode(frame)
return (id: id, data: data)
} catch {
self.logger.error(
"gateway \(kind) encode failed \(method, privacy: .public) error=\(error.localizedDescription, privacy: .public)")
throw error
}
}
private func failPending(_ error: Error) async {
let waiters = self.pending
self.pending.removeAll()
@@ -1,212 +0,0 @@
import Foundation
import Testing
@testable import ClawdbotKit
import ClawdbotProtocol
private final class FakeWebSocketTask: WebSocketTasking, @unchecked Sendable {
private let lock = NSLock()
private var queue: [URLSessionWebSocketTask.Message] = []
private var pendingHandler: (@Sendable (Result<URLSessionWebSocketTask.Message, Error>) -> Void)?
private var pendingContinuation: CheckedContinuation<URLSessionWebSocketTask.Message, Error>?
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
var state: URLSessionTask.State = .running
func resume() {}
func cancel(with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
state = .canceling
}
func send(_ message: URLSessionWebSocketTask.Message) async throws {
guard case let .data(data) = message else { return }
guard let frame = try? decoder.decode(RequestFrame.self, from: data) else { return }
switch frame.method {
case "connect":
enqueueResponse(id: frame.id, payload: helloOkPayload())
default:
enqueueResponse(id: frame.id, payload: ["ok": true])
}
}
func receive() async throws -> URLSessionWebSocketTask.Message {
try await withCheckedThrowingContinuation { cont in
lock.lock()
if !queue.isEmpty {
let msg = queue.removeFirst()
lock.unlock()
cont.resume(returning: msg)
return
}
pendingContinuation = cont
lock.unlock()
}
}
func receive(
completionHandler: @escaping @Sendable (Result<URLSessionWebSocketTask.Message, Error>) -> Void)
{
lock.lock()
if !queue.isEmpty {
let msg = queue.removeFirst()
lock.unlock()
completionHandler(.success(msg))
return
}
pendingHandler = completionHandler
lock.unlock()
}
func enqueue(_ message: URLSessionWebSocketTask.Message) {
lock.lock()
if let handler = pendingHandler {
pendingHandler = nil
lock.unlock()
handler(.success(message))
return
}
if let continuation = pendingContinuation {
pendingContinuation = nil
lock.unlock()
continuation.resume(returning: message)
return
}
queue.append(message)
lock.unlock()
}
private func enqueueResponse(id: String, payload: [String: Any]) {
let response = ResponseFrame(
type: "res",
id: id,
ok: true,
payload: ClawdbotProtocol.AnyCodable(payload),
error: nil)
guard let data = try? encoder.encode(response) else { return }
enqueue(.data(data))
}
private func helloOkPayload() -> [String: Any] {
[
"type": "hello.ok",
"protocol": 1,
"server": [:],
"features": [:],
"snapshot": [
"presence": [],
"health": [:],
"stateVersion": [
"presence": 0,
"health": 0,
],
"uptimeMs": 0,
],
"policy": [
"tickIntervalMs": 1000,
],
]
}
}
private final class FakeWebSocketSession: WebSocketSessioning {
let task: FakeWebSocketTask
init(task: FakeWebSocketTask) {
self.task = task
}
func makeWebSocketTask(url: URL) -> WebSocketTaskBox {
WebSocketTaskBox(task: task)
}
}
private actor AsyncSignal {
private var continuation: CheckedContinuation<Result<Void, Error>, Never>?
private var stored: Result<Void, Error>?
func finish(_ result: Result<Void, Error>) {
if let continuation {
self.continuation = nil
continuation.resume(returning: result)
return
}
stored = result
}
func wait() async throws {
let result = await withCheckedContinuation { cont in
if let stored {
self.stored = nil
cont.resume(returning: stored)
return
}
continuation = cont
}
switch result {
case .success:
return
case let .failure(error):
throw error
}
}
}
private enum TestError: Error {
case timeout
}
struct GatewayChannelTests {
@Test
func listenRearmsBeforePushHandler() async throws {
let task = FakeWebSocketTask()
let session = FakeWebSocketSession(task: task)
let signal = AsyncSignal()
let url = URL(string: "ws://example.invalid")!
final class ChannelBox { var channel: GatewayChannelActor? }
let box = ChannelBox()
let channel = GatewayChannelActor(
url: url,
token: nil,
session: WebSocketSessionBox(session: session),
pushHandler: { push in
guard case let .event(evt) = push, evt.event == "test.event" else { return }
guard let channel = box.channel else { return }
let params: [String: ClawdbotKit.AnyCodable] = [
"event": ClawdbotKit.AnyCodable("test"),
"payloadJSON": ClawdbotKit.AnyCodable(NSNull()),
]
do {
_ = try await channel.request(method: "node.event", params: params, timeoutMs: 50)
await signal.finish(.success(()))
} catch {
await signal.finish(.failure(error))
}
})
box.channel = channel
let challenge = EventFrame(
type: "event",
event: "connect.challenge",
payload: ClawdbotProtocol.AnyCodable(["nonce": "test-nonce"]),
seq: nil,
stateversion: nil)
let encoder = JSONEncoder()
task.enqueue(.data(try encoder.encode(challenge)))
try await channel.connect()
let event = EventFrame(
type: "event",
event: "test.event",
payload: ClawdbotProtocol.AnyCodable([:]),
seq: nil,
stateversion: nil)
task.enqueue(.data(try encoder.encode(event)))
try await AsyncTimeout.withTimeout(seconds: 1, onTimeout: { TestError.timeout }) {
try await signal.wait()
}
}
}
-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>
<meta name="color-scheme" content="dark light" />
<link rel="icon" href="./favicon.ico" sizes="any" />
<script type="module" crossorigin src="./assets/index-DQcOTEYz.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-08nzABV3.css">
<script type="module" crossorigin src="./assets/index-DsXRcnEw.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-BvhR9FCb.css">
</head>
<body>
<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.
-11
View File
@@ -138,7 +138,6 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
- [Can I use self-hosted models (llama.cpp, vLLM, Ollama)?](#can-i-use-selfhosted-models-llamacpp-vllm-ollama)
- [What do Clawd, Flawd, and Krill use for models?](#what-do-clawd-flawd-and-krill-use-for-models)
- [How do I switch models on the fly (without restarting)?](#how-do-i-switch-models-on-the-fly-without-restarting)
- [Can I use GPT 5.2 for daily tasks and Codex 5.2 for coding](#can-i-use-gpt-52-for-daily-tasks-and-codex-52-for-coding)
- [Why do I see “Model … is not allowed” and then no reply?](#why-do-i-see-model-is-not-allowed-and-then-no-reply)
- [Why do I see “Unknown model: minimax/MiniMax-M2.1”?](#why-do-i-see-unknown-model-minimaxminimaxm21)
- [Can I use MiniMax as my default and OpenAI for complex tasks?](#can-i-use-minimax-as-my-default-and-openai-for-complex-tasks)
@@ -1948,16 +1947,6 @@ Re-run `/model` **without** the `@profile` suffix:
If you want to return to the default, pick it from `/model` (or send `/model <default provider/model>`).
Use `/model status` to confirm which auth profile is active.
### Can I use GPT 5.2 for daily tasks and Codex 5.2 for coding
Yes. Set one as default and switch as needed:
- **Quick switch (per session):** `/model gpt-5.2` for daily tasks, `/model gpt-5.2-codex` for coding.
- **Default + switch:** set `agents.defaults.model.primary` to `openai-codex/gpt-5.2`, then switch to `openai-codex/gpt-5.2-codex` when coding (or the other way around).
- **Sub-agents:** route coding tasks to sub-agents with a different default model.
See [Models](/concepts/models) and [Slash commands](/tools/slash-commands).
### Why do I see Model is not allowed and then no reply
If `agents.defaults.models` is set, it becomes the **allowlist** for `/model` and any
-24
View File
@@ -192,30 +192,6 @@ Use this if you want diagnostics events available to plugins or custom sinks:
}
```
### Diagnostics flags (targeted logs)
Use flags to turn on extra, targeted debug logs without raising `logging.level`.
Flags are case-insensitive and support wildcards (e.g. `telegram.*` or `*`).
```json
{
"diagnostics": {
"flags": ["telegram.http"]
}
}
```
Env override (one-off):
```
CLAWDBOT_DIAGNOSTICS=telegram.http,telegram.payload
```
Notes:
- Flag logs go to the standard log file (same as `logging.file`).
- Output is still redacted according to `logging.redactSensitive`.
- Full guide: [/diagnostics/flags](/diagnostics/flags).
### Export to OpenTelemetry
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 |
| `--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 |
| `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`.
### 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
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
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:
```bash
fly ssh console --command "cat /data/clawdbot.json"
fly ssh console --command "cat /data/.clawdbot/clawdbot.json"
```
### Writing Config via SSH
@@ -288,24 +281,18 @@ The `fly ssh console -C` command doesn't support shell redirection. To write a c
```bash
# 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
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:
```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
```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
- For WhatsApp/Telegram onboarding, use `fly ssh console`
- Persistent data lives on the volume at `/data`
- Signal requires Java + signal-cli; use a custom image and keep memory at 2GB+.
## Cost
@@ -33,7 +33,7 @@ export function registerMatrixAutoJoin(params: {
// For "allowlist" mode, handle invites manually
client.on("room.invite", async (roomId: string, _inviteEvent: unknown) => {
if (autoJoin !== "allowlist") return;
// Get room alias if available
let alias: string | undefined;
let altAliases: string[] = [];
@@ -25,7 +25,7 @@ async function fetchMatrixMediaBuffer(params: {
// matrix-bot-sdk provides mxcToHttp helper
const url = params.client.mxcToHttp(params.mxcUrl);
if (!url) return null;
// Use the client's download method which handles auth
try {
const buffer = await params.client.downloadContent(params.mxcUrl);
@@ -61,7 +61,7 @@ async function fetchEncryptedMediaBuffer(params: {
Buffer.from(encryptedBuffer),
params.file,
);
return { buffer: decrypted };
}
@@ -77,7 +77,7 @@ export async function downloadMatrixMedia(params: {
placeholder: string;
} | null> {
let fetched: { buffer: Buffer; headerType?: string } | null;
if (params.file) {
// Encrypted media
fetched = await fetchEncryptedMediaBuffer({
@@ -93,7 +93,7 @@ export async function downloadMatrixMedia(params: {
maxBytes: params.maxBytes,
});
}
if (!fetched) return null;
const headerType = fetched.headerType ?? params.contentType ?? undefined;
const saved = await getMatrixRuntime().channel.media.saveMediaBuffer(
+1 -1
View File
@@ -11,4 +11,4 @@ export function resolveMattermostGroupRequireMention(
});
if (typeof account.requireMention === "boolean") return account.requireMention;
return true;
}
}
@@ -112,4 +112,4 @@ export function listEnabledMattermostAccounts(cfg: ClawdbotConfig): ResolvedMatt
return listMattermostAccountIds(cfg)
.map((accountId) => resolveMattermostAccount({ cfg, accountId }))
.filter((account) => account.enabled);
}
}
@@ -205,4 +205,4 @@ export async function uploadMattermostFile(
throw new Error("Mattermost file upload failed");
}
return info;
}
}
@@ -147,4 +147,4 @@ export function resolveThreadSessionKeys(params: {
? `${params.baseSessionKey}:thread:${threadId}`
: params.baseSessionKey;
return { sessionKey, parentSessionKey: params.parentSessionKey };
}
}
@@ -67,4 +67,4 @@ export async function probeMattermost(
} finally {
if (timer) clearTimeout(timer);
}
}
}
@@ -39,4 +39,4 @@ export async function promptAccountId(params: PromptAccountIdParams): Promise<st
);
}
return normalized;
}
}
+1 -1
View File
@@ -184,4 +184,4 @@ export const mattermostOnboardingAdapter: ChannelOnboardingAdapter = {
mattermost: { ...cfg.channels?.mattermost, enabled: false },
},
}),
};
};
@@ -22,11 +22,11 @@ parallel:
security = session: security_expert
prompt: "Perform a deep security audit of the changes. Look for OWASP top 10 issues."
context: overview
perf = session: performance_expert
prompt: "Analyze the performance implications. Identify potential bottlenecks or regressions."
context: overview
style = session: reviewer
prompt: "Review for code style, maintainability, and adherence to best practices."
context: overview
@@ -19,3 +19,4 @@ export type CallManagerContext = {
transcriptWaiters: Map<CallId, TranscriptWaiter>;
maxDurationTimers: Map<CallId, NodeJS.Timeout>;
};
@@ -175,3 +175,4 @@ export function processEvent(ctx: CallManagerContext, event: NormalizedEvent): v
persistCallRecord(ctx.storePath, call);
}
@@ -31,3 +31,4 @@ export function findCall(params: {
providerCallId: params.callIdOrProviderCallId,
});
}
@@ -48,3 +48,4 @@ export function addTranscriptEntry(
};
call.transcript.push(entry);
}
@@ -86,3 +86,4 @@ export async function getCallHistoryFromStore(
return calls;
}
@@ -84,3 +84,4 @@ export function waitForFinalTranscript(
ctx.transcriptWaiters.set(callId, { resolve, reject, timeout });
});
}
@@ -7,3 +7,4 @@ export function generateNotifyTwiml(message: string, voice: string): string {
<Hangup/>
</Response>`;
}
@@ -26,3 +26,4 @@ describe("PlivoProvider", () => {
expect(result.providerResponseBody).toContain('length="300"');
});
});
@@ -27,3 +27,4 @@ export function verifyTwilioProviderWebhook(params: {
reason: result.reason,
};
}
+1
View File
@@ -15,3 +15,4 @@ describe("zalouser outbound chunker", () => {
expect(chunks.every((c) => c.length <= limit)).toBe(true);
});
});
+10 -10
View File
@@ -124,7 +124,7 @@ EOF
# Function to list categories
list_categories() {
echo -e "${BLUE}Fetching VibeTunnel log categories from the last hour...${NC}\n"
# Get unique categories from recent logs
log show --predicate "subsystem == \"$SUBSYSTEM\"" --last 1h 2>/dev/null | \
grep -E "category: \"[^\"]+\"" | \
@@ -133,7 +133,7 @@ list_categories() {
while read -r cat; do
echo " • $cat"
done
echo -e "\n${YELLOW}Note: Only categories with recent activity are shown${NC}"
}
@@ -230,29 +230,29 @@ fi
if [[ "$STREAM_MODE" == true ]]; then
# Streaming mode
CMD="sudo log stream --predicate '$PREDICATE' --level $LOG_LEVEL --info"
echo -e "${GREEN}Streaming VibeTunnel logs continuously...${NC}"
echo -e "${YELLOW}Press Ctrl+C to stop${NC}\n"
else
# Show mode
CMD="sudo log show --predicate '$PREDICATE'"
# Add log level for show command
if [[ "$LOG_LEVEL" == "debug" ]]; then
CMD="$CMD --debug"
else
CMD="$CMD --info"
fi
# Add time range
CMD="$CMD --last $TIME_RANGE"
if [[ "$SHOW_TAIL" == true ]]; then
echo -e "${GREEN}Showing last $TAIL_LINES log lines from the past $TIME_RANGE${NC}"
else
echo -e "${GREEN}Showing all logs from the past $TIME_RANGE${NC}"
fi
# Show applied filters
if [[ "$ERRORS_ONLY" == true ]]; then
echo -e "${RED}Filter: Errors only${NC}"
@@ -277,14 +277,14 @@ if [[ -n "$OUTPUT_FILE" ]]; then
if sudo -n /usr/bin/log show --last 1s 2>&1 | grep -q "password"; then
handle_sudo_error
fi
echo -e "${BLUE}Exporting logs to: $OUTPUT_FILE${NC}\n"
if [[ "$SHOW_TAIL" == true ]] && [[ "$STREAM_MODE" == false ]]; then
eval "$CMD" 2>&1 | tail -n "$TAIL_LINES" > "$OUTPUT_FILE"
else
eval "$CMD" > "$OUTPUT_FILE" 2>&1
fi
# Check if file was created and has content
if [[ -s "$OUTPUT_FILE" ]]; then
LINE_COUNT=$(wc -l < "$OUTPUT_FILE" | tr -d ' ')
@@ -298,7 +298,7 @@ else
if sudo -n /usr/bin/log show --last 1s 2>&1 | grep -q "password"; then
handle_sudo_error
fi
if [[ "$SHOW_TAIL" == true ]] && [[ "$STREAM_MODE" == false ]]; then
# Apply tail for non-streaming mode
eval "$CMD" 2>&1 | tail -n "$TAIL_LINES"
+2 -2
View File
@@ -102,12 +102,12 @@ ws.send(
);
const connectRes = await onceFrame((o) => o?.type === \"res\" && o?.id === \"c1\");
if (!connectRes.ok) throw new Error(\"connect failed: \" + (connectRes.error?.message ?? \"unknown\"));
ws.send(JSON.stringify({ type: \"req\", id: \"h1\", method: \"health\" }));
const healthRes = await onceFrame((o) => o?.type === \"res\" && o?.id === \"h1\", 10000);
if (!healthRes.ok) throw new Error(\"health failed: \" + (healthRes.error?.message ?? \"unknown\"));
if (healthRes.payload?.ok !== true) throw new Error(\"unexpected health payload\");
ws.close();
console.log(\"ok\");
NODE"
-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;
lines: number;
};
+1 -1
View File
@@ -84,7 +84,7 @@ curl http://127.0.0.1:8000/places/{place_id}
"open_now": true
}
],
"next_page_token": "..."
"next_page_token": "..."
}
```
+1 -30
View File
@@ -8,7 +8,6 @@ export type ModelCatalogEntry = {
provider: string;
contextWindow?: number;
reasoning?: boolean;
input?: Array<"text" | "image">;
};
type DiscoveredModel = {
@@ -17,7 +16,6 @@ type DiscoveredModel = {
provider: string;
contextWindow?: number;
reasoning?: boolean;
input?: Array<"text" | "image">;
};
type PiSdkModule = typeof import("@mariozechner/pi-coding-agent");
@@ -82,10 +80,7 @@ export async function loadModelCatalog(params?: {
? entry.contextWindow
: undefined;
const reasoning = typeof entry?.reasoning === "boolean" ? entry.reasoning : undefined;
const input = Array.isArray(entry?.input)
? (entry.input as Array<"text" | "image">)
: undefined;
models.push({ id, name, provider, contextWindow, reasoning, input });
models.push({ id, name, provider, contextWindow, reasoning });
}
if (models.length === 0) {
@@ -110,27 +105,3 @@ export async function loadModelCatalog(params?: {
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"}}';
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, "");
}
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 {
const match = raw.match(HTTP_STATUS_PREFIX_RE);
if (!match) return false;
@@ -344,7 +321,7 @@ export function sanitizeUserFacingText(text: string): string {
return formatRawAssistantErrorForUi(trimmed);
}
return collapseConsecutiveDuplicateBlocks(stripped);
return stripped;
}
export function isRateLimitAssistantError(msg: AssistantMessage | undefined): boolean {
+4 -10
View File
@@ -10,17 +10,11 @@ describe("extractModelDirective", () => {
expect(result.cleaned).toBe("");
});
it("does not treat /models as a /model directive", () => {
it("extracts /models with argument", () => {
const result = extractModelDirective("/models gpt-5");
expect(result.hasDirective).toBe(false);
expect(result.rawModel).toBeUndefined();
expect(result.cleaned).toBe("/models gpt-5");
});
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");
expect(result.hasDirective).toBe(true);
expect(result.rawModel).toBe("gpt-5");
expect(result.cleaned).toBe("");
});
it("extracts /model with provider/model format", () => {
+1 -1
View File
@@ -14,7 +14,7 @@ export function extractModelDirective(
if (!body) return { cleaned: "", hasDirective: false };
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);
-1
View File
@@ -100,7 +100,6 @@ export async function runDaemonInstall(opts: DaemonInstallOptions) {
if (json) warnings.push(message);
else defaultRuntime.log(message);
},
config: cfg,
});
try {
-3
View File
@@ -11,7 +11,6 @@ import {
} from "./daemon-runtime.js";
import { guardCancel } from "./onboard-helpers.js";
import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js";
import { loadConfig } from "../config/config.js";
export async function maybeInstallDaemon(params: {
runtime: RuntimeEnv;
@@ -82,14 +81,12 @@ export async function maybeInstallDaemon(params: {
progress.setLabel("Preparing Gateway service…");
const cfg = loadConfig();
const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({
env: process.env,
port: params.port,
token: params.gatewayToken,
runtime: daemonRuntime,
warn: (message, title) => note(message, title),
config: cfg,
});
progress.setLabel("Installing Gateway service…");
-134
View File
@@ -95,140 +95,6 @@ describe("buildGatewayInstallPlan", () => {
expect(warn).toHaveBeenCalledWith("Node too old", "Gateway runtime");
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", () => {
+1 -12
View File
@@ -7,8 +7,6 @@ import {
} from "../daemon/runtime-paths.js";
import { buildServiceEnvironment } from "../daemon/service-env.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";
type WarnFn = (message: string, title?: string) => void;
@@ -33,8 +31,6 @@ export async function buildGatewayInstallPlan(params: {
devMode?: boolean;
nodePath?: string;
warn?: WarnFn;
/** Full config to extract env vars from (env vars + inline env keys). */
config?: ClawdbotConfig;
}): Promise<GatewayInstallPlan> {
const devMode = params.devMode ?? resolveGatewayDevMode();
const nodePath =
@@ -54,7 +50,7 @@ export async function buildGatewayInstallPlan(params: {
const warning = renderSystemNodeWarning(systemNode, programArguments[0]);
if (warning) params.warn?.(warning, "Gateway runtime");
}
const serviceEnvironment = buildServiceEnvironment({
const environment = buildServiceEnvironment({
env: params.env,
port: params.port,
token: params.token,
@@ -64,13 +60,6 @@ export async function buildGatewayInstallPlan(params: {
: 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 };
}
@@ -161,7 +161,6 @@ export async function maybeRepairGatewayDaemon(params: {
token: params.cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN,
runtime: daemonRuntime,
warn: (message, title) => note(message, title),
config: params.cfg,
});
try {
await service.install({
-2
View File
@@ -110,7 +110,6 @@ export async function maybeMigrateLegacyGatewayService(
token: cfg.gateway?.auth?.token ?? process.env.CLAWDBOT_GATEWAY_TOKEN,
runtime: daemonRuntime,
warn: (message, title) => note(message, title),
config: cfg,
});
try {
await service.install({
@@ -178,7 +177,6 @@ export async function maybeRepairGatewayServiceConfig(
runtime: needsNodeRuntime && systemNodePath ? "node" : runtimeChoice,
nodePath: systemNodePath ?? undefined,
warn: (message, title) => note(message, title),
config: cfg,
});
const expectedEntrypoint = findGatewayEntrypoint(programArguments);
const currentEntrypoint = findGatewayEntrypoint(command.programArguments);
@@ -38,7 +38,6 @@ export async function installGatewayDaemonNonInteractive(params: {
token: gatewayToken,
runtime: daemonRuntimeRaw,
warn: (message) => runtime.log(message),
config: params.nextConfig,
});
try {
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";
import { VERSION } from "../version.js";
import { MissingEnvVarError, resolveConfigEnvVars } from "./env-substitution.js";
import { collectConfigEnvVars } from "./env-vars.js";
import { ConfigIncludeError, resolveConfigIncludes } from "./includes.js";
import { findLegacyConfigIssues } from "./legacy.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 {
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)) {
if (env[key]?.trim()) continue;
env[key] = value;
-3
View File
@@ -107,7 +107,6 @@ const FIELD_LABELS: Record<string, string> = {
"update.channel": "Update Channel",
"update.checkOnStart": "Update Check on Start",
"diagnostics.enabled": "Diagnostics Enabled",
"diagnostics.flags": "Diagnostics Flags",
"diagnostics.otel.enabled": "OpenTelemetry Enabled",
"diagnostics.otel.endpoint": "OpenTelemetry Endpoint",
"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.allowProfiles":
"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":
"Log cache trace snapshots for embedded agent runs (default: false).",
"diagnostics.cacheTrace.filePath":
-2
View File
@@ -135,8 +135,6 @@ export type DiagnosticsCacheTraceConfig = {
export type DiagnosticsConfig = {
enabled?: boolean;
/** Optional ad-hoc diagnostics flags (e.g. "telegram.http"). */
flags?: string[];
otel?: DiagnosticsOtelConfig;
cacheTrace?: DiagnosticsCacheTraceConfig;
};
-1
View File
@@ -62,7 +62,6 @@ export const ClawdbotSchema = z
diagnostics: z
.object({
enabled: z.boolean().optional(),
flags: z.array(z.string()).optional(),
otel: z
.object({
enabled: z.boolean().optional(),
-9
View File
@@ -14,7 +14,6 @@ import {
writeConfigFile,
} from "../config/config.js";
import { isDiagnosticsEnabled } from "../infra/diagnostic-events.js";
import { logAcceptedEnvOption } from "../infra/env.js";
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
import { clearAgentRunContext, onAgentEvent } from "../infra/agent-events.js";
import { onHeartbeatEvent } from "../infra/heartbeat-events.js";
@@ -150,14 +149,6 @@ export async function startGatewayServer(
): Promise<GatewayServer> {
// Ensure all default port derivations (browser/canvas) see the actual runtime 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();
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";
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 {
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;
-41
View File
@@ -4,11 +4,6 @@ import os from "node:os";
import path from "node:path";
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 { applyTemplate } from "../auto-reply/templating.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({
cfg,
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,
ReactionTypeEmoji,
} 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 { logVerbose } from "../globals.js";
import { recordChannelActivity } from "../infra/channel-activity.js";
import { formatErrorMessage, formatUncaughtError } from "../infra/errors.js";
import { isDiagnosticFlagEnabled } from "../infra/diagnostic-flags.js";
import { formatErrorMessage } from "../infra/errors.js";
import type { RetryConfig } from "../infra/retry.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 { isGifMedia } from "../media/mime.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 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 }) {
if (explicit?.trim()) return explicit.trim();
@@ -194,12 +178,7 @@ export async function sendMessageTelegram(
configRetry: account.config.retry,
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) => {
if (!/400: Bad Request: chat not found/i.test(formatErrorMessage(err))) return err;
return new Error(
@@ -238,31 +217,30 @@ export async function sendMessageTelegram(
parse_mode: "HTML" as const,
...baseParams,
};
const res = await requestWithDiag(
() => api.sendMessage(chatId, htmlText, sendParams),
"message",
).catch(async (err) => {
// Telegram rejects malformed HTML (e.g., unsupported tags or entities).
// When that happens, fall back to plain text so the message still delivers.
const errText = formatErrorMessage(err);
if (PARSE_ERR_RE.test(errText)) {
if (opts.verbose) {
console.warn(`telegram HTML parse failed, retrying as plain text: ${errText}`);
const res = await request(() => api.sendMessage(chatId, htmlText, sendParams), "message").catch(
async (err) => {
// Telegram rejects malformed HTML (e.g., unsupported tags or entities).
// When that happens, fall back to plain text so the message still delivers.
const errText = formatErrorMessage(err);
if (PARSE_ERR_RE.test(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;
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);
});
throw wrapChatNotFound(err);
},
);
return res;
};
@@ -299,20 +277,19 @@ export async function sendMessageTelegram(
| Awaited<ReturnType<typeof api.sendAnimation>>
| Awaited<ReturnType<typeof api.sendDocument>>;
if (isGif) {
result = await requestWithDiag(
() => api.sendAnimation(chatId, file, mediaParams),
"animation",
).catch((err) => {
throw wrapChatNotFound(err);
});
result = await request(() => api.sendAnimation(chatId, file, mediaParams), "animation").catch(
(err) => {
throw wrapChatNotFound(err);
},
);
} 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) => {
throw wrapChatNotFound(err);
},
);
} 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) => {
throw wrapChatNotFound(err);
},
@@ -325,27 +302,24 @@ export async function sendMessageTelegram(
logFallback: logVerbose,
});
if (useVoice) {
result = await requestWithDiag(
() => api.sendVoice(chatId, file, mediaParams),
"voice",
).catch((err) => {
throw wrapChatNotFound(err);
});
result = await request(() => api.sendVoice(chatId, file, mediaParams), "voice").catch(
(err) => {
throw wrapChatNotFound(err);
},
);
} else {
result = await requestWithDiag(
() => api.sendAudio(chatId, file, mediaParams),
"audio",
).catch((err) => {
throw wrapChatNotFound(err);
});
result = await request(() => api.sendAudio(chatId, file, mediaParams), "audio").catch(
(err) => {
throw wrapChatNotFound(err);
},
);
}
} else {
result = await requestWithDiag(
() => api.sendDocument(chatId, file, mediaParams),
"document",
).catch((err) => {
throw wrapChatNotFound(err);
});
result = await request(() => api.sendDocument(chatId, file, mediaParams), "document").catch(
(err) => {
throw wrapChatNotFound(err);
},
);
}
const mediaMessageId = String(result?.message_id ?? "unknown");
const resolvedChatId = String(result?.chat?.id ?? chatId);
@@ -426,12 +400,6 @@ export async function reactMessageTelegram(
configRetry: account.config.retry,
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 trimmedEmoji = emoji.trim();
// 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") {
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 };
}
@@ -478,13 +446,7 @@ export async function deleteMessageTelegram(
configRetry: account.config.retry,
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;
});
await requestWithDiag(() => api.deleteMessage(chatId, messageId), "deleteMessage");
await request(() => api.deleteMessage(chatId, messageId), "deleteMessage");
logVerbose(`[telegram] Deleted message ${messageId} from chat ${chatId}`);
return { ok: true };
}
-1
View File
@@ -169,7 +169,6 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption
token: settings.gatewayToken,
runtime: daemonRuntime,
warn: (message, title) => prompter.note(message, title),
config: nextConfig,
});
progress.update("Installing Gateway service…");
+1
View File
@@ -1,2 +1,3 @@
import "./styles.css";
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 {
/* Background - Deep Navy Slate */
--bg: #0c0d12;
--bg-accent: #0d0e14;
--bg-elevated: #181a21;
--bg-hover: #252830;
--bg-muted: #252830;
/* Card / Surface */
--card: #13151c;
--card-foreground: #f8fafc;
--card-highlight: rgba(255, 255, 255, 0.04);
--popover: #13151c;
--popover-foreground: #f8fafc;
/* Panel */
--panel: #0c0d12;
--panel-strong: #181a21;
--panel-hover: #252830;
--chrome: rgba(12, 13, 18, 0.95);
--chrome-strong: rgba(12, 13, 18, 0.98);
/* Text */
--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 */
--bg: #0a0f14;
--bg-accent: #111826;
--bg-grad-1: #162031;
--bg-grad-2: #1f2a22;
--bg-overlay: rgba(255, 255, 255, 0.05);
--bg-glow: rgba(245, 159, 74, 0.12);
--panel: rgba(14, 20, 30, 0.88);
--panel-strong: rgba(18, 26, 38, 0.96);
--chrome: rgba(9, 14, 20, 0.72);
--chrome-strong: rgba(9, 14, 20, 0.86);
--text: rgba(244, 246, 251, 0.96);
--chat-text: rgba(231, 237, 244, 0.92);
--muted: rgba(156, 169, 189, 0.72);
--border: rgba(255, 255, 255, 0.09);
--border-strong: rgba(255, 255, 255, 0.16);
--accent: #f59f4a;
--accent-2: #34c7b7;
--ok: #2bd97f;
--warn: #f2c94c;
--danger: #ff6b6b;
--focus: rgba(245, 159, 74, 0.35);
--grid-line: rgba(255, 255, 255, 0.04);
--theme-switch-x: 50%;
--theme-switch-y: 50%;
/* Typography */
--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;
/* 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;
--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;
}
/* Light theme */
: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, 0.04);
--popover: #ffffff;
--popover-foreground: #1c1917;
--panel: #f8f8f7;
--panel-strong: #f0efed;
--panel-hover: #e5e3e1;
--chrome: rgba(248, 248, 247, 0.95);
--chrome-strong: rgba(248, 248, 247, 0.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, 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);
--bg: #f5f1ea;
--bg-accent: #ffffff;
--bg-grad-1: #f1e6d6;
--bg-grad-2: #e5eef4;
--bg-overlay: rgba(28, 32, 46, 0.05);
--bg-glow: rgba(52, 199, 183, 0.14);
--panel: rgba(255, 255, 255, 0.9);
--panel-strong: rgba(255, 255, 255, 0.97);
--chrome: rgba(255, 255, 255, 0.75);
--chrome-strong: rgba(255, 255, 255, 0.88);
--text: rgba(27, 36, 50, 0.98);
--chat-text: rgba(36, 48, 66, 0.9);
--muted: rgba(80, 94, 114, 0.7);
--border: rgba(18, 24, 40, 0.12);
--border-strong: rgba(18, 24, 40, 0.2);
--accent: #e28a3f;
--accent-2: #1ba99d;
--ok: #1aa86c;
--warn: #b3771c;
--danger: #d44848;
--focus: rgba(226, 138, 63, 0.35);
--grid-line: rgba(18, 24, 40, 0.06);
color-scheme: light;
}
@@ -181,21 +69,44 @@ body {
body {
margin: 0;
font: 400 14px/1.5 var(--font-body);
letter-spacing: -0.011em;
background: var(--bg);
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);
-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 {
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% {
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) {
mix-blend-mode: normal;
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) {
@@ -230,12 +141,7 @@ clawdbot-app {
}
a {
color: var(--accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
color: inherit;
}
button,
@@ -246,35 +152,10 @@ select {
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 {
from {
opacity: 0;
transform: translateY(4px);
transform: translateY(6px);
}
to {
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 {
from {
opacity: 0;
transform: translateY(8px);
transform: translateY(12px);
}
to {
opacity: 1;
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;
align-items: flex-start;
margin-bottom: 16px;
margin-left: 4px;
margin-left: 16px;
margin-right: 16px;
}
@@ -70,23 +70,23 @@
}
.chat-avatar.user {
background: var(--accent-subtle);
color: var(--accent);
background: rgba(245, 159, 74, 0.2);
color: rgba(245, 159, 74, 1);
}
.chat-avatar.assistant {
background: var(--secondary);
color: var(--muted);
background: rgba(52, 199, 183, 0.2);
color: rgba(52, 199, 183, 1);
}
.chat-avatar.other {
background: var(--secondary);
color: var(--muted);
background: rgba(150, 150, 150, 0.2);
color: rgba(150, 150, 150, 1);
}
.chat-avatar.tool {
background: var(--secondary);
color: var(--muted);
background: rgba(134, 142, 150, 0.2);
color: rgba(134, 142, 150, 1);
}
/* Image avatar support */
@@ -100,9 +100,9 @@ img.chat-avatar {
.chat-bubble {
position: relative;
display: inline-block;
border: 1px solid transparent;
background: var(--card);
border-radius: var(--radius-lg);
border: 1px solid var(--border);
background: rgba(0, 0, 0, 0.12);
border-radius: 12px;
padding: 10px 14px;
box-shadow: none;
transition: background 150ms ease-out, border-color 150ms ease-out;
@@ -119,9 +119,9 @@ img.chat-avatar {
top: 6px;
right: 8px;
border: 1px solid var(--border);
background: var(--bg);
background: rgba(0, 0, 0, 0.22);
color: var(--muted);
border-radius: var(--radius-md);
border-radius: 8px;
padding: 4px 6px;
font-size: 14px;
line-height: 1;
@@ -132,40 +132,9 @@ img.chat-avatar {
}
.chat-copy-btn__icon {
display: inline-flex;
width: 14px;
height: 14px;
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;
display: inline-block;
width: 1em;
text-align: center;
}
.chat-bubble:hover .chat-copy-btn {
@@ -174,7 +143,7 @@ img.chat-avatar {
}
.chat-copy-btn:hover {
background: var(--bg-hover);
background: rgba(0, 0, 0, 0.3);
}
.chat-copy-btn[data-copying="1"] {
@@ -185,17 +154,17 @@ img.chat-avatar {
.chat-copy-btn[data-error="1"] {
opacity: 1;
pointer-events: auto;
border-color: var(--danger-subtle);
background: var(--danger-subtle);
color: var(--danger);
border-color: rgba(255, 69, 58, 0.8);
background: rgba(255, 69, 58, 0.18);
color: rgba(255, 69, 58, 1);
}
.chat-copy-btn[data-copied="1"] {
opacity: 1;
pointer-events: auto;
border-color: var(--ok-subtle);
background: var(--ok-subtle);
color: var(--ok);
border-color: rgba(52, 199, 183, 0.8);
background: rgba(52, 199, 183, 0.18);
color: rgba(52, 199, 183, 1);
}
.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 {
background: var(--bg-hover);
background: rgba(0, 0, 0, 0.18);
}
/* User bubbles have different styling */
.chat-group.user .chat-bubble {
background: var(--accent-subtle);
border-color: transparent;
}
:root[data-theme="light"] .chat-group.user .chat-bubble {
border-color: rgba(234, 88, 12, 0.2);
background: rgba(251, 146, 60, 0.12);
background: rgba(245, 159, 74, 0.15);
border-color: rgba(245, 159, 74, 0.3);
}
.chat-group.user .chat-bubble:hover {
background: rgba(255, 77, 77, 0.15);
background: rgba(245, 159, 74, 0.22);
}
/* Streaming animation */
+16 -44
View File
@@ -52,8 +52,8 @@
flex: 1 1 0; /* Grow, shrink, and use 0 base for proper scrolling */
overflow-y: auto;
overflow-x: hidden;
padding: 12px 4px;
margin: 0 -4px;
padding: 12px;
margin: 0 -12px;
min-height: 0; /* Allow shrinking for flex scroll behavior */
border-radius: 12px;
background: transparent;
@@ -87,39 +87,23 @@
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 {
position: sticky;
bottom: 0;
flex-shrink: 0;
display: flex;
align-items: stretch;
align-items: flex-end;
gap: 12px;
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%);
z-index: 10;
}
:root[data-theme="light"] .chat-compose {
background: linear-gradient(to bottom, transparent, var(--bg-content) 20%);
}
.chat-compose__field {
flex: 1 1 auto;
min-width: 0;
display: flex;
align-items: stretch;
}
/* Hide the "Message" label - keep textarea only */
@@ -130,11 +114,10 @@
/* Override .field textarea min-height (180px) from components.css */
.chat-compose .chat-compose__field textarea {
width: 100%;
height: 40px;
min-height: 40px;
min-height: 36px;
max-height: 150px;
padding: 9px 12px;
border-radius: 8px;
padding: 8px 12px;
border-radius: 10px;
resize: vertical;
white-space: pre-wrap;
font-family: var(--font-body);
@@ -151,18 +134,13 @@
flex-shrink: 0;
display: flex;
align-items: stretch;
gap: 8px;
}
.chat-compose .chat-compose__actions .btn {
padding: 0 16px;
padding: 8px 16px;
font-size: 13px;
height: 40px;
min-height: 40px;
max-height: 40px;
line-height: 1;
min-height: 36px;
white-space: nowrap;
box-sizing: border-box;
}
/* Chat controls - moved to content-header area, left aligned */
@@ -216,27 +194,20 @@
/* Light theme icon button overrides */
:root[data-theme="light"] .btn--icon {
background: #ffffff;
border-color: var(--border);
background: rgba(255, 255, 255, 0.9);
border-color: rgba(16, 24, 40, 0.2);
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 {
background: #ffffff;
border-color: var(--border-strong);
color: var(--text);
background: rgba(255, 255, 255, 1);
border-color: rgba(16, 24, 40, 0.3);
color: rgba(16, 24, 40, 0.9);
}
.btn--icon svg {
display: block;
width: 18px;
height: 18px;
stroke: currentColor;
fill: none;
stroke-width: 1.5px;
stroke-linecap: round;
stroke-linejoin: round;
}
.chat-controls__session select {
@@ -279,3 +250,4 @@
min-width: 120px;
}
}
+4 -40
View File
@@ -14,8 +14,8 @@
}
:root[data-theme="light"] .chat-thinking {
border-color: rgba(16, 24, 40, 0.25);
background: rgba(16, 24, 40, 0.04);
border-color: rgba(16, 24, 40, 0.18);
background: rgba(16, 24, 40, 0.03);
}
.chat-text {
@@ -75,46 +75,9 @@
}
.chat-text :where(blockquote) {
border-left: 3px solid var(--border-strong);
border-left: 3px solid var(--border);
padding-left: 12px;
margin-left: 0;
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) {
@@ -122,3 +85,4 @@
border-top: 1px solid var(--border);
margin: 1em 0;
}
+17 -48
View File
@@ -4,8 +4,6 @@
border-radius: 8px;
padding: 12px;
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;
/* Fixed max-height to ensure cards don't expand too much */
max-height: 120px;
@@ -13,8 +11,8 @@
}
.chat-tool-card:hover {
border-color: var(--border-strong);
background: var(--bg-hover);
border-color: var(--accent);
background: rgba(0, 0, 0, 0.06);
}
/* First tool card in a group - no top margin */
@@ -52,63 +50,33 @@
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
width: 18px;
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;
}
.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 */
.chat-tool-card__action {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--accent);
opacity: 0.8;
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 {
opacity: 1;
}
/* Status indicator for completed/empty results */
.chat-tool-card__status {
display: inline-flex;
align-items: center;
font-size: 14px;
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 {
font-size: 11px;
margin-top: 4px;
@@ -126,18 +94,18 @@
color: var(--muted);
margin-top: 8px;
padding: 8px 10px;
background: var(--secondary);
border-radius: var(--radius-md);
background: rgba(0, 0, 0, 0.08);
border-radius: 6px;
white-space: pre-wrap;
overflow: hidden;
max-height: 44px;
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 {
background: var(--bg-hover);
border-color: var(--border-strong);
background: rgba(0, 0, 0, 0.12);
border-color: rgba(255, 255, 255, 0.08);
}
/* Short inline output */
@@ -146,8 +114,8 @@
color: var(--text);
margin-top: 6px;
padding: 6px 8px;
background: var(--secondary);
border-radius: var(--radius-sm);
background: rgba(0, 0, 0, 0.06);
border-radius: 4px;
white-space: pre-wrap;
word-break: break-word;
}
@@ -196,3 +164,4 @@
transform: scale(1);
}
}
File diff suppressed because it is too large. Load diff
+296 -335
View File
File diff suppressed because it is too large. Load diff
+114 -205
View File
@@ -1,14 +1,10 @@
/* ===========================================
Shell Layout
=========================================== */
.shell {
--shell-pad: 16px;
--shell-gap: 16px;
--shell-nav-width: 220px;
--shell-topbar-height: 56px;
--shell-focus-duration: 200ms;
--shell-focus-ease: var(--ease-out);
--shell-focus-duration: 220ms;
--shell-focus-ease: cubic-bezier(0.2, 0.85, 0.25, 1);
height: 100vh;
display: grid;
grid-template-columns: var(--shell-nav-width) minmax(0, 1fr);
@@ -17,7 +13,7 @@
"topbar topbar"
"nav content";
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);
overflow: hidden;
}
@@ -65,10 +61,6 @@
gap: 0;
}
/* ===========================================
Topbar
=========================================== */
.topbar {
grid-area: topbar;
position: sticky;
@@ -81,7 +73,8 @@
padding: 0 20px;
height: var(--shell-topbar-height);
border-bottom: 1px solid var(--border);
background: var(--bg);
background: var(--panel);
backdrop-filter: blur(18px);
}
.topbar-left {
@@ -91,83 +84,49 @@
}
.topbar .nav-collapse-toggle {
width: 36px;
height: 36px;
width: 44px;
height: 44px;
margin-bottom: 0;
}
.topbar .nav-collapse-toggle__icon {
width: 20px;
height: 20px;
font-size: 22px;
}
.topbar .nav-collapse-toggle__icon svg {
width: 20px;
height: 20px;
}
/* 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;
flex-direction: column;
gap: 1px;
gap: 2px;
}
.brand-title {
font-size: 15px;
font-family: var(--font-display);
font-size: 16px;
letter-spacing: 1px;
text-transform: uppercase;
font-weight: 600;
letter-spacing: -0.02em;
line-height: 1.1;
color: var(--text-strong);
}
.brand-sub {
font-size: 11px;
font-weight: 500;
font-size: 10px;
color: var(--muted);
letter-spacing: 0.02em;
letter-spacing: 0.8px;
text-transform: uppercase;
line-height: 1;
}
/* Topbar status */
.topbar-status {
display: flex;
align-items: center;
gap: 8px;
}
/* Smaller pill and theme toggle in topbar */
.topbar-status .pill {
padding: 6px 10px;
padding: 4px 10px;
gap: 6px;
font-size: 12px;
font-weight: 500;
height: 32px;
box-sizing: border-box;
}
.topbar-status .pill .mono {
display: flex;
align-items: center;
line-height: 1;
margin-top: 0px;
font-size: 11px;
}
.topbar-status .statusDot {
@@ -176,9 +135,9 @@
}
.topbar-status .theme-toggle {
--theme-item: 24px;
--theme-gap: 2px;
--theme-pad: 3px;
--theme-item: 22px;
--theme-gap: 4px;
--theme-pad: 4px;
}
.topbar-status .theme-icon {
@@ -186,22 +145,17 @@
height: 12px;
}
/* ===========================================
Navigation Sidebar
=========================================== */
.nav {
grid-area: nav;
overflow-y: auto;
overflow-x: hidden;
padding: 16px 12px;
padding: 16px;
border-right: 1px solid var(--border);
background: var(--bg);
transition:
width 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;
background: var(--panel);
backdrop-filter: blur(18px);
transition: width var(--shell-focus-duration) var(--shell-focus-ease),
padding var(--shell-focus-duration) var(--shell-focus-ease);
min-height: 0; /* Allow grid item to shrink and enable scrolling */
}
.shell--chat-focus .nav {
@@ -210,9 +164,9 @@
border-width: 0;
overflow: hidden;
pointer-events: none;
opacity: 0;
}
/* Collapsed nav sidebar - completely hidden */
.nav--collapsed {
width: 0;
min-width: 0;
@@ -223,7 +177,7 @@
pointer-events: none;
}
/* Nav collapse toggle */
/* Nav collapse toggle button */
.nav-collapse-toggle {
width: 32px;
height: 32px;
@@ -232,88 +186,71 @@
justify-content: center;
background: transparent;
border: 1px solid transparent;
border-radius: var(--radius-md);
border-radius: 6px;
cursor: pointer;
transition:
background var(--duration-fast) ease,
border-color var(--duration-fast) ease;
transition: background 150ms ease, border-color 150ms ease;
margin-bottom: 16px;
}
.nav-collapse-toggle:hover {
background: var(--bg-hover);
background: rgba(255, 255, 255, 0.08);
border-color: var(--border);
}
:root[data-theme="light"] .nav-collapse-toggle:hover {
background: rgba(0, 0, 0, 0.06);
}
.nav-collapse-toggle__icon {
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
font-size: 16px;
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 {
margin-bottom: 20px;
margin-bottom: 18px;
display: grid;
gap: 2px;
gap: 6px;
padding-bottom: 12px;
border-bottom: 1px dashed rgba(255, 255, 255, 0.08);
}
.nav-group:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.nav-group__items {
display: grid;
gap: 1px;
gap: 4px;
}
.nav-group--collapsed .nav-group__items {
display: none;
}
/* Nav label */
.nav-label {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
width: 100%;
padding: 6px 10px;
padding: 4px 0;
font-size: 11px;
font-weight: 500;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 1.4px;
color: var(--text);
opacity: 0.7;
margin-bottom: 4px;
background: transparent;
border: none;
cursor: pointer;
text-align: left;
border-radius: var(--radius-sm);
transition:
color var(--duration-fast) ease,
background var(--duration-fast) ease;
}
.nav-label:hover {
color: var(--text);
background: var(--bg-hover);
opacity: 1;
}
.nav-label--static {
@@ -321,8 +258,7 @@
}
.nav-label--static:hover {
color: var(--muted);
background: transparent;
opacity: 0.7;
}
.nav-label__text {
@@ -330,152 +266,131 @@
}
.nav-label__chevron {
font-size: 10px;
opacity: 0.5;
transition: transform var(--duration-fast) ease;
font-size: 12px;
opacity: 0.6;
}
.nav-group--collapsed .nav-label__chevron {
transform: rotate(-90deg);
}
/* Nav items */
.nav-item {
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 10px;
padding: 8px 10px;
border-radius: var(--radius-md);
gap: 8px;
padding: 10px 12px 10px 14px;
border-radius: 12px;
border: 1px solid transparent;
background: transparent;
color: var(--muted);
cursor: pointer;
text-decoration: none;
transition:
border-color var(--duration-fast) ease,
background var(--duration-fast) ease,
color var(--duration-fast) ease;
transition: border-color 160ms ease, background 160ms ease, color 160ms ease;
}
.nav-item__icon {
width: 16px;
height: 16px;
font-size: 16px;
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
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 {
font-size: 13px;
font-weight: 500;
white-space: nowrap;
}
.nav-item:hover {
color: var(--text);
background: var(--bg-hover);
text-decoration: none;
border-color: rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.06);
}
.nav-item:hover .nav-item__icon {
opacity: 1;
.nav-item::before {
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 {
color: var(--text-strong);
background: var(--accent-subtle);
color: var(--text);
border-color: rgba(245, 159, 74, 0.45);
background: rgba(245, 159, 74, 0.12);
}
.nav-item.active .nav-item__icon {
opacity: 1;
color: var(--accent);
.nav-item.active::before {
background: var(--accent);
box-shadow: 0 0 12px rgba(245, 159, 74, 0.4);
}
/* ===========================================
Content Area
=========================================== */
.content {
grid-area: content;
padding: 8px 8px 24px;
padding: 8px 6px 20px;
display: flex;
flex-direction: column;
gap: 20px;
min-height: 0;
overflow-y: auto;
overflow-y: auto; /* Enable vertical scrolling for pages with long content */
overflow-x: hidden;
}
:root[data-theme="light"] .content {
background: var(--bg-content);
}
/* Chat handles its own scrolling (chat-thread); avoid double scrollbars. */
.content--chat {
overflow: hidden;
padding-bottom: 0;
}
/* Content header */
.shell--chat .content {
/* No-op: keep chat layout consistent with other tabs */
}
.content-header {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
padding: 4px 8px;
gap: 12px;
padding: 0 6px;
overflow: hidden;
transform-origin: top center;
transition:
opacity var(--shell-focus-duration) var(--shell-focus-ease),
transition: opacity 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),
padding var(--shell-focus-duration) var(--shell-focus-ease);
max-height: 80px;
max-height: 90px;
}
.shell--chat-focus .content-header {
opacity: 0;
transform: translateY(-8px);
transform: translateY(-10px);
max-height: 0px;
padding: 0;
pointer-events: none;
}
.page-title {
font-size: 24px;
font-weight: 600;
letter-spacing: -0.02em;
line-height: 1.2;
color: var(--text-strong);
font-family: var(--font-display);
font-size: 26px;
letter-spacing: 0.6px;
}
.page-sub {
color: var(--muted);
font-size: 13px;
font-weight: 400;
margin-top: 4px;
font-size: 12px;
letter-spacing: 0.4px;
}
.page-meta {
display: flex;
gap: 8px;
gap: 10px;
}
/* Chat view header adjustments */
/* Chat view: header and controls side by side */
.content--chat .content-header {
flex-direction: row;
align-items: center;
@@ -495,13 +410,9 @@
flex-shrink: 0;
}
/* ===========================================
Grid Utilities
=========================================== */
.grid {
display: grid;
gap: 16px;
gap: 18px;
}
.grid-cols-2 {
@@ -514,42 +425,39 @@
.stat-grid {
display: grid;
gap: 12px;
gap: 14px;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}
.note-grid {
display: grid;
gap: 12px;
gap: 14px;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.row {
display: flex;
gap: 10px;
gap: 12px;
align-items: center;
}
.stack {
display: grid;
gap: 12px;
gap: 14px;
}
.filters {
display: flex;
flex-wrap: wrap;
gap: 8px;
gap: 10px;
align-items: center;
}
/* ===========================================
Responsive - Tablet
=========================================== */
@media (max-width: 1100px) {
.shell {
--shell-pad: 12px;
--shell-gap: 12px;
--shell-nav-col: 1fr;
grid-template-columns: 1fr;
grid-template-rows: auto auto 1fr;
grid-template-areas:
@@ -562,18 +470,17 @@
position: static;
max-height: none;
display: flex;
gap: 6px;
gap: 16px;
overflow-x: auto;
border-right: none;
border-bottom: 1px solid var(--border);
padding: 10px 14px;
background: var(--bg);
padding: 12px;
}
.nav-group {
grid-auto-flow: column;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
margin-bottom: 0;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
border-bottom: none;
padding-bottom: 0;
}
.grid-cols-2,
@@ -583,11 +490,13 @@
.topbar {
position: static;
padding: 12px 14px;
gap: 10px;
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.topbar-status {
width: 100%;
flex-wrap: wrap;
}
+61 -121
View File
@@ -1,15 +1,12 @@
/* ===========================================
Mobile Layout
=========================================== */
/* Tablet: Horizontal nav */
/* Tablet/Mobile nav fix (under 1100px) - single horizontal scroll row */
@media (max-width: 1100px) {
/* Flatten nav into single horizontal scroll */
.nav {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
gap: 4px;
padding: 10px 14px;
gap: 6px;
padding: 10px 12px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
@@ -19,18 +16,21 @@
display: none;
}
/* Nav groups should flow inline, not stack */
.nav-group {
display: contents;
display: contents; /* Flatten group wrapper - items flow directly into .nav */
}
.nav-group__items {
display: contents;
display: contents; /* Flatten items wrapper too */
}
/* Hide group labels on tablet/mobile */
.nav-label {
display: none;
}
/* Don't hide nav items even if group is "collapsed" */
.nav-group--collapsed .nav-group__items {
display: contents;
}
@@ -38,22 +38,27 @@
.nav-item {
padding: 8px 14px;
font-size: 13px;
border-radius: var(--radius-md);
border-radius: 10px;
white-space: nowrap;
flex-shrink: 0;
}
.nav-item::before {
display: none;
}
}
/* Mobile-specific styles */
/* Mobile-specific improvements */
@media (max-width: 600px) {
.shell {
--shell-pad: 8px;
--shell-gap: 8px;
}
/* Topbar */
/* Compact topbar for mobile */
.topbar {
padding: 10px 12px;
border-radius: 12px;
gap: 8px;
flex-direction: row;
flex-wrap: wrap;
@@ -67,7 +72,8 @@
}
.brand-title {
font-size: 14px;
font-size: 15px;
letter-spacing: 0.3px;
}
.brand-sub {
@@ -94,10 +100,11 @@
display: none;
}
/* Nav */
/* Horizontal scrollable nav for mobile */
.nav {
padding: 8px 10px;
gap: 4px;
padding: 8px;
border-radius: 12px;
gap: 8px;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
@@ -115,14 +122,18 @@
}
.nav-item {
padding: 6px 10px;
padding: 7px 10px;
font-size: 12px;
border-radius: var(--radius-md);
border-radius: 8px;
white-space: nowrap;
flex-shrink: 0;
}
/* Content */
.nav-item::before {
display: none;
}
/* Hide page title on mobile - nav already shows where you are */
.content-header {
display: none;
}
@@ -132,17 +143,17 @@
gap: 12px;
}
/* Cards */
/* Smaller cards on mobile */
.card {
padding: 12px;
border-radius: var(--radius-md);
border-radius: 12px;
}
.card-title {
font-size: 13px;
font-size: 14px;
}
/* Stats */
/* Stat grid adjustments */
.stat-grid {
gap: 8px;
grid-template-columns: repeat(2, 1fr);
@@ -150,24 +161,24 @@
.stat {
padding: 10px;
border-radius: var(--radius-md);
border-radius: 10px;
}
.stat-label {
font-size: 11px;
font-size: 10px;
}
.stat-value {
font-size: 18px;
font-size: 16px;
}
/* Notes */
/* Notes grid */
.note-grid {
grid-template-columns: 1fr;
gap: 8px;
gap: 10px;
}
/* Forms */
/* Form fields */
.form-grid {
grid-template-columns: 1fr;
gap: 10px;
@@ -177,14 +188,14 @@
.field textarea,
.field select {
padding: 8px 10px;
border-radius: var(--radius-md);
border-radius: 10px;
font-size: 14px;
}
/* Buttons */
.btn {
padding: 8px 12px;
font-size: 12px;
font-size: 13px;
}
/* Pills */
@@ -193,7 +204,7 @@
font-size: 12px;
}
/* Chat */
/* Chat-specific mobile improvements */
.chat-header {
flex-direction: column;
align-items: stretch;
@@ -216,16 +227,17 @@
.chat-thread {
margin-top: 8px;
padding: 12px 8px;
padding: 10px 8px;
border-radius: 12px;
}
.chat-msg {
max-width: 90%;
max-width: 92%;
}
.chat-bubble {
padding: 8px 12px;
border-radius: var(--radius-md);
padding: 8px 10px;
border-radius: 12px;
}
.chat-compose {
@@ -235,14 +247,14 @@
.chat-compose__field textarea {
min-height: 60px;
padding: 8px 10px;
border-radius: var(--radius-md);
border-radius: 12px;
font-size: 14px;
}
/* Log stream */
/* Log stream mobile */
.log-stream {
border-radius: var(--radius-md);
max-height: 380px;
border-radius: 10px;
max-height: 400px;
}
.log-row {
@@ -267,14 +279,14 @@
font-size: 12px;
}
/* Lists */
/* List items */
.list-item {
padding: 10px;
border-radius: var(--radius-md);
border-radius: 10px;
}
.list-title {
font-size: 13px;
font-size: 14px;
}
.list-sub {
@@ -284,91 +296,19 @@
/* Code blocks */
.code-block {
padding: 8px;
border-radius: var(--radius-md);
border-radius: 10px;
font-size: 11px;
}
/* Theme toggle */
/* Theme toggle smaller */
.theme-toggle {
--theme-item: 24px;
--theme-gap: 2px;
--theme-pad: 3px;
--theme-gap: 4px;
--theme-pad: 4px;
}
.theme-icon {
width: 12px;
height: 12px;
}
}
/* 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;
width: 14px;
height: 14px;
}
}
+1
View File
@@ -3,3 +3,4 @@ export type EventLogEntry = {
event: string;
payload?: unknown;
};
Loaded 100 of 127 files, more files were not shown because too many files have changed in this diff. Show more