refactor(webchat): SwiftUI-only WebChat UI

# Conflicts:
#	apps/macos/Package.swift
This commit is contained in:
Peter Steinberger
2025-12-17 23:05:28 +01:00
parent ca85d217ec
commit 875cf9a054
7451 changed files with 218063 additions and 776607 deletions

View File

@@ -51,7 +51,6 @@ let package = Package(
resources: [
.copy("Resources/Clawdis.icns"),
.copy("Resources/CanvasA2UI"),
.copy("Resources/WebChat"),
.copy("Resources/DeviceModels"),
],
swiftSettings: [

View File

@@ -142,20 +142,6 @@ final class AppState {
}
}
var webChatEnabled: Bool {
didSet { self.ifNotPreview { UserDefaults.standard.set(self.webChatEnabled, forKey: webChatEnabledKey) } }
}
var webChatSwiftUIEnabled: Bool {
didSet { self.ifNotPreview { UserDefaults.standard.set(
self.webChatSwiftUIEnabled,
forKey: webChatSwiftUIEnabledKey) } }
}
var webChatPort: Int {
didSet { self.ifNotPreview { UserDefaults.standard.set(self.webChatPort, forKey: webChatPortKey) } }
}
var canvasEnabled: Bool {
didSet { self.ifNotPreview { UserDefaults.standard.set(self.canvasEnabled, forKey: canvasEnabledKey) } }
}
@@ -243,10 +229,6 @@ final class AppState {
self.remoteTarget = UserDefaults.standard.string(forKey: remoteTargetKey) ?? ""
self.remoteIdentity = UserDefaults.standard.string(forKey: remoteIdentityKey) ?? ""
self.remoteProjectRoot = UserDefaults.standard.string(forKey: remoteProjectRootKey) ?? ""
self.webChatEnabled = UserDefaults.standard.object(forKey: webChatEnabledKey) as? Bool ?? true
self.webChatSwiftUIEnabled = UserDefaults.standard.object(forKey: webChatSwiftUIEnabledKey) as? Bool ?? false
let storedPort = UserDefaults.standard.integer(forKey: webChatPortKey)
self.webChatPort = storedPort > 0 ? storedPort : 18788
self.canvasEnabled = UserDefaults.standard.object(forKey: canvasEnabledKey) as? Bool ?? true
self.peekabooBridgeEnabled = UserDefaults.standard
.object(forKey: peekabooBridgeEnabledKey) as? Bool ?? true
@@ -376,9 +358,6 @@ extension AppState {
state.iconOverride = .system
state.heartbeatsEnabled = true
state.connectionMode = .local
state.webChatEnabled = true
state.webChatSwiftUIEnabled = false
state.webChatPort = 18788
state.canvasEnabled = true
state.remoteTarget = "user@example.com"
state.remoteIdentity = "~/.ssh/id_ed25519"
@@ -399,19 +378,6 @@ enum AppStateStore {
}
}
static var webChatEnabled: Bool {
UserDefaults.standard.object(forKey: webChatEnabledKey) as? Bool ?? true
}
static var webChatSwiftUIEnabled: Bool {
UserDefaults.standard.object(forKey: webChatSwiftUIEnabledKey) as? Bool ?? false
}
static var webChatPort: Int {
let stored = UserDefaults.standard.integer(forKey: webChatPortKey)
return stored > 0 ? stored : 18788
}
static var canvasEnabled: Bool {
UserDefaults.standard.object(forKey: canvasEnabledKey) as? Bool ?? true
}

View File

@@ -22,8 +22,6 @@ struct ConfigSettings: View {
@State private var allowAutosave = false
@State private var heartbeatMinutes: Int?
@State private var heartbeatBody: String = "HEARTBEAT"
@AppStorage(webChatEnabledKey) private var webChatEnabled: Bool = true
@AppStorage(webChatPortKey) private var webChatPort: Int = 18788
// clawd browser settings (stored in ~/.clawdis/clawdis.json under "browser")
@State private var browserEnabled: Bool = true
@@ -54,7 +52,6 @@ struct ConfigSettings: View {
self.header
self.agentSection
self.heartbeatSection
self.webChatSection
self.browserSection
Spacer(minLength: 0)
}
@@ -188,39 +185,6 @@ struct ConfigSettings: View {
.frame(maxWidth: .infinity, alignment: .leading)
}
private var webChatSection: some View {
GroupBox("Web Chat") {
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 14, verticalSpacing: 10) {
GridRow {
self.gridLabel("Enabled")
Toggle("", isOn: self.$webChatEnabled)
.labelsHidden()
.toggleStyle(.checkbox)
}
GridRow {
self.gridLabel("Port")
TextField("18788", value: self.$webChatPort, formatter: NumberFormatter())
.textFieldStyle(.roundedBorder)
.frame(width: 100)
.disabled(!self.webChatEnabled)
}
GridRow {
Color.clear
.frame(width: self.labelColumnWidth, height: 1)
Text(
"""
Mac app connects to the gateways loopback web chat on this port.
Remote mode uses SSH -L to forward it.
""")
.font(.footnote)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
private var browserSection: some View {
GroupBox("Browser (clawd)") {
Grid(alignment: .leadingFirstTextBaseline, horizontalSpacing: 14, verticalSpacing: 10) {

View File

@@ -8,7 +8,7 @@ final class ConnectionModeCoordinator {
private let logger = Logger(subsystem: "com.steipete.clawdis", category: "connection")
/// Apply the requested connection mode by starting/stopping local gateway,
/// managing the control-channel SSH tunnel, and cleaning up WebChat tunnels.
/// managing the control-channel SSH tunnel, and cleaning up chat windows/panels.
func apply(mode: AppState.ConnectionMode, paused: Bool) async {
switch mode {
case .local:

View File

@@ -20,9 +20,6 @@ let connectionModeKey = "clawdis.connectionMode"
let remoteTargetKey = "clawdis.remoteTarget"
let remoteIdentityKey = "clawdis.remoteIdentity"
let remoteProjectRootKey = "clawdis.remoteProjectRoot"
let webChatEnabledKey = "clawdis.webChatEnabled"
let webChatSwiftUIEnabledKey = "clawdis.webChatSwiftUIEnabled"
let webChatPortKey = "clawdis.webChatPort"
let canvasEnabledKey = "clawdis.canvasEnabled"
let cameraEnabledKey = "clawdis.cameraEnabled"
let peekabooBridgeEnabledKey = "clawdis.peekabooBridgeEnabled"

View File

@@ -193,12 +193,6 @@ enum DebugActions {
typealias PortListener = PortGuardian.ReportListener
typealias PortReport = PortGuardian.PortReport
@MainActor
static func openChatInBrowser() async {
let session = WebChatManager.shared.preferredSessionKey()
await WebChatManager.shared.openInBrowser(sessionKey: session)
}
static func checkGatewayPorts() async -> [PortReport] {
let mode = CommandResolver.connectionSettings().mode
return await PortGuardian.shared.diagnose(mode: mode)

View File

@@ -24,7 +24,6 @@ struct DebugSettings: View {
@State private var portReports: [DebugActions.PortReport] = []
@State private var portKillStatus: String?
@State private var pendingKill: DebugActions.PortListener?
@AppStorage(webChatSwiftUIEnabledKey) private var webChatSwiftUIEnabled: Bool = false
@AppStorage(attachExistingGatewayOnlyKey) private var attachExistingGatewayOnly: Bool = false
@AppStorage(debugFileLogEnabledKey) private var diagnosticsFileLogEnabled: Bool = false
@@ -278,7 +277,7 @@ struct DebugSettings: View {
}
if self.portReports.isEmpty, !self.portCheckInFlight {
Text("Check which process owns 18788/18789 and suggest fixes.")
Text("Check which process owns 18789 and suggest fixes.")
.font(.caption2)
.foregroundStyle(.secondary)
} else {
@@ -579,12 +578,10 @@ struct DebugSettings: View {
.frame(maxWidth: 280, alignment: .leading)
}
GridRow {
self.gridLabel("Web chat")
Toggle("Use SwiftUI web chat (glass)", isOn: self.$webChatSwiftUIEnabled)
.toggleStyle(.checkbox)
.help(
"When enabled, the menu bar chat window/panel uses the native SwiftUI UI instead of the " +
"bundled WKWebView.")
self.gridLabel("Chat")
Text("Native SwiftUI")
.font(.callout)
.foregroundStyle(.secondary)
}
}
}

View File

@@ -16,7 +16,7 @@ enum GatewayAgentChannel: String, Codable, CaseIterable, Sendable {
self = GatewayAgentChannel(rawValue: normalized) ?? .last
}
var isDeliverable: Bool { self == .whatsapp || self == .telegram }
var isDeliverable: Bool { self != .webchat }
func shouldDeliver(_ deliver: Bool) -> Bool { deliver && self.isDeliverable }
}

View File

@@ -106,10 +106,6 @@ struct ClawdisApp: App {
@MainActor
private func toggleWebChatPanel() {
guard AppStateStore.webChatEnabled else {
self.isMenuPresented = true
return
}
self.isMenuPresented = false
WebChatManager.shared.togglePanel(
sessionKey: WebChatManager.shared.preferredSessionKey(),
@@ -159,7 +155,7 @@ private final class StatusItemMouseHandlerView: NSView {
final class AppDelegate: NSObject, NSApplicationDelegate {
private var state: AppState?
private let webChatAutoLogger = Logger(subsystem: "com.steipete.clawdis", category: "WebChat")
private let webChatAutoLogger = Logger(subsystem: "com.steipete.clawdis", category: "Chat")
private let socketServer = ControlSocketServer()
let updaterController: UpdaterProviding = makeUpdaterController()
@@ -192,9 +188,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
Task { await PeekabooBridgeHostCoordinator.shared.setEnabled(AppStateStore.shared.peekabooBridgeEnabled) }
self.scheduleFirstRunOnboardingIfNeeded()
// Developer/testing helper: auto-open WebChat when launched with --webchat
if CommandLine.arguments.contains("--webchat") {
self.webChatAutoLogger.debug("Auto-opening web chat via --webchat flag")
// Developer/testing helper: auto-open chat when launched with --chat (or legacy --webchat).
if CommandLine.arguments.contains("--chat") || CommandLine.arguments.contains("--webchat") {
self.webChatAutoLogger.debug("Auto-opening chat via CLI flag")
WebChatManager.shared.show(sessionKey: "main")
}
}

View File

@@ -41,10 +41,8 @@ struct MenuContent: View {
if self.showVoiceWakeMicPicker {
self.voiceWakeMicMenu
}
if AppStateStore.webChatEnabled {
Button("Open Chat") {
WebChatManager.shared.show(sessionKey: WebChatManager.shared.preferredSessionKey())
}
Button("Open Chat") {
WebChatManager.shared.show(sessionKey: WebChatManager.shared.preferredSessionKey())
}
Toggle(
isOn: Binding(
@@ -205,11 +203,6 @@ struct MenuContent: View {
} label: {
Label("Send Test Notification", systemImage: "bell")
}
Button {
Task { await DebugActions.openChatInBrowser() }
} label: {
Label("Open Chat in Browser…", systemImage: "safari")
}
Divider()
Button {
DebugActions.restartGateway()

View File

@@ -38,7 +38,7 @@ actor PortGuardian {
func sweep(mode: AppState.ConnectionMode) async {
self.logger.info("port sweep starting (mode=\(mode.rawValue, privacy: .public))")
let ports = [18788, 18789]
let ports = [18789]
for port in ports {
let listeners = await self.listeners(on: port)
guard !listeners.isEmpty else { continue }
@@ -141,7 +141,7 @@ actor PortGuardian {
}
func diagnose(mode: AppState.ConnectionMode) async -> [PortReport] {
let ports = [18788, 18789]
let ports = [18789]
var reports: [PortReport] = []
for port in ports {
@@ -155,9 +155,7 @@ actor PortGuardian {
expectedDesc = "SSH tunnel to remote gateway"
okPredicate = { $0.command.lowercased().contains("ssh") }
case .local:
expectedDesc = port == 18788
? "Gateway webchat/static host"
: "Gateway websocket (node/tsx)"
expectedDesc = "Gateway websocket (node/tsx)"
okPredicate = { listener in
let c = listener.command.lowercased()
return expectedCommands.contains { c.contains($0) }
@@ -291,8 +289,6 @@ actor PortGuardian {
case .remote:
// Remote mode expects an SSH tunnel for the gateway WebSocket port.
if port == 18789 { return cmd.contains("ssh") }
// WebChat assets may be served locally (Clawdis) or forwarded via an older SSH tunnel.
if port == 18788 { return cmd.contains("clawdis") || cmd.contains("ssh") }
return false
case .local:
return expectedCommands.contains { cmd.contains($0) }

View File

@@ -0,0 +1,247 @@
import Foundation
import Network
import OSLog
#if canImport(Darwin)
import Darwin
#endif
/// Port forwarding tunnel for remote mode.
///
/// Uses `ssh -N -L` to forward the remote gateway ports to localhost.
final class RemotePortTunnel {
private static let logger = Logger(subsystem: "com.steipete.clawdis", category: "remote.tunnel")
let process: Process
let localPort: UInt16?
private let stderrHandle: FileHandle?
private init(process: Process, localPort: UInt16?, stderrHandle: FileHandle?) {
self.process = process
self.localPort = localPort
self.stderrHandle = stderrHandle
}
deinit {
Self.cleanupStderr(self.stderrHandle)
let pid = self.process.processIdentifier
self.process.terminate()
Task { await PortGuardian.shared.removeRecord(pid: pid) }
}
func terminate() {
Self.cleanupStderr(self.stderrHandle)
let pid = self.process.processIdentifier
if self.process.isRunning {
self.process.terminate()
self.process.waitUntilExit()
}
Task { await PortGuardian.shared.removeRecord(pid: pid) }
}
static func create(remotePort: Int, preferredLocalPort: UInt16? = nil) async throws -> RemotePortTunnel {
let settings = CommandResolver.connectionSettings()
guard settings.mode == .remote, let parsed = CommandResolver.parseSSHTarget(settings.target) else {
throw NSError(
domain: "RemotePortTunnel",
code: 3,
userInfo: [NSLocalizedDescriptionKey: "Remote mode is not configured"])
}
let localPort = try await Self.findPort(preferred: preferredLocalPort)
var args: [String] = [
"-o", "BatchMode=yes",
"-o", "IdentitiesOnly=yes",
"-o", "ExitOnForwardFailure=yes",
"-o", "ServerAliveInterval=15",
"-o", "ServerAliveCountMax=3",
"-o", "TCPKeepAlive=yes",
"-N",
"-L", "\(localPort):127.0.0.1:\(remotePort)",
]
if parsed.port > 0 { args.append(contentsOf: ["-p", String(parsed.port)]) }
let identity = settings.identity.trimmingCharacters(in: .whitespacesAndNewlines)
if !identity.isEmpty { args.append(contentsOf: ["-i", identity]) }
let userHost = parsed.user.map { "\($0)@\(parsed.host)" } ?? parsed.host
args.append(userHost)
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/ssh")
process.arguments = args
let pipe = Pipe()
process.standardError = pipe
let stderrHandle = pipe.fileHandleForReading
// Consume stderr so ssh cannot block if it logs.
stderrHandle.readabilityHandler = { handle in
let data = handle.readSafely(upToCount: 64 * 1024)
guard !data.isEmpty else {
// EOF (or read failure): stop monitoring to avoid spinning on a closed pipe.
Self.cleanupStderr(handle)
return
}
guard let line = String(data: data, encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines),
!line.isEmpty
else { return }
Self.logger.error("ssh tunnel stderr: \(line, privacy: .public)")
}
process.terminationHandler = { _ in
Self.cleanupStderr(stderrHandle)
}
try process.run()
// If ssh exits immediately (e.g. local port already in use), surface stderr and ensure we stop monitoring.
try? await Task.sleep(nanoseconds: 150_000_000) // 150ms
if !process.isRunning {
let stderr = Self.drainStderr(stderrHandle)
let msg = stderr.isEmpty ? "ssh tunnel exited immediately" : "ssh tunnel failed: \(stderr)"
throw NSError(domain: "RemotePortTunnel", code: 4, userInfo: [NSLocalizedDescriptionKey: msg])
}
// Track tunnel so we can clean up stale listeners on restart.
Task {
await PortGuardian.shared.record(
port: Int(localPort),
pid: process.processIdentifier,
command: process.executableURL?.path ?? "ssh",
mode: CommandResolver.connectionSettings().mode)
}
return RemotePortTunnel(process: process, localPort: localPort, stderrHandle: stderrHandle)
}
private static func findPort(preferred: UInt16?) async throws -> UInt16 {
if let preferred, self.portIsFree(preferred) { return preferred }
return try await withCheckedThrowingContinuation { cont in
let queue = DispatchQueue(label: "com.steipete.clawdis.remote.tunnel.port", qos: .utility)
do {
let listener = try NWListener(using: .tcp, on: .any)
listener.newConnectionHandler = { connection in connection.cancel() }
listener.stateUpdateHandler = { state in
switch state {
case .ready:
if let port = listener.port?.rawValue {
listener.stateUpdateHandler = nil
listener.cancel()
cont.resume(returning: port)
}
case let .failed(error):
listener.stateUpdateHandler = nil
listener.cancel()
cont.resume(throwing: error)
default:
break
}
}
listener.start(queue: queue)
} catch {
cont.resume(throwing: error)
}
}
}
private static func portIsFree(_ port: UInt16) -> Bool {
#if canImport(Darwin)
// NWListener can succeed even when only one address family is held. Mirror what ssh needs by checking
// both 127.0.0.1 and ::1 for availability.
return self.canBindIPv4(port) && self.canBindIPv6(port)
#else
do {
let listener = try NWListener(using: .tcp, on: NWEndpoint.Port(rawValue: port)!)
listener.cancel()
return true
} catch {
return false
}
#endif
}
#if canImport(Darwin)
private static func canBindIPv4(_ port: UInt16) -> Bool {
let fd = socket(AF_INET, SOCK_STREAM, 0)
guard fd >= 0 else { return false }
defer { _ = Darwin.close(fd) }
var one: Int32 = 1
_ = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, socklen_t(MemoryLayout.size(ofValue: one)))
var addr = sockaddr_in()
addr.sin_len = UInt8(MemoryLayout<sockaddr_in>.size)
addr.sin_family = sa_family_t(AF_INET)
addr.sin_port = port.bigEndian
addr.sin_addr = in_addr(s_addr: inet_addr("127.0.0.1"))
let result = withUnsafePointer(to: &addr) { ptr in
ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in
Darwin.bind(fd, sa, socklen_t(MemoryLayout<sockaddr_in>.size))
}
}
return result == 0
}
private static func canBindIPv6(_ port: UInt16) -> Bool {
let fd = socket(AF_INET6, SOCK_STREAM, 0)
guard fd >= 0 else { return false }
defer { _ = Darwin.close(fd) }
var one: Int32 = 1
_ = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, socklen_t(MemoryLayout.size(ofValue: one)))
var addr = sockaddr_in6()
addr.sin6_len = UInt8(MemoryLayout<sockaddr_in6>.size)
addr.sin6_family = sa_family_t(AF_INET6)
addr.sin6_port = port.bigEndian
var loopback = in6_addr()
_ = withUnsafeMutablePointer(to: &loopback) { ptr in
inet_pton(AF_INET6, "::1", ptr)
}
addr.sin6_addr = loopback
let result = withUnsafePointer(to: &addr) { ptr in
ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in
Darwin.bind(fd, sa, socklen_t(MemoryLayout<sockaddr_in6>.size))
}
}
return result == 0
}
#endif
private static func cleanupStderr(_ handle: FileHandle?) {
guard let handle else { return }
Self.cleanupStderr(handle)
}
private static func cleanupStderr(_ handle: FileHandle) {
if handle.readabilityHandler != nil {
handle.readabilityHandler = nil
}
try? handle.close()
}
private static func drainStderr(_ handle: FileHandle) -> String {
handle.readabilityHandler = nil
defer { try? handle.close() }
do {
let data = try handle.readToEnd() ?? Data()
return String(data: data, encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
} catch {
self.logger.debug("Failed to drain ssh stderr: \(error, privacy: .public)")
return ""
}
}
#if SWIFT_PACKAGE
static func _testPortIsFree(_ port: UInt16) -> Bool {
self.portIsFree(port)
}
static func _testDrainStderr(_ handle: FileHandle) -> String {
self.drainStderr(handle)
}
#endif
}

View File

@@ -4,7 +4,7 @@ import Foundation
actor RemoteTunnelManager {
static let shared = RemoteTunnelManager()
private var controlTunnel: WebChatTunnel?
private var controlTunnel: RemotePortTunnel?
func controlTunnelPortIfRunning() async -> UInt16? {
if let tunnel = self.controlTunnel,
@@ -38,7 +38,7 @@ actor RemoteTunnelManager {
if let local = await self.controlTunnelPortIfRunning() { return local }
let desiredPort = UInt16(GatewayEnvironment.gatewayPort())
let tunnel = try await WebChatTunnel.create(
let tunnel = try await RemotePortTunnel.create(
remotePort: GatewayEnvironment.gatewayPort(),
preferredLocalPort: desiredPort)
self.controlTunnel = tunnel

View File

@@ -1,29 +0,0 @@
import { LitElement } from "lit";
import type { Agent } from "./agent/agent.js";
import "./components/AgentInterface.js";
import type { AgentTool } from "@mariozechner/pi-ai";
import type { AgentInterface } from "./components/AgentInterface.js";
import type { SandboxRuntimeProvider } from "./components/sandbox/SandboxRuntimeProvider.js";
import { ArtifactsPanel } from "./tools/artifacts/index.js";
export declare class ChatPanel extends LitElement {
agent?: Agent;
agentInterface?: AgentInterface;
artifactsPanel?: ArtifactsPanel;
private hasArtifacts;
private artifactCount;
private showArtifactsPanel;
private windowWidth;
private resizeHandler;
createRenderRoot(): this;
connectedCallback(): void;
disconnectedCallback(): void;
setAgent(agent: Agent, config?: {
onApiKeyRequired?: (provider: string) => Promise<boolean>;
onBeforeSend?: () => void | Promise<void>;
onCostClick?: () => void;
sandboxUrlProvider?: () => string;
toolsFactory?: (agent: Agent, agentInterface: AgentInterface, artifactsPanel: ArtifactsPanel, runtimeProvidersFactory: () => SandboxRuntimeProvider[]) => AgentTool<any>[];
}): Promise<void>;
render(): import("lit-html").TemplateResult<1>;
}
//# sourceMappingURL=ChatPanel.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"ChatPanel.d.ts","sourceRoot":"","sources":["../src/ChatPanel.ts"],"names":[],"mappings":"AACA,OAAO,EAAQ,UAAU,EAAE,MAAM,KAAK,CAAC;AAEvC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAGrE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gDAAgD,CAAC;AAC7F,OAAO,EAAE,cAAc,EAAyB,MAAM,4BAA4B,CAAC;AAOnF,qBACa,SAAU,SAAQ,UAAU;IACxB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,cAAc,CAAC,EAAE,cAAc,CAAC;IACvC,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,WAAW,CAAK;IAEjC,OAAO,CAAC,aAAa,CAGnB;IAEF,gBAAgB;IAIP,iBAAiB;IAejB,oBAAoB;IAKvB,QAAQ,CACb,KAAK,EAAE,KAAK,EACZ,MAAM,CAAC,EAAE;QACR,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;QAC1D,YAAY,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1C,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;QACzB,kBAAkB,CAAC,EAAE,MAAM,MAAM,CAAC;QAClC,YAAY,CAAC,EAAE,CACd,KAAK,EAAE,KAAK,EACZ,cAAc,EAAE,cAAc,EAC9B,cAAc,EAAE,cAAc,EAC9B,uBAAuB,EAAE,MAAM,sBAAsB,EAAE,KACnD,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC;KACtB;IAwFF,MAAM;CAkDN"}

View File

@@ -1,197 +0,0 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Badge } from "@mariozechner/mini-lit/dist/Badge.js";
import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
import "./components/AgentInterface.js";
import { ArtifactsRuntimeProvider } from "./components/sandbox/ArtifactsRuntimeProvider.js";
import { AttachmentsRuntimeProvider } from "./components/sandbox/AttachmentsRuntimeProvider.js";
import { ArtifactsPanel, ArtifactsToolRenderer } from "./tools/artifacts/index.js";
import { registerToolRenderer } from "./tools/renderer-registry.js";
import { i18n } from "./utils/i18n.js";
const BREAKPOINT = 800; // px - switch between overlay and side-by-side
let ChatPanel = class ChatPanel extends LitElement {
constructor() {
super(...arguments);
this.hasArtifacts = false;
this.artifactCount = 0;
this.showArtifactsPanel = false;
this.windowWidth = 0;
this.resizeHandler = () => {
this.windowWidth = window.innerWidth;
this.requestUpdate();
};
}
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.windowWidth = window.innerWidth; // Set initial width after connection
window.addEventListener("resize", this.resizeHandler);
this.style.display = "flex";
this.style.flexDirection = "column";
this.style.height = "100%";
this.style.minHeight = "0";
// Update width after initial render
requestAnimationFrame(() => {
this.windowWidth = window.innerWidth;
this.requestUpdate();
});
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("resize", this.resizeHandler);
}
async setAgent(agent, config) {
this.agent = agent;
// Create AgentInterface
this.agentInterface = document.createElement("agent-interface");
this.agentInterface.session = agent;
this.agentInterface.sessionThinkingLevel = config?.sessionThinkingLevel ?? agent?.state?.thinkingLevel ?? "off";
this.agentInterface.enableAttachments = true;
// Hide model selector in the embedded chat; use fixed model configured at bootstrap.
this.agentInterface.enableModelSelector = false;
this.agentInterface.enableThinkingSelector = true;
this.agentInterface.showThemeToggle = false;
// In embedded mode, bypass API key prompts; native transport handles auth.
this.agentInterface.onApiKeyRequired = async () => true;
this.agentInterface.onApiKeyRequired = config?.onApiKeyRequired;
this.agentInterface.onBeforeSend = config?.onBeforeSend;
this.agentInterface.onCostClick = config?.onCostClick;
// Set up artifacts panel
this.artifactsPanel = new ArtifactsPanel();
this.artifactsPanel.agent = agent; // Pass agent for HTML artifact runtime providers
if (config?.sandboxUrlProvider) {
this.artifactsPanel.sandboxUrlProvider = config.sandboxUrlProvider;
}
// Register the standalone tool renderer (not the panel itself)
registerToolRenderer("artifacts", new ArtifactsToolRenderer(this.artifactsPanel));
// Runtime providers factory for REPL tools (read-write access)
const runtimeProvidersFactory = () => {
const attachments = [];
for (const message of this.agent.state.messages) {
if (message.role === "user") {
message.attachments?.forEach((a) => {
attachments.push(a);
});
}
}
const providers = [];
// Add attachments provider if there are attachments
if (attachments.length > 0) {
providers.push(new AttachmentsRuntimeProvider(attachments));
}
// Add artifacts provider with read-write access (for REPL)
providers.push(new ArtifactsRuntimeProvider(this.artifactsPanel, this.agent, true));
return providers;
};
this.artifactsPanel.onArtifactsChange = () => {
const count = this.artifactsPanel?.artifacts?.size ?? 0;
const created = count > this.artifactCount;
this.hasArtifacts = count > 0;
this.artifactCount = count;
if (this.hasArtifacts && created) {
this.showArtifactsPanel = true;
}
this.requestUpdate();
};
this.artifactsPanel.onClose = () => {
this.showArtifactsPanel = false;
this.requestUpdate();
};
this.artifactsPanel.onOpen = () => {
this.showArtifactsPanel = true;
this.requestUpdate();
};
// Set tools on the agent
// Pass runtimeProvidersFactory so consumers can configure their own REPL tools
const additionalTools = config?.toolsFactory?.(agent, this.agentInterface, this.artifactsPanel, runtimeProvidersFactory) || [];
const tools = [this.artifactsPanel.tool, ...additionalTools];
this.agent.setTools(tools);
// Reconstruct artifacts from existing messages
// Temporarily disable the onArtifactsChange callback to prevent auto-opening on load
const originalCallback = this.artifactsPanel.onArtifactsChange;
this.artifactsPanel.onArtifactsChange = undefined;
await this.artifactsPanel.reconstructFromMessages(this.agent.state.messages);
this.artifactsPanel.onArtifactsChange = originalCallback;
this.hasArtifacts = this.artifactsPanel.artifacts.size > 0;
this.artifactCount = this.artifactsPanel.artifacts.size;
this.requestUpdate();
}
render() {
if (!this.agent || !this.agentInterface) {
return html `<div class="flex items-center justify-center h-full">
<div class="text-muted-foreground">No agent set</div>
</div>`;
}
const isMobile = this.windowWidth < BREAKPOINT;
// Set panel props
if (this.artifactsPanel) {
this.artifactsPanel.collapsed = !this.showArtifactsPanel;
this.artifactsPanel.overlay = isMobile;
}
return html `
<div class="relative w-full h-full overflow-hidden flex">
<div class="h-full" style="${!isMobile && this.showArtifactsPanel && this.hasArtifacts ? "width: 50%;" : "width: 100%;"}">
${this.agentInterface}
</div>
<!-- Floating pill when artifacts exist and panel is collapsed -->
${this.hasArtifacts && !this.showArtifactsPanel
? html `
<button
class="absolute z-30 top-4 left-1/2 -translate-x-1/2 pointer-events-auto"
@click=${() => {
this.showArtifactsPanel = true;
this.requestUpdate();
}}
title=${i18n("Show artifacts")}
>
${Badge(html `
<span class="inline-flex items-center gap-1">
<span>${i18n("Artifacts")}</span>
<span class="text-[10px] leading-none bg-primary-foreground/20 text-primary-foreground rounded px-1 font-mono tabular-nums">${this.artifactCount}</span>
</span>
`)}
</button>
`
: ""}
<div class="h-full ${isMobile ? "absolute inset-0 pointer-events-none" : ""}" style="${!isMobile ? (!this.hasArtifacts || !this.showArtifactsPanel ? "display: none;" : "width: 50%;") : ""}">
${this.artifactsPanel}
</div>
</div>
`;
}
};
__decorate([
state()
], ChatPanel.prototype, "agent", void 0);
__decorate([
state()
], ChatPanel.prototype, "agentInterface", void 0);
__decorate([
state()
], ChatPanel.prototype, "artifactsPanel", void 0);
__decorate([
state()
], ChatPanel.prototype, "hasArtifacts", void 0);
__decorate([
state()
], ChatPanel.prototype, "artifactCount", void 0);
__decorate([
state()
], ChatPanel.prototype, "showArtifactsPanel", void 0);
__decorate([
state()
], ChatPanel.prototype, "windowWidth", void 0);
ChatPanel = __decorate([
customElement("pi-chat-panel")
], ChatPanel);
export { ChatPanel };
//# sourceMappingURL=ChatPanel.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,62 +0,0 @@
import { type AgentTool, type Message, type Model } from "@mariozechner/pi-ai";
import type { AppMessage } from "../components/Messages.js";
import type { Attachment } from "../utils/attachment-utils.js";
import type { AgentTransport } from "./transports/types.js";
import type { DebugLogEntry } from "./types.js";
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high";
export interface AgentState {
systemPrompt: string;
model: Model<any>;
thinkingLevel: ThinkingLevel;
tools: AgentTool<any>[];
messages: AppMessage[];
isStreaming: boolean;
streamMessage: Message | null;
pendingToolCalls: Set<string>;
error?: string;
}
export type AgentEvent = {
type: "state-update";
state: AgentState;
} | {
type: "error-no-model";
} | {
type: "error-no-api-key";
provider: string;
} | {
type: "started";
} | {
type: "completed";
};
export interface AgentOptions {
initialState?: Partial<AgentState>;
debugListener?: (entry: DebugLogEntry) => void;
transport: AgentTransport;
messageTransformer?: (messages: AppMessage[]) => Message[] | Promise<Message[]>;
}
export declare class Agent {
private _state;
private listeners;
private abortController?;
private transport;
private debugListener?;
private messageTransformer;
private messageQueue;
constructor(opts: AgentOptions);
get state(): AgentState;
subscribe(fn: (e: AgentEvent) => void): () => void;
setSystemPrompt(v: string): void;
setModel(m: Model<any>): void;
setThinkingLevel(l: ThinkingLevel): void;
setTools(t: AgentTool<any>[]): void;
replaceMessages(ms: AppMessage[]): void;
appendMessage(m: AppMessage): void;
queueMessage(m: AppMessage): Promise<void>;
clearMessages(): void;
abort(): void;
private logState;
prompt(input: string, attachments?: Attachment[]): Promise<void>;
private patch;
private emit;
}
//# sourceMappingURL=agent.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/agent/agent.ts"],"names":[],"mappings":"AACA,OAAO,EACN,KAAK,SAAS,EAId,KAAK,OAAO,EACZ,KAAK,KAAK,EAEV,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,KAAK,EAAkB,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAmBhD,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE1E,MAAM,WAAW,UAAU;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,aAAa,EAAE,aAAa,CAAC;IAC7B,KAAK,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC;IACxB,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,EAAE,OAAO,GAAG,IAAI,CAAC;IAC9B,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,UAAU,GACnB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,GAC3C;IAAE,IAAI,EAAE,gBAAgB,CAAA;CAAE,GAC1B;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC;AAEzB,MAAM,WAAW,YAAY;IAC5B,YAAY,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IACnC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C,SAAS,EAAE,cAAc,CAAC;IAE1B,kBAAkB,CAAC,EAAE,CAAC,QAAQ,EAAE,UAAU,EAAE,KAAK,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;CAChF;AAED,qBAAa,KAAK;IACjB,OAAO,CAAC,MAAM,CAUZ;IACF,OAAO,CAAC,SAAS,CAAsC;IACvD,OAAO,CAAC,eAAe,CAAC,CAAkB;IAC1C,OAAO,CAAC,SAAS,CAAiB;IAClC,OAAO,CAAC,aAAa,CAAC,CAAiC;IACvD,OAAO,CAAC,kBAAkB,CAA6D;IACvF,OAAO,CAAC,YAAY,CAAwC;gBAEhD,IAAI,EAAE,YAAY;IAO9B,IAAI,KAAK,IAAI,UAAU,CAEtB;IAED,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,GAAG,MAAM,IAAI;IAOlD,eAAe,CAAC,CAAC,EAAE,MAAM;IAGzB,QAAQ,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC;IAGtB,gBAAgB,CAAC,CAAC,EAAE,aAAa;IAGjC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE;IAG5B,eAAe,CAAC,EAAE,EAAE,UAAU,EAAE;IAGhC,aAAa,CAAC,CAAC,EAAE,UAAU;IAGrB,YAAY,CAAC,CAAC,EAAE,UAAU;IAQhC,aAAa;IAIb,KAAK;IAIL,OAAO,CAAC,QAAQ;IAKV,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,UAAU,EAAE;IA8LtD,OAAO,CAAC,KAAK;IAKb,OAAO,CAAC,IAAI;CAKZ"}

View File

@@ -1,276 +0,0 @@
import { getModel, } from "@mariozechner/pi-ai";
// Default transformer: Keep only LLM-compatible messages, strip app-specific fields
function defaultMessageTransformer(messages) {
return messages
.filter((m) => {
// Only keep standard LLM message roles
return m.role === "user" || m.role === "assistant" || m.role === "toolResult";
})
.map((m) => {
if (m.role === "user") {
// Strip attachments field (app-specific)
const { attachments, ...rest } = m;
return rest;
}
return m;
});
}
export class Agent {
constructor(opts) {
this._state = {
systemPrompt: "",
model: getModel("google", "gemini-2.5-flash-lite-preview-06-17"),
thinkingLevel: "off",
tools: [],
messages: [],
isStreaming: false,
streamMessage: null,
pendingToolCalls: new Set(),
error: undefined,
};
this.listeners = new Set();
this.messageQueue = [];
this._state = { ...this._state, ...opts.initialState };
this.debugListener = opts.debugListener;
this.transport = opts.transport;
this.messageTransformer = opts.messageTransformer || defaultMessageTransformer;
}
get state() {
return this._state;
}
subscribe(fn) {
this.listeners.add(fn);
fn({ type: "state-update", state: this._state });
return () => this.listeners.delete(fn);
}
// Mutators
setSystemPrompt(v) {
this.patch({ systemPrompt: v });
}
setModel(m) {
this.patch({ model: m });
}
setThinkingLevel(l) {
this.patch({ thinkingLevel: l });
}
setTools(t) {
this.patch({ tools: t });
}
replaceMessages(ms) {
this.patch({ messages: ms.slice() });
}
appendMessage(m) {
this.patch({ messages: [...this._state.messages, m] });
}
async queueMessage(m) {
// Transform message and queue it for injection at next turn
const transformed = await this.messageTransformer([m]);
this.messageQueue.push({
original: m,
llm: transformed[0], // undefined if filtered out
});
}
clearMessages() {
this.patch({ messages: [] });
}
abort() {
this.abortController?.abort();
}
logState(message) {
const { systemPrompt, model, messages } = this._state;
console.log(message, { systemPrompt, model, messages });
}
async prompt(input, attachments, opts) {
const model = this._state.model;
if (!model) {
this.emit({ type: "error-no-model" });
return;
}
// Build user message with attachments
const content = [{ type: "text", text: input }];
if (attachments?.length) {
for (const a of attachments) {
if (a.type === "image") {
content.push({ type: "image", data: a.content, mimeType: a.mimeType });
}
else if (a.type === "document" && a.extractedText) {
content.push({
type: "text",
text: `\n\n[Document: ${a.fileName}]\n${a.extractedText}`,
isDocument: true,
});
}
}
}
const userMessage = {
role: "user",
content,
attachments: attachments?.length ? attachments : undefined,
timestamp: Date.now(),
};
this.abortController = new AbortController();
this.patch({ isStreaming: true, streamMessage: null, error: undefined });
this.emit({ type: "started" });
const thinkingLevel = (opts?.thinkingOverride ?? this._state.thinkingLevel) ?? "off";
const reasoning = thinkingLevel === "off"
? undefined
: thinkingLevel === "minimal"
? "low"
: thinkingLevel;
const shouldSendOverride = opts?.transient === true;
const cfg = {
systemPrompt: this._state.systemPrompt,
tools: this._state.tools,
model,
reasoning,
thinkingOverride: shouldSendOverride ? thinkingLevel : undefined,
thinkingOnce: shouldSendOverride ? thinkingLevel : undefined,
getQueuedMessages: async () => {
// Return queued messages (they'll be added to state via message_end event)
const queued = this.messageQueue.slice();
this.messageQueue = [];
return queued;
},
};
try {
let partial = null;
let turnDebug = null;
let turnStart = 0;
this.logState("prompt started, current state:");
// Transform app messages to LLM-compatible messages (initial set)
const llmMessages = await this.messageTransformer(this._state.messages);
console.log("transformed messages:", llmMessages);
for await (const ev of this.transport.run(llmMessages, userMessage, cfg, this.abortController.signal)) {
switch (ev.type) {
case "turn_start": {
turnStart = performance.now();
// Build request context snapshot (use transformed messages)
const ctx = {
systemPrompt: this._state.systemPrompt,
messages: [...llmMessages],
tools: this._state.tools,
};
turnDebug = {
timestamp: new Date().toISOString(),
request: {
provider: cfg.model.provider,
model: cfg.model.id,
context: { ...ctx },
},
sseEvents: [],
};
break;
}
case "message_start":
case "message_update": {
partial = ev.message;
// Collect SSE-like events for debug (drop heavy partial)
if (ev.type === "message_update" && ev.assistantMessageEvent && turnDebug) {
const copy = { ...ev.assistantMessageEvent };
if (copy && "partial" in copy)
delete copy.partial;
turnDebug.sseEvents.push(JSON.stringify(copy));
if (!turnDebug.ttft)
turnDebug.ttft = performance.now() - turnStart;
}
this.patch({ streamMessage: ev.message });
break;
}
case "message_end": {
partial = null;
this.appendMessage(ev.message);
this.patch({ streamMessage: null });
if (turnDebug) {
if (ev.message.role !== "assistant" && ev.message.role !== "toolResult") {
turnDebug.request.context.messages.push(ev.message);
}
if (ev.message.role === "assistant")
turnDebug.response = ev.message;
}
break;
}
case "tool_execution_start": {
const s = new Set(this._state.pendingToolCalls);
s.add(ev.toolCallId);
this.patch({ pendingToolCalls: s });
break;
}
case "tool_execution_end": {
const s = new Set(this._state.pendingToolCalls);
s.delete(ev.toolCallId);
this.patch({ pendingToolCalls: s });
break;
}
case "turn_end": {
// finalize current turn
if (turnDebug) {
turnDebug.totalTime = performance.now() - turnStart;
this.debugListener?.(turnDebug);
turnDebug = null;
}
break;
}
case "agent_end": {
this.patch({ streamMessage: null });
break;
}
}
}
if (partial && partial.role === "assistant" && partial.content.length > 0) {
const onlyEmpty = !partial.content.some((c) => (c.type === "thinking" && c.thinking.trim().length > 0) ||
(c.type === "text" && c.text.trim().length > 0) ||
(c.type === "toolCall" && c.name.trim().length > 0));
if (!onlyEmpty) {
this.appendMessage(partial);
}
else {
if (this.abortController?.signal.aborted) {
throw new Error("Request was aborted");
}
}
}
}
catch (err) {
if (String(err?.message || err) === "no-api-key") {
this.emit({ type: "error-no-api-key", provider: model.provider });
}
else {
const msg = {
role: "assistant",
content: [{ type: "text", text: "" }],
api: model.api,
provider: model.provider,
model: model.id,
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
stopReason: this.abortController?.signal.aborted ? "aborted" : "error",
errorMessage: err?.message || String(err),
timestamp: Date.now(),
};
this.appendMessage(msg);
this.patch({ error: err?.message || String(err) });
}
}
finally {
this.patch({ isStreaming: false, streamMessage: null, pendingToolCalls: new Set() });
this.abortController = undefined;
this.emit({ type: "completed" });
}
this.logState("final state:");
}
patch(p) {
this._state = { ...this._state, ...p };
this.emit({ type: "state-update", state: this._state });
}
emit(e) {
for (const listener of this.listeners) {
listener(e);
}
}
}
//# sourceMappingURL=agent.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,11 +0,0 @@
import type { Message } from "@mariozechner/pi-ai";
import type { AgentRunConfig, AgentTransport } from "./types.js";
/**
* Transport that uses an app server with user authentication tokens.
* The server manages user accounts and proxies requests to LLM providers.
*/
export declare class AppTransport implements AgentTransport {
private readonly proxyUrl;
run(messages: Message[], userMessage: Message, cfg: AgentRunConfig, signal?: AbortSignal): AsyncGenerator<import("@mariozechner/pi-ai").AgentEvent, void, unknown>;
}
//# sourceMappingURL=AppTransport.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"AppTransport.d.ts","sourceRoot":"","sources":["../../../src/agent/transports/AppTransport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAOX,OAAO,EAKP,MAAM,qBAAqB,CAAC;AAO7B,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AA0SjE;;;GAGG;AACH,qBAAa,YAAa,YAAW,cAAc;IAElD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;IAErD,GAAG,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,WAAW;CAsC/F"}

View File

@@ -1,318 +0,0 @@
import { agentLoop } from "@mariozechner/pi-ai";
import { AssistantMessageEventStream } from "@mariozechner/pi-ai/dist/utils/event-stream.js";
import { parseStreamingJson } from "@mariozechner/pi-ai/dist/utils/json-parse.js";
import { clearAuthToken, getAuthToken } from "../../utils/auth-token.js";
import { i18n } from "../../utils/i18n.js";
/**
* Stream function that proxies through a server instead of calling providers directly.
* The server strips the partial field from delta events to reduce bandwidth.
* We reconstruct the partial message client-side.
*/
function streamSimpleProxy(model, context, options, proxyUrl) {
const stream = new AssistantMessageEventStream();
(async () => {
// Initialize the partial message that we'll build up from events
const partial = {
role: "assistant",
stopReason: "stop",
content: [],
api: model.api,
provider: model.provider,
model: model.id,
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
timestamp: Date.now(),
};
let reader;
// Set up abort handler to cancel the reader
const abortHandler = () => {
if (reader) {
reader.cancel("Request aborted by user").catch(() => { });
}
};
if (options.signal) {
options.signal.addEventListener("abort", abortHandler);
}
try {
const response = await fetch(`${proxyUrl}/api/stream`, {
method: "POST",
headers: {
Authorization: `Bearer ${options.authToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model,
context,
options: {
temperature: options.temperature,
maxTokens: options.maxTokens,
reasoning: options.reasoning,
// Don't send apiKey or signal - those are added server-side
},
}),
signal: options.signal,
});
if (!response.ok) {
let errorMessage = `Proxy error: ${response.status} ${response.statusText}`;
try {
const errorData = await response.json();
if (errorData.error) {
errorMessage = `Proxy error: ${errorData.error}`;
}
}
catch {
// Couldn't parse error response, use default message
}
throw new Error(errorMessage);
}
// Parse SSE stream
reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done)
break;
// Check if aborted after reading
if (options.signal?.aborted) {
throw new Error("Request aborted by user");
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6).trim();
if (data) {
const proxyEvent = JSON.parse(data);
let event;
// Handle different event types
// Server sends events with partial for non-delta events,
// and without partial for delta events
switch (proxyEvent.type) {
case "start":
event = { type: "start", partial };
break;
case "text_start":
partial.content[proxyEvent.contentIndex] = {
type: "text",
text: "",
};
event = { type: "text_start", contentIndex: proxyEvent.contentIndex, partial };
break;
case "text_delta": {
const content = partial.content[proxyEvent.contentIndex];
if (content?.type === "text") {
content.text += proxyEvent.delta;
event = {
type: "text_delta",
contentIndex: proxyEvent.contentIndex,
delta: proxyEvent.delta,
partial,
};
}
else {
throw new Error("Received text_delta for non-text content");
}
break;
}
case "text_end": {
const content = partial.content[proxyEvent.contentIndex];
if (content?.type === "text") {
content.textSignature = proxyEvent.contentSignature;
event = {
type: "text_end",
contentIndex: proxyEvent.contentIndex,
content: content.text,
partial,
};
}
else {
throw new Error("Received text_end for non-text content");
}
break;
}
case "thinking_start":
partial.content[proxyEvent.contentIndex] = {
type: "thinking",
thinking: "",
};
event = { type: "thinking_start", contentIndex: proxyEvent.contentIndex, partial };
break;
case "thinking_delta": {
const content = partial.content[proxyEvent.contentIndex];
if (content?.type === "thinking") {
content.thinking += proxyEvent.delta;
event = {
type: "thinking_delta",
contentIndex: proxyEvent.contentIndex,
delta: proxyEvent.delta,
partial,
};
}
else {
throw new Error("Received thinking_delta for non-thinking content");
}
break;
}
case "thinking_end": {
const content = partial.content[proxyEvent.contentIndex];
if (content?.type === "thinking") {
content.thinkingSignature = proxyEvent.contentSignature;
event = {
type: "thinking_end",
contentIndex: proxyEvent.contentIndex,
content: content.thinking,
partial,
};
}
else {
throw new Error("Received thinking_end for non-thinking content");
}
break;
}
case "toolcall_start":
partial.content[proxyEvent.contentIndex] = {
type: "toolCall",
id: proxyEvent.id,
name: proxyEvent.toolName,
arguments: {},
partialJson: "",
};
event = { type: "toolcall_start", contentIndex: proxyEvent.contentIndex, partial };
break;
case "toolcall_delta": {
const content = partial.content[proxyEvent.contentIndex];
if (content?.type === "toolCall") {
content.partialJson += proxyEvent.delta;
content.arguments = parseStreamingJson(content.partialJson) || {};
event = {
type: "toolcall_delta",
contentIndex: proxyEvent.contentIndex,
delta: proxyEvent.delta,
partial,
};
partial.content[proxyEvent.contentIndex] = { ...content }; // Trigger reactivity
}
else {
throw new Error("Received toolcall_delta for non-toolCall content");
}
break;
}
case "toolcall_end": {
const content = partial.content[proxyEvent.contentIndex];
if (content?.type === "toolCall") {
delete content.partialJson;
event = {
type: "toolcall_end",
contentIndex: proxyEvent.contentIndex,
toolCall: content,
partial,
};
}
break;
}
case "done":
partial.stopReason = proxyEvent.reason;
partial.usage = proxyEvent.usage;
event = { type: "done", reason: proxyEvent.reason, message: partial };
break;
case "error":
partial.stopReason = proxyEvent.reason;
partial.errorMessage = proxyEvent.errorMessage;
partial.usage = proxyEvent.usage;
event = { type: "error", reason: proxyEvent.reason, error: partial };
break;
default: {
// Exhaustive check
const _exhaustiveCheck = proxyEvent;
console.warn(`Unhandled event type: ${proxyEvent.type}`);
break;
}
}
// Push the event to stream
if (event) {
stream.push(event);
}
else {
throw new Error("Failed to create event from proxy event");
}
}
}
}
}
// Check if aborted after reading
if (options.signal?.aborted) {
throw new Error("Request aborted by user");
}
stream.end();
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.toLowerCase().includes("proxy") && errorMessage.includes("Unauthorized")) {
clearAuthToken();
}
partial.stopReason = options.signal?.aborted ? "aborted" : "error";
partial.errorMessage = errorMessage;
stream.push({
type: "error",
reason: partial.stopReason,
error: partial,
});
stream.end();
}
finally {
// Clean up abort handler
if (options.signal) {
options.signal.removeEventListener("abort", abortHandler);
}
}
})();
return stream;
}
// Proxy transport executes the turn using a remote proxy server
/**
* Transport that uses an app server with user authentication tokens.
* The server manages user accounts and proxies requests to LLM providers.
*/
export class AppTransport {
constructor() {
// Hardcoded proxy URL for now - will be made configurable later
this.proxyUrl = "https://genai.mariozechner.at";
}
async *run(messages, userMessage, cfg, signal) {
const authToken = await getAuthToken();
if (!authToken) {
throw new Error(i18n("Auth token is required for proxy transport"));
}
// Use proxy - no local API key needed
const streamFn = (model, context, options) => {
return streamSimpleProxy(model, context, {
...options,
authToken,
}, this.proxyUrl);
};
// Messages are already LLM-compatible (filtered by Agent)
const context = {
systemPrompt: cfg.systemPrompt,
messages,
tools: cfg.tools,
};
const pc = {
model: cfg.model,
reasoning: cfg.reasoning,
getQueuedMessages: cfg.getQueuedMessages,
};
// Yield events from the upstream agentLoop iterator
// Pass streamFn as the 5th parameter to use proxy
for await (const ev of agentLoop(userMessage, context, pc, signal, streamFn)) {
yield ev;
}
}
}
//# sourceMappingURL=AppTransport.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,10 +0,0 @@
import { type Message } from "@mariozechner/pi-ai";
import type { AgentRunConfig, AgentTransport } from "./types.js";
/**
* Transport that calls LLM providers directly.
* Uses CORS proxy only for providers that require it (Anthropic OAuth, Z-AI).
*/
export declare class ProviderTransport implements AgentTransport {
run(messages: Message[], userMessage: Message, cfg: AgentRunConfig, signal?: AbortSignal): AsyncGenerator<import("@mariozechner/pi-ai").AgentEvent, void, unknown>;
}
//# sourceMappingURL=ProviderTransport.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"ProviderTransport.d.ts","sourceRoot":"","sources":["../../../src/agent/transports/ProviderTransport.ts"],"names":[],"mappings":"AAAA,OAAO,EAIN,KAAK,OAAO,EAEZ,MAAM,qBAAqB,CAAC;AAG7B,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjE;;;GAGG;AACH,qBAAa,iBAAkB,YAAW,cAAc;IAChD,GAAG,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,WAAW;CAiC/F"}

View File

@@ -1,38 +0,0 @@
import { agentLoop, } from "@mariozechner/pi-ai";
import { getAppStorage } from "../../storage/app-storage.js";
import { applyProxyIfNeeded } from "../../utils/proxy-utils.js";
/**
* Transport that calls LLM providers directly.
* Uses CORS proxy only for providers that require it (Anthropic OAuth, Z-AI).
*/
export class ProviderTransport {
async *run(messages, userMessage, cfg, signal) {
// Get API key from storage
const apiKey = await getAppStorage().providerKeys.get(cfg.model.provider);
if (!apiKey) {
throw new Error("no-api-key");
}
// Get proxy URL from settings (if available)
const proxyEnabled = await getAppStorage().settings.get("proxy.enabled");
const proxyUrl = await getAppStorage().settings.get("proxy.url");
// Apply proxy only if this provider/key combination requires it
const model = applyProxyIfNeeded(cfg.model, apiKey, proxyEnabled ? proxyUrl || undefined : undefined);
// Messages are already LLM-compatible (filtered by Agent)
const context = {
systemPrompt: cfg.systemPrompt,
messages,
tools: cfg.tools,
};
const pc = {
model,
reasoning: cfg.reasoning,
apiKey,
getQueuedMessages: cfg.getQueuedMessages,
};
// Yield events from agentLoop
for await (const ev of agentLoop(userMessage, context, pc, signal)) {
yield ev;
}
}
}
//# sourceMappingURL=ProviderTransport.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"ProviderTransport.js","sourceRoot":"","sources":["../../../src/agent/transports/ProviderTransport.ts"],"names":[],"mappings":"AAAA,OAAO,EAGN,SAAS,GAGT,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAGhE;;;GAGG;AACH,MAAM,OAAO,iBAAiB;IAC7B,KAAK,CAAC,CAAC,GAAG,CAAC,QAAmB,EAAE,WAAoB,EAAE,GAAmB,EAAE,MAAoB;QAC9F,2BAA2B;QAC3B,MAAM,MAAM,GAAG,MAAM,aAAa,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC1E,IAAI,CAAC,MAAM,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC;QAC/B,CAAC;QAED,6CAA6C;QAC7C,MAAM,YAAY,GAAG,MAAM,aAAa,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAU,eAAe,CAAC,CAAC;QAClF,MAAM,QAAQ,GAAG,MAAM,aAAa,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAS,WAAW,CAAC,CAAC;QAEzE,gEAAgE;QAChE,MAAM,KAAK,GAAG,kBAAkB,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,QAAQ,IAAI,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QAEtG,0DAA0D;QAC1D,MAAM,OAAO,GAAiB;YAC7B,YAAY,EAAE,GAAG,CAAC,YAAY;YAC9B,QAAQ;YACR,KAAK,EAAE,GAAG,CAAC,KAAK;SAChB,CAAC;QAEF,MAAM,EAAE,GAAoB;YAC3B,KAAK;YACL,SAAS,EAAE,GAAG,CAAC,SAAS;YACxB,MAAM;YACN,iBAAiB,EAAE,GAAG,CAAC,iBAAiB;SACxC,CAAC;QAEF,8BAA8B;QAC9B,IAAI,KAAK,EAAE,MAAM,EAAE,IAAI,SAAS,CAAC,WAAqC,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC;YAC9F,MAAM,EAAE,CAAC;QACV,CAAC;IACF,CAAC;CACD"}

View File

@@ -1,4 +0,0 @@
export * from "./AppTransport.js";
export * from "./ProviderTransport.js";
export * from "./types.js";
//# sourceMappingURL=index.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/agent/transports/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,wBAAwB,CAAC;AACvC,cAAc,YAAY,CAAC"}

View File

@@ -1,4 +0,0 @@
export * from "./AppTransport.js";
export * from "./ProviderTransport.js";
export * from "./types.js";
//# sourceMappingURL=index.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/agent/transports/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,wBAAwB,CAAC;AACvC,cAAc,YAAY,CAAC"}

View File

@@ -1,48 +0,0 @@
import type { StopReason, Usage } from "@mariozechner/pi-ai";
export type ProxyAssistantMessageEvent = {
type: "start";
} | {
type: "text_start";
contentIndex: number;
} | {
type: "text_delta";
contentIndex: number;
delta: string;
} | {
type: "text_end";
contentIndex: number;
contentSignature?: string;
} | {
type: "thinking_start";
contentIndex: number;
} | {
type: "thinking_delta";
contentIndex: number;
delta: string;
} | {
type: "thinking_end";
contentIndex: number;
contentSignature?: string;
} | {
type: "toolcall_start";
contentIndex: number;
id: string;
toolName: string;
} | {
type: "toolcall_delta";
contentIndex: number;
delta: string;
} | {
type: "toolcall_end";
contentIndex: number;
} | {
type: "done";
reason: Extract<StopReason, "stop" | "length" | "toolUse">;
usage: Usage;
} | {
type: "error";
reason: Extract<StopReason, "aborted" | "error">;
errorMessage: string;
usage: Usage;
};
//# sourceMappingURL=proxy-types.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"proxy-types.d.ts","sourceRoot":"","sources":["../../../src/agent/transports/proxy-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAE7D,MAAM,MAAM,0BAA0B,GACnC;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC3D;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAE,GACrE;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/D;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAE,GACzE;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC9E;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC/D;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GAC1F;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC,UAAU,EAAE,SAAS,GAAG,OAAO,CAAC,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC"}

View File

@@ -1,2 +0,0 @@
export {};
//# sourceMappingURL=proxy-types.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"proxy-types.js","sourceRoot":"","sources":["../../../src/agent/transports/proxy-types.ts"],"names":[],"mappings":""}

View File

@@ -1,12 +0,0 @@
import type { AgentEvent, AgentTool, Message, Model, QueuedMessage } from "@mariozechner/pi-ai";
export interface AgentRunConfig {
systemPrompt: string;
tools: AgentTool<any>[];
model: Model<any>;
reasoning?: "low" | "medium" | "high";
getQueuedMessages?: <T>() => Promise<QueuedMessage<T>[]>;
}
export interface AgentTransport {
run(messages: Message[], userMessage: Message, config: AgentRunConfig, signal?: AbortSignal): AsyncIterable<AgentEvent>;
}
//# sourceMappingURL=types.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/agent/transports/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGhG,MAAM,WAAW,cAAc;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC;IACxB,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,SAAS,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACtC,iBAAiB,CAAC,EAAE,CAAC,CAAC,OAAO,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;CACzD;AAKD,MAAM,WAAW,cAAc;IAC9B,GAAG,CACF,QAAQ,EAAE,OAAO,EAAE,EACnB,WAAW,EAAE,OAAO,EACpB,MAAM,EAAE,cAAc,EACtB,MAAM,CAAC,EAAE,WAAW,GAClB,aAAa,CAAC,UAAU,CAAC,CAAC;CAC7B"}

View File

@@ -1,2 +0,0 @@
export {};
//# sourceMappingURL=types.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/agent/transports/types.ts"],"names":[],"mappings":""}

View File

@@ -1,15 +0,0 @@
import type { AssistantMessage, Context } from "@mariozechner/pi-ai";
export interface DebugLogEntry {
timestamp: string;
request: {
provider: string;
model: string;
context: Context;
};
response?: AssistantMessage;
error?: unknown;
sseEvents: string[];
ttft?: number;
totalTime?: number;
}
//# sourceMappingURL=types.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/agent/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAErE,MAAM,WAAW,aAAa;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAC/D,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB"}

View File

@@ -1,2 +0,0 @@
export {};
//# sourceMappingURL=types.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/agent/types.ts"],"names":[],"mappings":""}

File diff suppressed because one or more lines are too long

View File

@@ -1,459 +0,0 @@
// Bundled entry point for the macOS WKWebView web chat.
// New version: talks directly to the Gateway WebSocket (chat.* methods), no /rpc or file watchers.
/* global window, document */
if (!globalThis.process) {
globalThis.process = { env: {} };
}
import { formatError } from "./format-error.js";
const logStatus = (msg) => {
try {
console.log(msg);
const el = document.getElementById("app");
// Keep the animated boot loader visible; don't overwrite it with status text.
if (el && !el.dataset.booted) el.dataset.status = msg;
} catch {
// Ignore logging failures—never block bootstrap.
}
};
const randomId = () => {
if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID();
return `id-${Math.random().toString(16).slice(2)}-${Date.now()}`;
};
const ensureErrorStyles = () => {
if (document.getElementById("webchat-error-style")) return;
const style = document.createElement("style");
style.id = "webchat-error-style";
style.textContent = `
body.webchat-error {
padding: 28px;
}
`;
document.head.appendChild(style);
};
class GatewaySocket {
constructor(url) {
this.url = url;
this.ws = null;
this.pending = new Map();
this.handlers = new Map();
this.connectPromise = null;
this.reconnectTimer = null;
this.retryMs = 500;
this.maxRetryMs = 10_000;
this.hello = null;
}
async connect() {
if (this.ws && this.ws.readyState === WebSocket.OPEN && this.hello) {
return this.hello;
}
if (this.connectPromise) return this.connectPromise;
this.connectPromise = new Promise((resolve, reject) => {
let settled = false;
const settle = (err, value) => {
if (settled) return;
settled = true;
this.connectPromise = null;
if (err) reject(err);
else resolve(value);
};
const ws = new WebSocket(this.url);
this.ws = ws;
ws.onopen = () => {
const id = randomId();
logStatus(`ws: open -> sending connect (${this.url})`);
const params = {
minProtocol: 2,
maxProtocol: 2,
client: {
name: "webchat-ui",
version: "dev",
platform: "browser",
mode: "webchat",
instanceId: randomId(),
},
caps: [],
};
ws.send(JSON.stringify({ type: "req", id, method: "connect", params }));
this.pending.set(id, {
resolve: (value) => settle(null, value),
reject: (err) => settle(err),
_handshake: true,
});
};
ws.onerror = (err) => {
logStatus(`ws: error ${formatError(err)}`);
settle(err);
};
ws.onclose = (ev) => {
logStatus(
`ws: close code=${ev.code} reason=${ev.reason || "n/a"} clean=${ev.wasClean}`,
);
if (this.ws === ws) {
this.ws = null;
this.hello = null;
}
if (this.pending.size > 0) {
for (const [, p] of this.pending)
p.reject(new Error("gateway closed"));
this.pending.clear();
}
if (ev.code !== 1000) {
settle(new Error(`gateway closed ${ev.code}`));
} else {
settle(new Error("gateway closed"));
}
this.scheduleReconnect();
};
ws.onmessage = (ev) => {
let msg;
try {
msg = JSON.parse(ev.data);
} catch {
return;
}
if (msg.type === "event") {
const cb = this.handlers.get(msg.event);
if (cb) cb(msg.payload, msg);
return;
}
if (msg.type === "res") {
const pending = this.pending.get(msg.id);
if (!pending) return;
this.pending.delete(msg.id);
if (msg.ok) {
if (pending._handshake) {
const helloOk = msg.payload;
logStatus(
`ws: hello-ok presence=${helloOk?.snapshot?.presence?.length ?? 0} healthOk=${helloOk?.snapshot?.health?.ok ?? "n/a"}`,
);
this.handlers.set("snapshot", helloOk.snapshot);
this.hello = helloOk;
this.retryMs = 500;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
pending.resolve(helloOk);
} else {
pending.resolve(msg.payload);
}
} else {
pending.reject(new Error(msg.error?.message || "gateway error"));
}
}
};
});
return this.connectPromise;
}
on(event, handler) {
this.handlers.set(event, handler);
}
scheduleReconnect() {
if (this.reconnectTimer) return;
const delay = this.retryMs;
this.retryMs = Math.min(this.retryMs * 2, this.maxRetryMs);
logStatus(`ws: reconnect in ${delay}ms`);
this.reconnectTimer = setTimeout(async () => {
this.reconnectTimer = null;
try {
await this.connect();
} catch {
this.scheduleReconnect();
}
}, delay);
}
async ensureConnected() {
if (this.ws && this.ws.readyState === WebSocket.OPEN) return;
try {
await this.connect();
} catch (err) {
logStatus(`ws: connect failed ${formatError(err)}`);
this.scheduleReconnect();
throw new Error("gateway not connected");
}
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
this.scheduleReconnect();
throw new Error("gateway not connected");
}
}
async request(method, params, { timeoutMs = 30_000 } = {}) {
let lastErr = null;
for (let attempt = 0; attempt < 2; attempt++) {
try {
await this.ensureConnected();
const ws = this.ws;
if (!ws || ws.readyState !== WebSocket.OPEN) {
throw new Error("gateway not connected");
}
const id = randomId();
const frame = { type: "req", id, method, params };
ws.send(JSON.stringify(frame));
return await new Promise((resolve, reject) => {
this.pending.set(id, { resolve, reject });
setTimeout(() => {
if (this.pending.has(id)) {
this.pending.delete(id);
reject(new Error(`${method} timed out`));
}
}, timeoutMs);
});
} catch (err) {
lastErr = err;
this.scheduleReconnect();
}
}
throw lastErr instanceof Error ? lastErr : new Error("gateway not connected");
}
}
class ChatTransport {
constructor(sessionKey, gateway, healthOkRef) {
this.sessionKey = sessionKey;
this.gateway = gateway;
this.healthOkRef = healthOkRef;
this.pendingRuns = new Map();
this.gateway.on("chat", (payload) => {
const runId = payload?.runId;
const pending = runId ? this.pendingRuns.get(runId) : null;
if (!pending) return;
if (payload.state === "error") {
pending.reject(new Error(payload.errorMessage || "chat error"));
this.pendingRuns.delete(runId);
return;
}
if (payload.state === "delta") return; // ignore partials for now
pending.resolve(payload);
this.pendingRuns.delete(runId);
});
}
async *run(_messages, userMessage, cfg, _signal) {
if (!this.healthOkRef.current) {
throw new Error("gateway health not OK; cannot send");
}
const text = userMessage.content?.[0]?.text ?? "";
const attachments = (userMessage.attachments || []).map((a) => ({
type: a.type,
mimeType: a.mimeType,
fileName: a.fileName,
content:
typeof a.content === "string"
? a.content
: btoa(String.fromCharCode(...new Uint8Array(a.content))),
}));
const thinking =
cfg?.thinkingOnce ?? cfg?.thinkingOverride ?? cfg?.thinking ?? undefined;
const runId = randomId();
const pending = new Promise((resolve, reject) => {
this.pendingRuns.set(runId, { resolve, reject });
setTimeout(() => {
if (this.pendingRuns.has(runId)) {
this.pendingRuns.delete(runId);
reject(new Error("chat timed out"));
}
}, 30_000);
});
await this.gateway.request("chat.send", {
sessionKey: this.sessionKey,
message: text,
attachments: attachments.length ? attachments : undefined,
thinking,
idempotencyKey: runId,
timeoutMs: 30_000,
});
yield { type: "turn_start" };
const payload = await pending;
const message = payload?.message || {
role: "assistant",
content: [{ type: "text", text: "" }],
timestamp: Date.now(),
};
yield { type: "message_start", message };
yield { type: "message_end", message };
yield { type: "turn_end" };
yield { type: "agent_end" };
}
}
const startChat = async () => {
logStatus("boot: starting imports");
const { Agent } = await import("./agent/agent.js");
const { ChatPanel } = await import("./ChatPanel.js");
const { AppStorage, setAppStorage } = await import(
"./storage/app-storage.js"
);
const { SettingsStore } = await import("./storage/stores/settings-store.js");
const { ProviderKeysStore } = await import(
"./storage/stores/provider-keys-store.js"
);
const { SessionsStore } = await import("./storage/stores/sessions-store.js");
const { CustomProvidersStore } = await import(
"./storage/stores/custom-providers-store.js"
);
const { IndexedDBStorageBackend } = await import(
"./storage/backends/indexeddb-storage-backend.js"
);
const { getModel } = await import("@mariozechner/pi-ai");
logStatus("boot: modules loaded");
// Storage init
const backend = new IndexedDBStorageBackend({
dbName: "clawdis-webchat",
version: 1,
stores: [
new SettingsStore().getConfig(),
new ProviderKeysStore().getConfig(),
new SessionsStore().getConfig(),
SessionsStore.getMetadataConfig(),
new CustomProvidersStore().getConfig(),
],
});
const settingsStore = new SettingsStore();
const providerKeysStore = new ProviderKeysStore();
const sessionsStore = new SessionsStore();
const customProvidersStore = new CustomProvidersStore();
for (const store of [
settingsStore,
providerKeysStore,
sessionsStore,
customProvidersStore,
]) {
store.setBackend(backend);
}
const storage = new AppStorage(
settingsStore,
providerKeysStore,
sessionsStore,
customProvidersStore,
backend,
);
setAppStorage(storage);
// Seed dummy API key
try {
await providerKeysStore.set("anthropic", "embedded");
} catch (err) {
logStatus(`storage warn: could not seed provider key: ${err}`);
}
// Gateway WS
const params = new URLSearchParams(window.location.search);
const sessionKey = params.get("session") || "main";
const wsUrl = (() => {
const loc = new URL(window.location.href);
const requestedPort = Number.parseInt(
params.get("gatewayPort") ?? "",
10,
);
const gatewayPort =
Number.isInteger(requestedPort) &&
requestedPort > 0 &&
requestedPort <= 65_535
? requestedPort
: 18_789;
const gatewayHost = params.get("gatewayHost") || loc.hostname || "127.0.0.1";
const u = new URL(`ws://${gatewayHost}:${gatewayPort}/`);
return u.toString();
})();
logStatus(`boot: connecting gateway (${wsUrl})`);
const gateway = new GatewaySocket(wsUrl);
const hello = await gateway.connect();
const healthOkRef = { current: Boolean(hello?.snapshot?.health?.ok ?? true) };
// Update health on demand when we get tick; simplest is to poll health occasionally.
gateway.on("tick", async () => {
try {
const health = await gateway.request("health", {}, { timeoutMs: 5_000 });
healthOkRef.current = !!health?.ok;
} catch {
healthOkRef.current = false;
}
});
logStatus("boot: fetching history");
const history = await gateway.request("chat.history", { sessionKey });
const initialMessages = Array.isArray(history?.messages)
? history.messages
: [];
const thinkingLevel =
typeof history?.thinkingLevel === "string" ? history.thinkingLevel : "off";
const agent = new Agent({
initialState: {
systemPrompt: "You are Clawd (primary session).",
model: getModel("anthropic", "claude-opus-4-5"),
thinkingLevel,
messages: initialMessages,
},
transport: new ChatTransport(sessionKey, gateway, healthOkRef),
});
const origPrompt = agent.prompt.bind(agent);
agent.prompt = async (input, attachments) => {
const userMessage = {
role: "user",
content: [{ type: "text", text: input }],
attachments: attachments?.length ? attachments : undefined,
timestamp: Date.now(),
};
agent.appendMessage(userMessage);
return origPrompt(input, attachments);
};
const panel = new ChatPanel();
panel.style.height = "100%";
panel.style.display = "block";
await panel.setAgent(agent, { sessionThinkingLevel: thinkingLevel });
const mount = document.getElementById("app");
if (!mount) throw new Error("#app container missing");
mount.dataset.booted = "1";
mount.textContent = "";
mount.appendChild(panel);
logStatus("boot: ready");
};
startChat().catch((err) => {
const msg = formatError(err);
logStatus(`boot failed: ${msg}`);
document.body.dataset.webchatError = "1";
ensureErrorStyles();
document.body.classList.add("webchat-error");
document.body.style.color = "#b32d2d";
document.body.style.fontFamily = "SFMono-Regular, Menlo, Consolas, monospace";
document.body.style.padding = "28px";
document.body.style.lineHeight = "1.5";
document.body.style.whiteSpace = "pre-wrap";
document.body.innerText = "Web chat failed to connect.\n\n" + msg;
});

View File

@@ -1,39 +0,0 @@
import { LitElement } from "lit";
import "./MessageEditor.js";
import "./MessageList.js";
import "./Messages.js";
import type { Agent } from "../agent/agent.js";
import "./StreamingMessageContainer.js";
import type { Attachment } from "../utils/attachment-utils.js";
export declare class AgentInterface extends LitElement {
session?: Agent;
enableAttachments: boolean;
enableModelSelector: boolean;
enableThinkingSelector: boolean;
showThemeToggle: boolean;
onApiKeyRequired?: (provider: string) => Promise<boolean>;
onBeforeSend?: () => void | Promise<void>;
onBeforeToolCall?: (toolName: string, args: any) => boolean | Promise<boolean>;
onCostClick?: () => void;
private _messageEditor;
private _streamingContainer;
private _autoScroll;
private _lastScrollTop;
private _lastClientHeight;
private _scrollContainer?;
private _resizeObserver?;
private _unsubscribeSession?;
setInput(text: string, attachments?: Attachment[]): void;
setAutoScroll(enabled: boolean): void;
protected createRenderRoot(): HTMLElement | DocumentFragment;
willUpdate(changedProperties: Map<string, any>): void;
connectedCallback(): Promise<void>;
disconnectedCallback(): void;
private setupSessionSubscription;
private _handleScroll;
sendMessage(input: string, attachments?: Attachment[]): Promise<void>;
private renderMessages;
private renderStats;
render(): import("lit-html").TemplateResult<1>;
}
//# sourceMappingURL=AgentInterface.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"AgentInterface.d.ts","sourceRoot":"","sources":["../../src/components/AgentInterface.ts"],"names":[],"mappings":"AACA,OAAO,EAAQ,UAAU,EAAE,MAAM,KAAK,CAAC;AAIvC,OAAO,oBAAoB,CAAC;AAC5B,OAAO,kBAAkB,CAAC;AAC1B,OAAO,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,KAAK,EAAc,MAAM,mBAAmB,CAAC;AAE3D,OAAO,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAK/D,qBACa,cAAe,SAAQ,UAAU;IAEb,OAAO,CAAC,EAAE,KAAK,CAAC;IACnB,iBAAiB,UAAQ;IACzB,mBAAmB,UAAQ;IAC3B,sBAAsB,UAAQ;IAC9B,eAAe,UAAS;IAErB,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAE1D,YAAY,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1C,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE/E,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IAGhC,OAAO,CAAC,cAAc,CAAiB;IAC1B,OAAO,CAAC,mBAAmB,CAA6B;IAE9F,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,gBAAgB,CAAC,CAAc;IACvC,OAAO,CAAC,eAAe,CAAC,CAAiB;IACzC,OAAO,CAAC,mBAAmB,CAAC,CAAa;IAElC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,UAAU,EAAE;IAWjD,aAAa,CAAC,OAAO,EAAE,OAAO;cAIlB,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAI5D,UAAU,CAAC,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC;IASxC,iBAAiB;IAkCvB,oBAAoB;IAmB7B,OAAO,CAAC,wBAAwB;IAqBhC,OAAO,CAAC,aAAa,CAwBnB;IAEW,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,UAAU,EAAE;IAsClE,OAAO,CAAC,cAAc;IAmCtB,OAAO,CAAC,WAAW;IAgDV,MAAM;CA4Cf"}

View File

@@ -1,359 +0,0 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import { ModelSelector } from "../dialogs/ModelSelector.js";
import "./MessageEditor.js";
import "./MessageList.js";
import "./Messages.js"; // Import for side effects to register the custom elements
import { getAppStorage } from "../storage/app-storage.js";
import "./StreamingMessageContainer.js";
import { formatUsage } from "../utils/format.js";
import { i18n } from "../utils/i18n.js";
let AgentInterface = class AgentInterface extends LitElement {
constructor() {
super(...arguments);
this.enableAttachments = true;
this.enableModelSelector = true;
this.enableThinkingSelector = true;
this.showThemeToggle = false;
this.sessionThinkingLevel = "off";
this.pendingThinkingLevel = null;
this._autoScroll = true;
this._lastScrollTop = 0;
this._lastClientHeight = 0;
this._handleScroll = (_ev) => {
if (!this._scrollContainer)
return;
const currentScrollTop = this._scrollContainer.scrollTop;
const scrollHeight = this._scrollContainer.scrollHeight;
const clientHeight = this._scrollContainer.clientHeight;
const distanceFromBottom = scrollHeight - currentScrollTop - clientHeight;
// Ignore relayout due to message editor getting pushed up by stats
if (clientHeight < this._lastClientHeight) {
this._lastClientHeight = clientHeight;
return;
}
// Only disable auto-scroll if user scrolled UP or is far from bottom
if (currentScrollTop !== 0 && currentScrollTop < this._lastScrollTop && distanceFromBottom > 50) {
this._autoScroll = false;
}
else if (distanceFromBottom < 10) {
// Re-enable if very close to bottom
this._autoScroll = true;
}
this._lastScrollTop = currentScrollTop;
this._lastClientHeight = clientHeight;
};
}
setInput(text, attachments) {
const update = () => {
if (!this._messageEditor)
requestAnimationFrame(update);
else {
this._messageEditor.value = text;
this._messageEditor.attachments = attachments || [];
}
};
update();
}
setAutoScroll(enabled) {
this._autoScroll = enabled;
}
createRenderRoot() {
return this;
}
willUpdate(changedProperties) {
super.willUpdate(changedProperties);
// Re-subscribe when session property changes
if (changedProperties.has("session")) {
this.setupSessionSubscription();
}
}
async connectedCallback() {
super.connectedCallback();
this.style.display = "flex";
this.style.flexDirection = "column";
this.style.height = "100%";
this.style.minHeight = "0";
// Wait for first render to get scroll container
await this.updateComplete;
this._scrollContainer = this.querySelector(".overflow-y-auto");
if (this._scrollContainer) {
// Set up ResizeObserver to detect content changes
this._resizeObserver = new ResizeObserver(() => {
if (this._autoScroll && this._scrollContainer) {
this._scrollContainer.scrollTop = this._scrollContainer.scrollHeight;
}
});
// Observe the content container inside the scroll container
const contentContainer = this._scrollContainer.querySelector(".max-w-3xl");
if (contentContainer) {
this._resizeObserver.observe(contentContainer);
}
// Set up scroll listener with better detection
this._scrollContainer.addEventListener("scroll", this._handleScroll);
}
// Subscribe to external session if provided
this.setupSessionSubscription();
}
disconnectedCallback() {
super.disconnectedCallback();
// Clean up observers and listeners
if (this._resizeObserver) {
this._resizeObserver.disconnect();
this._resizeObserver = undefined;
}
if (this._scrollContainer) {
this._scrollContainer.removeEventListener("scroll", this._handleScroll);
}
if (this._unsubscribeSession) {
this._unsubscribeSession();
this._unsubscribeSession = undefined;
}
}
setupSessionSubscription() {
if (this._unsubscribeSession) {
this._unsubscribeSession();
this._unsubscribeSession = undefined;
}
if (!this.session)
return;
this._unsubscribeSession = this.session.subscribe(async (ev) => {
if (ev.type === "state-update") {
if (this.pendingThinkingLevel === null && ev.state.thinkingLevel) {
this.sessionThinkingLevel = ev.state.thinkingLevel;
}
if (this._streamingContainer) {
this._streamingContainer.isStreaming = ev.state.isStreaming;
this._streamingContainer.setMessage(ev.state.streamMessage, !ev.state.isStreaming);
}
this.requestUpdate();
}
else if (ev.type === "error-no-model") {
// TODO show some UI feedback
}
else if (ev.type === "error-no-api-key") {
// Handled by onApiKeyRequired callback
}
});
}
async sendMessage(input, attachments) {
if ((!input.trim() && attachments?.length === 0) || this.session?.state.isStreaming)
return;
const session = this.session;
if (!session)
throw new Error("No session set on AgentInterface");
if (!session.state.model)
throw new Error("No model set on AgentInterface");
// Check if API key exists for the provider (only needed in direct mode)
const provider = session.state.model.provider;
const apiKey = await getAppStorage().providerKeys.get(provider);
// If no API key, prompt for it
if (!apiKey) {
if (!this.onApiKeyRequired) {
console.error("No API key configured and no onApiKeyRequired handler set");
return;
}
const success = await this.onApiKeyRequired(provider);
// If still no API key, abort the send
if (!success) {
return;
}
}
// Call onBeforeSend hook before sending
if (this.onBeforeSend) {
await this.onBeforeSend();
}
const baseThinking =
this.sessionThinkingLevel || session.state.thinkingLevel || "off";
const thinkingOverride = this.pendingThinkingLevel ?? baseThinking;
const transient =
this.pendingThinkingLevel !== null &&
this.pendingThinkingLevel !== baseThinking;
// Only clear editor after we know we can send
this._messageEditor.value = "";
this._messageEditor.attachments = [];
this._autoScroll = true; // Enable auto-scroll when sending a message
await this.session?.prompt(input, attachments, {
thinkingOverride,
transient,
});
this.pendingThinkingLevel = null;
// Reset editor thinking selector to session baseline
if (this._messageEditor) {
this._messageEditor.thinkingLevel = this.sessionThinkingLevel || "off";
}
}
renderMessages() {
if (!this.session)
return html `<div class="p-4 text-center text-muted-foreground">${i18n("No session available")}</div>`;
const state = this.session.state;
// Build a map of tool results to allow inline rendering in assistant messages
const toolResultsById = new Map();
for (const message of state.messages) {
if (message.role === "toolResult") {
toolResultsById.set(message.toolCallId, message);
}
}
return html `
<div class="flex flex-col gap-3">
<!-- Stable messages list - won't re-render during streaming -->
<message-list
.messages=${this.session.state.messages}
.tools=${state.tools}
.pendingToolCalls=${this.session ? this.session.state.pendingToolCalls : new Set()}
.isStreaming=${state.isStreaming}
.onCostClick=${this.onCostClick}
></message-list>
<!-- Streaming message container - manages its own updates -->
<streaming-message-container
class="${state.isStreaming ? "" : "hidden"}"
.tools=${state.tools}
.isStreaming=${state.isStreaming}
.pendingToolCalls=${state.pendingToolCalls}
.toolResultsById=${toolResultsById}
.onCostClick=${this.onCostClick}
></streaming-message-container>
</div>
`;
}
renderStats() {
if (!this.session)
return html `<div class="text-xs h-5"></div>`;
const state = this.session.state;
const totals = state.messages
.filter((m) => m.role === "assistant")
.reduce((acc, msg) => {
const usage = msg.usage;
if (usage) {
acc.input += usage.input;
acc.output += usage.output;
acc.cacheRead += usage.cacheRead;
acc.cacheWrite += usage.cacheWrite;
acc.cost.total += usage.cost.total;
}
return acc;
}, {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
});
const hasTotals = totals.input || totals.output || totals.cacheRead || totals.cacheWrite;
const totalsText = hasTotals ? formatUsage(totals) : "";
return html `
<div class="text-xs text-muted-foreground flex justify-between items-center h-5">
<div class="flex items-center gap-1">
${this.showThemeToggle ? html `<theme-toggle></theme-toggle>` : html ``}
</div>
<div class="flex ml-auto items-center gap-3">
${totalsText
? this.onCostClick
? html `<span class="cursor-pointer hover:text-foreground transition-colors" @click=${this.onCostClick}>${totalsText}</span>`
: html `<span>${totalsText}</span>`
: ""}
</div>
</div>
`;
}
render() {
if (!this.session)
return html `<div class="p-4 text-center text-muted-foreground">${i18n("No session set")}</div>`;
const session = this.session;
const state = this.session.state;
return html `
<div class="flex flex-col h-full bg-background text-foreground">
<!-- Messages Area -->
<div class="flex-1 overflow-y-auto">
<div class="max-w-3xl mx-auto p-4 pb-0">${this.renderMessages()}</div>
</div>
<!-- Input Area -->
<div class="shrink-0">
<div class="max-w-3xl mx-auto px-2">
<message-editor
.isStreaming=${state.isStreaming}
.currentModel=${state.model}
.thinkingLevel=${this.pendingThinkingLevel ?? this.sessionThinkingLevel ?? state.thinkingLevel}
.showAttachmentButton=${this.enableAttachments}
.showModelSelector=${this.enableModelSelector}
.showThinkingSelector=${this.enableThinkingSelector}
.onSend=${(input, attachments) => {
this.sendMessage(input, attachments);
}}
.onAbort=${() => session.abort()}
.onModelSelect=${() => {
ModelSelector.open(state.model, (model) => session.setModel(model));
}}
.onThinkingChange=${this.enableThinkingSelector
? (level) => {
this.pendingThinkingLevel = level;
if (this._messageEditor) {
this._messageEditor.thinkingLevel = level;
}
this.requestUpdate();
}
: undefined}
></message-editor>
${this.renderStats()}
</div>
</div>
</div>
`;
}
};
__decorate([
property({ attribute: false })
], AgentInterface.prototype, "session", void 0);
__decorate([
property({ type: Boolean })
], AgentInterface.prototype, "enableAttachments", void 0);
__decorate([
property({ type: Boolean })
], AgentInterface.prototype, "enableModelSelector", void 0);
__decorate([
property({ type: Boolean })
], AgentInterface.prototype, "enableThinkingSelector", void 0);
__decorate([
property({ type: String })
], AgentInterface.prototype, "sessionThinkingLevel", void 0);
__decorate([
property({ type: Boolean })
], AgentInterface.prototype, "showThemeToggle", void 0);
__decorate([
property({ attribute: false })
], AgentInterface.prototype, "onApiKeyRequired", void 0);
__decorate([
property({ attribute: false })
], AgentInterface.prototype, "onBeforeSend", void 0);
__decorate([
property({ attribute: false })
], AgentInterface.prototype, "onBeforeToolCall", void 0);
__decorate([
property({ attribute: false })
], AgentInterface.prototype, "onCostClick", void 0);
__decorate([
query("message-editor")
], AgentInterface.prototype, "_messageEditor", void 0);
__decorate([
query("streaming-message-container")
], AgentInterface.prototype, "_streamingContainer", void 0);
__decorate([
state()
], AgentInterface.prototype, "pendingThinkingLevel", void 0);
AgentInterface = __decorate([
customElement("agent-interface")
], AgentInterface);
export { AgentInterface };
// Register custom element with guard
if (!customElements.get("agent-interface")) {
customElements.define("agent-interface", AgentInterface);
}
//# sourceMappingURL=AgentInterface.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +0,0 @@
import { LitElement } from "lit";
import type { Attachment } from "../utils/attachment-utils.js";
export declare class AttachmentTile extends LitElement {
attachment: Attachment;
showDelete: boolean;
onDelete?: () => void;
protected createRenderRoot(): HTMLElement | DocumentFragment;
connectedCallback(): void;
private handleClick;
render(): import("lit-html").TemplateResult<1>;
}
//# sourceMappingURL=AttachmentTile.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"AttachmentTile.d.ts","sourceRoot":"","sources":["../../src/components/AttachmentTile.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAKjC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAG/D,qBACa,cAAe,SAAQ,UAAU;IACjB,UAAU,EAAG,UAAU,CAAC;IACvB,UAAU,UAAS;IACpC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;cAEf,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAI5D,iBAAiB,IAAI,IAAI;IAMlC,OAAO,CAAC,WAAW,CAEjB;IAEO,MAAM;CAmFf"}

View File

@@ -1,115 +0,0 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { icon } from "@mariozechner/mini-lit/dist/icons.js";
import { LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { html } from "lit/html.js";
import { FileSpreadsheet, FileText, X } from "lucide";
import { AttachmentOverlay } from "../dialogs/AttachmentOverlay.js";
import { i18n } from "../utils/i18n.js";
let AttachmentTile = class AttachmentTile extends LitElement {
constructor() {
super(...arguments);
this.showDelete = false;
this.handleClick = () => {
AttachmentOverlay.open(this.attachment);
};
}
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.style.display = "block";
this.classList.add("max-h-16");
}
render() {
const hasPreview = !!this.attachment.preview;
const isImage = this.attachment.type === "image";
const isPdf = this.attachment.mimeType === "application/pdf";
const isDocx = this.attachment.mimeType?.includes("wordprocessingml") ||
this.attachment.fileName.toLowerCase().endsWith(".docx");
const isPptx = this.attachment.mimeType?.includes("presentationml") ||
this.attachment.fileName.toLowerCase().endsWith(".pptx");
const isExcel = this.attachment.mimeType?.includes("spreadsheetml") ||
this.attachment.fileName.toLowerCase().endsWith(".xlsx") ||
this.attachment.fileName.toLowerCase().endsWith(".xls");
// Choose the appropriate icon
const getDocumentIcon = () => {
if (isExcel)
return icon(FileSpreadsheet, "md");
return icon(FileText, "md");
};
return html `
<div class="relative group inline-block">
${hasPreview
? html `
<div class="relative">
<img
src="data:${isImage ? this.attachment.mimeType : "image/png"};base64,${this.attachment.preview}"
class="w-16 h-16 object-cover rounded-lg border border-input cursor-pointer hover:opacity-80 transition-opacity"
alt="${this.attachment.fileName}"
title="${this.attachment.fileName}"
@click=${this.handleClick}
/>
${isPdf
? html `
<!-- PDF badge overlay -->
<div class="absolute bottom-0 left-0 right-0 bg-background/90 px-1 py-0.5 rounded-b-lg">
<div class="text-[10px] text-muted-foreground text-center font-medium">${i18n("PDF")}</div>
</div>
`
: ""}
</div>
`
: html `
<!-- Fallback: document icon + filename -->
<div
class="w-16 h-16 rounded-lg border border-input cursor-pointer hover:opacity-80 transition-opacity bg-muted text-muted-foreground flex flex-col items-center justify-center p-2"
@click=${this.handleClick}
title="${this.attachment.fileName}"
>
${getDocumentIcon()}
<div class="text-[10px] text-center truncate w-full">
${this.attachment.fileName.length > 10
? this.attachment.fileName.substring(0, 8) + "..."
: this.attachment.fileName}
</div>
</div>
`}
${this.showDelete
? html `
<button
@click=${(e) => {
e.stopPropagation();
this.onDelete?.();
}}
class="absolute -top-1 -right-1 w-5 h-5 bg-background hover:bg-muted text-muted-foreground hover:text-foreground rounded-full flex items-center justify-center opacity-100 hover:opacity-100 [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover:opacity-100 transition-opacity border border-input shadow-sm"
title="${i18n("Remove")}"
>
${icon(X, "xs")}
</button>
`
: ""}
</div>
`;
}
};
__decorate([
property({ type: Object })
], AttachmentTile.prototype, "attachment", void 0);
__decorate([
property({ type: Boolean })
], AttachmentTile.prototype, "showDelete", void 0);
__decorate([
property()
], AttachmentTile.prototype, "onDelete", void 0);
AttachmentTile = __decorate([
customElement("attachment-tile")
], AttachmentTile);
export { AttachmentTile };
//# sourceMappingURL=AttachmentTile.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"AttachmentTile.js","sourceRoot":"","sources":["../../src/components/AttachmentTile.ts"],"names":[],"mappings":"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"}

View File

@@ -1,12 +0,0 @@
import { LitElement } from "lit";
export declare class ConsoleBlock extends LitElement {
content: string;
variant: "default" | "error";
private copied;
protected createRenderRoot(): HTMLElement | DocumentFragment;
connectedCallback(): void;
private copy;
updated(): void;
render(): import("lit-html").TemplateResult<1>;
}
//# sourceMappingURL=ConsoleBlock.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"ConsoleBlock.d.ts","sourceRoot":"","sources":["../../src/components/ConsoleBlock.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAMjC,qBAAa,YAAa,SAAQ,UAAU;IAC/B,OAAO,EAAE,MAAM,CAAM;IACrB,OAAO,EAAE,SAAS,GAAG,OAAO,CAAa;IAC5C,OAAO,CAAC,MAAM,CAAS;cAEb,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAI5D,iBAAiB,IAAI,IAAI;YAKpB,IAAI;IAYT,OAAO;IAQP,MAAM;CAyBf"}

View File

@@ -1,84 +0,0 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { icon } from "@mariozechner/mini-lit";
import { LitElement } from "lit";
import { property, state } from "lit/decorators.js";
import { html } from "lit/html.js";
import { Check, Copy } from "lucide";
import { i18n } from "../utils/i18n.js";
export class ConsoleBlock extends LitElement {
constructor() {
super(...arguments);
this.content = "";
this.variant = "default";
this.copied = false;
}
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.style.display = "block";
}
async copy() {
try {
await navigator.clipboard.writeText(this.content || "");
this.copied = true;
setTimeout(() => {
this.copied = false;
}, 1500);
}
catch (e) {
console.error("Copy failed", e);
}
}
updated() {
// Auto-scroll to bottom on content changes
const container = this.querySelector(".console-scroll");
if (container) {
container.scrollTop = container.scrollHeight;
}
}
render() {
const isError = this.variant === "error";
const textClass = isError ? "text-destructive" : "text-foreground";
return html `
<div class="border border-border rounded-lg overflow-hidden">
<div class="flex items-center justify-between px-3 py-1.5 bg-muted border-b border-border">
<span class="text-xs text-muted-foreground font-mono">${i18n("console")}</span>
<button
@click=${() => this.copy()}
class="flex items-center gap-1 px-2 py-0.5 text-xs rounded hover:bg-accent text-muted-foreground hover:text-accent-foreground transition-colors"
title="${i18n("Copy output")}"
>
${this.copied ? icon(Check, "sm") : icon(Copy, "sm")}
${this.copied ? html `<span>${i18n("Copied!")}</span>` : ""}
</button>
</div>
<div class="console-scroll overflow-auto max-h-64">
<pre class="!bg-background !border-0 !rounded-none m-0 p-3 text-xs ${textClass} font-mono whitespace-pre-wrap">
${this.content || ""}</pre
>
</div>
</div>
`;
}
}
__decorate([
property()
], ConsoleBlock.prototype, "content", void 0);
__decorate([
property()
], ConsoleBlock.prototype, "variant", void 0);
__decorate([
state()
], ConsoleBlock.prototype, "copied", void 0);
// Register custom element
if (!customElements.get("console-block")) {
customElements.define("console-block", ConsoleBlock);
}
//# sourceMappingURL=ConsoleBlock.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"ConsoleBlock.js","sourceRoot":"","sources":["../../src/components/ConsoleBlock.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAExC,MAAM,OAAO,YAAa,SAAQ,UAAU;IAA5C;;QACa,YAAO,GAAW,EAAE,CAAC;QACrB,YAAO,GAAwB,SAAS,CAAC;QACpC,WAAM,GAAG,KAAK,CAAC;IAwDjC,CAAC;IAtDmB,gBAAgB;QAClC,OAAO,IAAI,CAAC;IACb,CAAC;IAEQ,iBAAiB;QACzB,KAAK,CAAC,iBAAiB,EAAE,CAAC;QAC1B,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,OAAO,CAAC;IAC9B,CAAC;IAEO,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC;YACJ,MAAM,SAAS,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;YACxD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;YACnB,UAAU,CAAC,GAAG,EAAE;gBACf,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;YACrB,CAAC,EAAE,IAAI,CAAC,CAAC;QACV,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACZ,OAAO,CAAC,KAAK,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;QACjC,CAAC;IACF,CAAC;IAEQ,OAAO;QACf,2CAA2C;QAC3C,MAAM,SAAS,GAAG,IAAI,CAAC,aAAa,CAAC,iBAAiB,CAAuB,CAAC;QAC9E,IAAI,SAAS,EAAE,CAAC;YACf,SAAS,CAAC,SAAS,GAAG,SAAS,CAAC,YAAY,CAAC;QAC9C,CAAC;IACF,CAAC;IAEQ,MAAM;QACd,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,KAAK,OAAO,CAAC;QACzC,MAAM,SAAS,GAAG,OAAO,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,iBAAiB,CAAC;QAEnE,OAAO,IAAI,CAAA;;;6DAGgD,IAAI,CAAC,SAAS,CAAC;;eAE7D,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE;;eAEjB,IAAI,CAAC,aAAa,CAAC;;QAE1B,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC;QAClD,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAA,SAAS,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;;;;0EAIU,SAAS;EACjF,IAAI,CAAC,OAAO,IAAI,EAAE;;;;GAIjB,CAAC;IACH,CAAC;CACD;AA1DY;IAAX,QAAQ,EAAE;6CAAsB;AACrB;IAAX,QAAQ,EAAE;6CAA0C;AACpC;IAAhB,KAAK,EAAE;4CAAwB;AA0DjC,0BAA0B;AAC1B,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,eAAe,CAAC,EAAE,CAAC;IAC1C,cAAc,CAAC,MAAM,CAAC,eAAe,EAAE,YAAY,CAAC,CAAC;AACtD,CAAC"}

View File

@@ -1,17 +0,0 @@
import { LitElement, type TemplateResult } from "lit";
import type { CustomProvider } from "../storage/stores/custom-providers-store.js";
export declare class CustomProviderCard extends LitElement {
provider: CustomProvider;
isAutoDiscovery: boolean;
status?: {
modelCount: number;
status: "connected" | "disconnected" | "checking";
};
onRefresh?: (provider: CustomProvider) => void;
onEdit?: (provider: CustomProvider) => void;
onDelete?: (provider: CustomProvider) => void;
protected createRenderRoot(): this;
private renderStatus;
render(): TemplateResult;
}
//# sourceMappingURL=CustomProviderCard.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"CustomProviderCard.d.ts","sourceRoot":"","sources":["../../src/components/CustomProviderCard.ts"],"names":[],"mappings":"AAEA,OAAO,EAAQ,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAE5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6CAA6C,CAAC;AAElF,qBACa,kBAAmB,SAAQ,UAAU;IACrB,QAAQ,EAAG,cAAc,CAAC;IACzB,eAAe,UAAS;IACzB,MAAM,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,WAAW,GAAG,cAAc,GAAG,UAAU,CAAA;KAAE,CAAC;IACnG,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,CAAC;IAC/C,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,CAAC;IAC5C,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,IAAI,CAAC;IAE1D,SAAS,CAAC,gBAAgB;IAI1B,OAAO,CAAC,YAAY;IAgCpB,MAAM,IAAI,cAAc;CAgDxB"}

View File

@@ -1,110 +0,0 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { i18n } from "@mariozechner/mini-lit";
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
let CustomProviderCard = class CustomProviderCard extends LitElement {
constructor() {
super(...arguments);
this.isAutoDiscovery = false;
}
createRenderRoot() {
return this;
}
renderStatus() {
if (!this.isAutoDiscovery) {
return html `
<div class="text-xs text-muted-foreground mt-1">
${i18n("Models")}: ${this.provider.models?.length || 0}
</div>
`;
}
if (!this.status)
return html ``;
const statusIcon = this.status.status === "connected"
? html `<span class="text-green-500">●</span>`
: this.status.status === "checking"
? html `<span class="text-yellow-500">●</span>`
: html `<span class="text-red-500">●</span>`;
const statusText = this.status.status === "connected"
? `${this.status.modelCount} ${i18n("models")}`
: this.status.status === "checking"
? i18n("Checking...")
: i18n("Disconnected");
return html `
<div class="text-xs text-muted-foreground mt-1 flex items-center gap-1">
${statusIcon} ${statusText}
</div>
`;
}
render() {
return html `
<div class="border border-border rounded-lg p-4 space-y-2">
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="font-medium text-sm text-foreground">${this.provider.name}</div>
<div class="text-xs text-muted-foreground mt-1">
<span class="capitalize">${this.provider.type}</span>
${this.provider.baseUrl ? html `${this.provider.baseUrl}` : ""}
</div>
${this.renderStatus()}
</div>
<div class="flex gap-2">
${this.isAutoDiscovery && this.onRefresh
? Button({
onClick: () => this.onRefresh?.(this.provider),
variant: "ghost",
size: "sm",
children: i18n("Refresh"),
})
: ""}
${this.onEdit
? Button({
onClick: () => this.onEdit?.(this.provider),
variant: "ghost",
size: "sm",
children: i18n("Edit"),
})
: ""}
${this.onDelete
? Button({
onClick: () => this.onDelete?.(this.provider),
variant: "ghost",
size: "sm",
children: i18n("Delete"),
})
: ""}
</div>
</div>
</div>
`;
}
};
__decorate([
property({ type: Object })
], CustomProviderCard.prototype, "provider", void 0);
__decorate([
property({ type: Boolean })
], CustomProviderCard.prototype, "isAutoDiscovery", void 0);
__decorate([
property({ type: Object })
], CustomProviderCard.prototype, "status", void 0);
__decorate([
property()
], CustomProviderCard.prototype, "onRefresh", void 0);
__decorate([
property()
], CustomProviderCard.prototype, "onEdit", void 0);
__decorate([
property()
], CustomProviderCard.prototype, "onDelete", void 0);
CustomProviderCard = __decorate([
customElement("custom-provider-card")
], CustomProviderCard);
export { CustomProviderCard };
//# sourceMappingURL=CustomProviderCard.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"CustomProviderCard.js","sourceRoot":"","sources":["../../src/components/CustomProviderCard.ts"],"names":[],"mappings":"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"}

View File

@@ -1,15 +0,0 @@
import { LitElement, type TemplateResult } from "lit";
/**
* Reusable expandable section component for tool renderers.
* Captures children in connectedCallback and re-renders them in the details area.
*/
export declare class ExpandableSection extends LitElement {
summary: string;
defaultExpanded: boolean;
private expanded;
private capturedChildren;
protected createRenderRoot(): this;
connectedCallback(): void;
render(): TemplateResult;
}
//# sourceMappingURL=ExpandableSection.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"ExpandableSection.d.ts","sourceRoot":"","sources":["../../src/components/ExpandableSection.ts"],"names":[],"mappings":"AACA,OAAO,EAAQ,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAI5D;;;GAGG;AACH,qBACa,iBAAkB,SAAQ,UAAU;IACpC,OAAO,EAAG,MAAM,CAAC;IACA,eAAe,UAAS;IAC5C,OAAO,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,gBAAgB,CAAc;IAEtC,SAAS,CAAC,gBAAgB;IAIjB,iBAAiB;IASjB,MAAM,IAAI,cAAc;CAgBjC"}

View File

@@ -1,63 +0,0 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { icon } from "@mariozechner/mini-lit";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ChevronDown, ChevronRight } from "lucide";
/**
* Reusable expandable section component for tool renderers.
* Captures children in connectedCallback and re-renders them in the details area.
*/
let ExpandableSection = class ExpandableSection extends LitElement {
constructor() {
super(...arguments);
this.defaultExpanded = false;
this.expanded = false;
this.capturedChildren = [];
}
createRenderRoot() {
return this; // light DOM
}
connectedCallback() {
super.connectedCallback();
// Capture children before first render
this.capturedChildren = Array.from(this.childNodes);
// Clear children (we'll re-insert them in render)
this.innerHTML = "";
this.expanded = this.defaultExpanded;
}
render() {
return html `
<div>
<button
@click=${() => {
this.expanded = !this.expanded;
}}
class="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors w-full text-left"
>
${icon(this.expanded ? ChevronDown : ChevronRight, "sm")}
<span>${this.summary}</span>
</button>
${this.expanded ? html `<div class="mt-2">${this.capturedChildren}</div>` : ""}
</div>
`;
}
};
__decorate([
property()
], ExpandableSection.prototype, "summary", void 0);
__decorate([
property({ type: Boolean })
], ExpandableSection.prototype, "defaultExpanded", void 0);
__decorate([
state()
], ExpandableSection.prototype, "expanded", void 0);
ExpandableSection = __decorate([
customElement("expandable-section")
], ExpandableSection);
export { ExpandableSection };
//# sourceMappingURL=ExpandableSection.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"ExpandableSection.js","sourceRoot":"","sources":["../../src/components/ExpandableSection.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,UAAU,EAAuB,MAAM,KAAK,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEnD;;;GAGG;AAEI,IAAM,iBAAiB,GAAvB,MAAM,iBAAkB,SAAQ,UAAU;IAA1C;;QAEuB,oBAAe,GAAG,KAAK,CAAC;QACpC,aAAQ,GAAG,KAAK,CAAC;QAC1B,qBAAgB,GAAW,EAAE,CAAC;IA+BvC,CAAC;IA7BU,gBAAgB;QACzB,OAAO,IAAI,CAAC,CAAC,YAAY;IAC1B,CAAC;IAEQ,iBAAiB;QACzB,KAAK,CAAC,iBAAiB,EAAE,CAAC;QAC1B,uCAAuC;QACvC,IAAI,CAAC,gBAAgB,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACpD,kDAAkD;QAClD,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;QACpB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC;IACtC,CAAC;IAEQ,MAAM;QACd,OAAO,IAAI,CAAA;;;cAGC,GAAG,EAAE;YACb,IAAI,CAAC,QAAQ,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC;QAChC,CAAC;;;OAGC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,YAAY,EAAE,IAAI,CAAC;aAChD,IAAI,CAAC,OAAO;;MAEnB,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAA,qBAAqB,IAAI,CAAC,gBAAgB,QAAQ,CAAC,CAAC,CAAC,EAAE;;GAE9E,CAAC;IACH,CAAC;CACD,CAAA;AAlCY;IAAX,QAAQ,EAAE;kDAAkB;AACA;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;0DAAyB;AACpC;IAAhB,KAAK,EAAE;mDAA0B;AAHtB,iBAAiB;IAD7B,aAAa,CAAC,oBAAoB,CAAC;GACvB,iBAAiB,CAmC7B"}

View File

@@ -1 +0,0 @@
{"version":3,"file":"Input.d.ts","sourceRoot":"","sources":["../../src/components/Input.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,kBAAkB,EAAM,MAAM,qCAAqC,CAAC;AAElF,OAAO,EAAE,KAAK,GAAG,EAAO,MAAM,uBAAuB,CAAC;AAGtD,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,GAAG,KAAK,GAAG,KAAK,GAAG,QAAQ,CAAC;AAC5F,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE3C,MAAM,WAAW,UAAW,SAAQ,kBAAkB;IACrD,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,GAAG,CAAC,gBAAgB,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,IAAI,CAAC;IAC7B,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,IAAI,CAAC;IAC9B,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,IAAI,CAAC;IACvC,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,IAAI,CAAC;CACrC;AAED,eAAO,MAAM,KAAK,qEAmFjB,CAAC"}

View File

@@ -1,57 +0,0 @@
import { fc } from "@mariozechner/mini-lit/dist/mini.js";
import { html } from "lit";
import { ref } from "lit/directives/ref.js";
import { i18n } from "../utils/i18n.js";
export const Input = fc(({ type = "text", size = "md", value = "", placeholder = "", label = "", error = "", disabled = false, required = false, name = "", autocomplete = "", min, max, step, inputRef, onInput, onChange, onKeyDown, onKeyUp, className = "", }) => {
const sizeClasses = {
sm: "h-8 px-3 py-1 text-sm",
md: "h-9 px-3 py-1 text-sm md:text-sm",
lg: "h-10 px-4 py-1 text-base",
};
const baseClasses = "flex w-full min-w-0 rounded-md border bg-transparent text-foreground shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium";
const interactionClasses = "placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground";
const focusClasses = "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]";
const darkClasses = "dark:bg-input/30";
const stateClasses = error
? "border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40"
: "border-input";
const disabledClasses = "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50";
const handleInput = (e) => {
onInput?.(e);
};
const handleChange = (e) => {
onChange?.(e);
};
return html `
<div class="flex flex-col gap-1.5 ${className}">
${label
? html `
<label class="text-sm font-medium text-foreground">
${label} ${required ? html `<span class="text-destructive">${i18n("*")}</span>` : ""}
</label>
`
: ""}
<input
type="${type}"
class="${baseClasses} ${sizeClasses[size]} ${interactionClasses} ${focusClasses} ${darkClasses} ${stateClasses} ${disabledClasses}"
.value=${value}
placeholder="${placeholder}"
?disabled=${disabled}
?required=${required}
?aria-invalid=${!!error}
name="${name}"
autocomplete="${autocomplete}"
min="${min ?? ""}"
max="${max ?? ""}"
step="${step ?? ""}"
@input=${handleInput}
@change=${handleChange}
@keydown=${onKeyDown}
@keyup=${onKeyUp}
${inputRef ? ref(inputRef) : ""}
/>
${error ? html `<span class="text-sm text-destructive">${error}</span>` : ""}
</div>
`;
});
//# sourceMappingURL=Input.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"Input.js","sourceRoot":"","sources":["../../src/components/Input.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,EAAE,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAC3B,OAAO,EAAY,GAAG,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AA0BxC,MAAM,CAAC,MAAM,KAAK,GAAG,EAAE,CACtB,CAAC,EACA,IAAI,GAAG,MAAM,EACb,IAAI,GAAG,IAAI,EACX,KAAK,GAAG,EAAE,EACV,WAAW,GAAG,EAAE,EAChB,KAAK,GAAG,EAAE,EACV,KAAK,GAAG,EAAE,EACV,QAAQ,GAAG,KAAK,EAChB,QAAQ,GAAG,KAAK,EAChB,IAAI,GAAG,EAAE,EACT,YAAY,GAAG,EAAE,EACjB,GAAG,EACH,GAAG,EACH,IAAI,EACJ,QAAQ,EACR,OAAO,EACP,QAAQ,EACR,SAAS,EACT,OAAO,EACP,SAAS,GAAG,EAAE,GACd,EAAE,EAAE;IACJ,MAAM,WAAW,GAAG;QACnB,EAAE,EAAE,uBAAuB;QAC3B,EAAE,EAAE,kCAAkC;QACtC,EAAE,EAAE,0BAA0B;KAC9B,CAAC;IAEF,MAAM,WAAW,GAChB,qNAAqN,CAAC;IACvN,MAAM,kBAAkB,GACvB,0FAA0F,CAAC;IAC5F,MAAM,YAAY,GAAG,+EAA+E,CAAC;IACrG,MAAM,WAAW,GAAG,kBAAkB,CAAC;IACvC,MAAM,YAAY,GAAG,KAAK;QACzB,CAAC,CAAC,2FAA2F;QAC7F,CAAC,CAAC,cAAc,CAAC;IAClB,MAAM,eAAe,GAAG,8EAA8E,CAAC;IAEvG,MAAM,WAAW,GAAG,CAAC,CAAQ,EAAE,EAAE;QAChC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;IACd,CAAC,CAAC;IAEF,MAAM,YAAY,GAAG,CAAC,CAAQ,EAAE,EAAE;QACjC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC;IACf,CAAC,CAAC;IAEF,OAAO,IAAI,CAAA;uCAC0B,SAAS;MAE3C,KAAK;QACJ,CAAC,CAAC,IAAI,CAAA;;UAEF,KAAK,IAAI,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAA,kCAAkC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;;OAEpF;QACD,CAAC,CAAC,EACJ;;aAES,IAAI;cACH,WAAW,IACnB,WAAW,CAAC,IAAI,CACjB,IAAI,kBAAkB,IAAI,YAAY,IAAI,WAAW,IAAI,YAAY,IAAI,eAAe;cAC/E,KAAK;oBACC,WAAW;iBACd,QAAQ;iBACR,QAAQ;qBACJ,CAAC,CAAC,KAAK;aACf,IAAI;qBACI,YAAY;YACrB,GAAG,IAAI,EAAE;YACT,GAAG,IAAI,EAAE;aACR,IAAI,IAAI,EAAE;cACT,WAAW;eACV,YAAY;gBACX,SAAS;cACX,OAAO;OACd,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE;;MAE9B,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA,0CAA0C,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE;;GAE5E,CAAC;AACH,CAAC,CACD,CAAC"}

View File

@@ -1,43 +0,0 @@
import type { Model } from "@mariozechner/pi-ai";
import { LitElement } from "lit";
import { type Attachment } from "../utils/attachment-utils.js";
import "./AttachmentTile.js";
export declare class MessageEditor extends LitElement {
private _value;
private textareaRef;
get value(): string;
set value(val: string);
isStreaming: boolean;
currentModel?: Model<any>;
thinkingLevel: "off" | "minimal" | "low" | "medium" | "high";
showAttachmentButton: boolean;
showModelSelector: boolean;
showThinkingSelector: boolean;
onInput?: (value: string) => void;
onSend?: (input: string, attachments: Attachment[]) => void;
onAbort?: () => void;
onModelSelect?: () => void;
onThinkingChange?: (level: "off" | "minimal" | "low" | "medium" | "high") => void;
onFilesChange?: (files: Attachment[]) => void;
attachments: Attachment[];
maxFiles: number;
maxFileSize: number;
acceptedTypes: string;
processingFiles: boolean;
isDragging: boolean;
private fileInputRef;
protected createRenderRoot(): HTMLElement | DocumentFragment;
private handleTextareaInput;
private handleKeyDown;
private handlePaste;
private handleSend;
private handleAttachmentClick;
private handleFilesSelected;
private removeFile;
private handleDragOver;
private handleDragLeave;
private handleDrop;
firstUpdated(): void;
render(): import("lit-html").TemplateResult<1>;
}
//# sourceMappingURL=MessageEditor.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"MessageEditor.d.ts","sourceRoot":"","sources":["../../src/components/MessageEditor.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAQ,UAAU,EAAE,MAAM,KAAK,CAAC;AAIvC,OAAO,EAAE,KAAK,UAAU,EAAkB,MAAM,8BAA8B,CAAC;AAE/E,OAAO,qBAAqB,CAAC;AAE7B,qBACa,aAAc,SAAQ,UAAU;IAC5C,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,WAAW,CAAoC;IAEvD,IACI,KAAK,IAIM,MAAM,CAFpB;IAED,IAAI,KAAK,CAAC,GAAG,EAAE,MAAM,EAIpB;IAEW,WAAW,UAAS;IACpB,YAAY,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAC1B,aAAa,EAAE,KAAK,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAS;IACrE,oBAAoB,UAAQ;IAC5B,iBAAiB,UAAQ;IACzB,oBAAoB,UAAQ;IAC5B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;IAC5D,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,KAAK,IAAI,CAAC;IAClF,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;IAC9C,WAAW,EAAE,UAAU,EAAE,CAAM;IAC/B,QAAQ,SAAM;IACd,WAAW,SAAoB;IAC/B,aAAa,SACqF;IAErG,eAAe,UAAS;IACxB,UAAU,UAAS;IAC5B,OAAO,CAAC,YAAY,CAAiC;cAElC,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAIrE,OAAO,CAAC,mBAAmB,CAIzB;IAEF,OAAO,CAAC,aAAa,CAUnB;IAEF,OAAO,CAAC,WAAW,CAgDjB;IAEF,OAAO,CAAC,UAAU,CAEhB;IAEF,OAAO,CAAC,qBAAqB,CAE3B;YAEY,mBAAmB;IAmCjC,OAAO,CAAC,UAAU;IAKlB,OAAO,CAAC,cAAc,CAMpB;IAEF,OAAO,CAAC,eAAe,CAUrB;IAEF,OAAO,CAAC,UAAU,CAkChB;IAEO,YAAY;IAOZ,MAAM;CAqKf"}

View File

@@ -1,414 +0,0 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { icon } from "@mariozechner/mini-lit";
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
import { Select } from "@mariozechner/mini-lit/dist/Select.js";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { createRef, ref } from "lit/directives/ref.js";
import { Brain, Loader2, Paperclip, Send, Sparkles, Square } from "lucide";
import { loadAttachment } from "../utils/attachment-utils.js";
import { i18n } from "../utils/i18n.js";
import "./AttachmentTile.js";
let MessageEditor = class MessageEditor extends LitElement {
constructor() {
super(...arguments);
this._value = "";
this.textareaRef = createRef();
this.isStreaming = false;
this.thinkingLevel = "off";
this.showAttachmentButton = true;
this.showModelSelector = true;
this.showThinkingSelector = true;
this.attachments = [];
this.maxFiles = 10;
this.maxFileSize = 20 * 1024 * 1024; // 20MB
this.acceptedTypes = "image/*,application/pdf,.docx,.pptx,.xlsx,.xls,.txt,.md,.json,.xml,.html,.css,.js,.ts,.jsx,.tsx,.yml,.yaml";
this.processingFiles = false;
this.isDragging = false;
this.fileInputRef = createRef();
this.handleTextareaInput = (e) => {
const textarea = e.target;
this.value = textarea.value;
this.onInput?.(this.value);
};
this.handleKeyDown = (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
if (!this.isStreaming && !this.processingFiles && (this.value.trim() || this.attachments.length > 0)) {
this.handleSend();
}
}
else if (e.key === "Escape" && this.isStreaming) {
e.preventDefault();
this.onAbort?.();
}
};
this.handlePaste = async (e) => {
const items = e.clipboardData?.items;
if (!items)
return;
const imageFiles = [];
// Check for image items in clipboard
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.type.startsWith("image/")) {
const file = item.getAsFile();
if (file) {
imageFiles.push(file);
}
}
}
// If we found images, process them
if (imageFiles.length > 0) {
e.preventDefault(); // Prevent default paste behavior
if (imageFiles.length + this.attachments.length > this.maxFiles) {
alert(`Maximum ${this.maxFiles} files allowed`);
return;
}
this.processingFiles = true;
const newAttachments = [];
for (const file of imageFiles) {
try {
if (file.size > this.maxFileSize) {
alert(`Image exceeds maximum size of ${Math.round(this.maxFileSize / 1024 / 1024)}MB`);
continue;
}
const attachment = await loadAttachment(file);
newAttachments.push(attachment);
}
catch (error) {
console.error("Error processing pasted image:", error);
alert(`Failed to process pasted image: ${String(error)}`);
}
}
this.attachments = [...this.attachments, ...newAttachments];
this.onFilesChange?.(this.attachments);
this.processingFiles = false;
}
};
this.handleSend = () => {
this.onSend?.(this.value, this.attachments);
};
this.handleAttachmentClick = () => {
this.fileInputRef.value?.click();
};
this.handleDragOver = (e) => {
e.preventDefault();
e.stopPropagation();
if (!this.isDragging) {
this.isDragging = true;
}
};
this.handleDragLeave = (e) => {
e.preventDefault();
e.stopPropagation();
// Only set isDragging to false if we're leaving the entire component
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX;
const y = e.clientY;
if (x <= rect.left || x >= rect.right || y <= rect.top || y >= rect.bottom) {
this.isDragging = false;
}
};
this.handleDrop = async (e) => {
e.preventDefault();
e.stopPropagation();
this.isDragging = false;
const files = Array.from(e.dataTransfer?.files || []);
if (files.length === 0)
return;
if (files.length + this.attachments.length > this.maxFiles) {
alert(`Maximum ${this.maxFiles} files allowed`);
return;
}
this.processingFiles = true;
const newAttachments = [];
for (const file of files) {
try {
if (file.size > this.maxFileSize) {
alert(`${file.name} exceeds maximum size of ${Math.round(this.maxFileSize / 1024 / 1024)}MB`);
continue;
}
const attachment = await loadAttachment(file);
newAttachments.push(attachment);
}
catch (error) {
console.error(`Error processing ${file.name}:`, error);
alert(`Failed to process ${file.name}: ${String(error)}`);
}
}
this.attachments = [...this.attachments, ...newAttachments];
this.onFilesChange?.(this.attachments);
this.processingFiles = false;
};
}
get value() {
return this._value;
}
set value(val) {
const oldValue = this._value;
this._value = val;
this.requestUpdate("value", oldValue);
}
createRenderRoot() {
return this;
}
async handleFilesSelected(e) {
const input = e.target;
const files = Array.from(input.files || []);
if (files.length === 0)
return;
if (files.length + this.attachments.length > this.maxFiles) {
alert(`Maximum ${this.maxFiles} files allowed`);
input.value = "";
return;
}
this.processingFiles = true;
const newAttachments = [];
for (const file of files) {
try {
if (file.size > this.maxFileSize) {
alert(`${file.name} exceeds maximum size of ${Math.round(this.maxFileSize / 1024 / 1024)}MB`);
continue;
}
const attachment = await loadAttachment(file);
newAttachments.push(attachment);
}
catch (error) {
console.error(`Error processing ${file.name}:`, error);
alert(`Failed to process ${file.name}: ${String(error)}`);
}
}
this.attachments = [...this.attachments, ...newAttachments];
this.onFilesChange?.(this.attachments);
this.processingFiles = false;
input.value = ""; // Reset input
}
removeFile(fileId) {
this.attachments = this.attachments.filter((f) => f.id !== fileId);
this.onFilesChange?.(this.attachments);
}
firstUpdated() {
const textarea = this.textareaRef.value;
if (textarea) {
textarea.focus();
}
}
render() {
// Check if current model supports thinking/reasoning
const model = this.currentModel;
const supportsThinking = model?.reasoning === true; // Models with reasoning:true support thinking
return html `
<div
class="bg-card rounded-xl border shadow-sm relative ${this.isDragging ? "border-primary border-2 bg-primary/5" : "border-border"}"
@dragover=${this.handleDragOver}
@dragleave=${this.handleDragLeave}
@drop=${this.handleDrop}
>
<!-- Drag overlay -->
${this.isDragging
? html `
<div class="absolute inset-0 bg-primary/10 rounded-xl pointer-events-none z-10 flex items-center justify-center">
<div class="text-primary font-medium">${i18n("Drop files here")}</div>
</div>
`
: ""}
<!-- Attachments -->
${this.attachments.length > 0
? html `
<div class="px-4 pt-3 pb-2 flex flex-wrap gap-2">
${this.attachments.map((attachment) => html `
<attachment-tile
.attachment=${attachment}
.showDelete=${true}
.onDelete=${() => this.removeFile(attachment.id)}
></attachment-tile>
`)}
</div>
`
: ""}
<textarea
class="w-full bg-transparent p-4 text-foreground placeholder-muted-foreground outline-none resize-none overflow-y-auto"
placeholder=${i18n("Type a message...")}
rows="1"
style="max-height: 200px; field-sizing: content; min-height: 1lh; height: auto;"
.value=${this.value}
@input=${this.handleTextareaInput}
@keydown=${this.handleKeyDown}
@paste=${this.handlePaste}
${ref(this.textareaRef)}
></textarea>
<!-- Hidden file input -->
<input
type="file"
${ref(this.fileInputRef)}
@change=${this.handleFilesSelected}
accept=${this.acceptedTypes}
multiple
style="display: none;"
/>
<!-- Button Row -->
<div class="px-2 pb-2 flex items-center justify-between">
<!-- Left side - attachment and thinking selector -->
<div class="flex gap-2 items-center">
${this.showAttachmentButton
? this.processingFiles
? html `
<div class="h-8 w-8 flex items-center justify-center">
${icon(Loader2, "sm", "animate-spin text-muted-foreground")}
</div>
`
: html `
${Button({
variant: "ghost",
size: "icon",
className: "h-8 w-8",
onClick: this.handleAttachmentClick,
children: icon(Paperclip, "sm"),
})}
`
: ""}
${supportsThinking && this.showThinkingSelector
? html `
${Select({
value: this.thinkingLevel,
placeholder: i18n("Off"),
options: [
{ value: "off", label: i18n("Off"), icon: icon(Brain, "sm") },
{ value: "minimal", label: i18n("Minimal"), icon: icon(Brain, "sm") },
{ value: "low", label: i18n("Low"), icon: icon(Brain, "sm") },
{ value: "medium", label: i18n("Medium"), icon: icon(Brain, "sm") },
{ value: "high", label: i18n("High"), icon: icon(Brain, "sm") },
],
onChange: (value) => {
this.onThinkingChange?.(value);
},
width: "80px",
size: "sm",
variant: "ghost",
fitContent: true,
})}
`
: ""}
</div>
<!-- Model selector and send on the right -->
<div class="flex gap-2 items-center">
${this.showModelSelector && this.currentModel
? html `
${Button({
variant: "ghost",
size: "sm",
onClick: () => {
// Focus textarea before opening model selector so focus returns there
this.textareaRef.value?.focus();
// Wait for next frame to ensure focus takes effect before dialog captures it
requestAnimationFrame(() => {
this.onModelSelect?.();
});
},
children: html `
${icon(Sparkles, "sm")}
<span class="ml-1">${this.currentModel.id}</span>
`,
className: "h-8 text-xs truncate",
})}
`
: ""}
${this.isStreaming
? html `
${Button({
variant: "ghost",
size: "icon",
onClick: this.onAbort,
children: icon(Square, "sm"),
className: "h-8 w-8",
})}
`
: html `
${Button({
variant: "ghost",
size: "icon",
onClick: this.handleSend,
disabled: (!this.value.trim() && this.attachments.length === 0) || this.processingFiles,
children: html `<div style="transform: rotate(-45deg)">${icon(Send, "sm")}</div>`,
className: "h-8 w-8",
})}
`}
</div>
</div>
</div>
`;
}
};
__decorate([
property()
], MessageEditor.prototype, "value", null);
__decorate([
property()
], MessageEditor.prototype, "isStreaming", void 0);
__decorate([
property()
], MessageEditor.prototype, "currentModel", void 0);
__decorate([
property()
], MessageEditor.prototype, "thinkingLevel", void 0);
__decorate([
property()
], MessageEditor.prototype, "showAttachmentButton", void 0);
__decorate([
property()
], MessageEditor.prototype, "showModelSelector", void 0);
__decorate([
property()
], MessageEditor.prototype, "showThinkingSelector", void 0);
__decorate([
property()
], MessageEditor.prototype, "onInput", void 0);
__decorate([
property()
], MessageEditor.prototype, "onSend", void 0);
__decorate([
property()
], MessageEditor.prototype, "onAbort", void 0);
__decorate([
property()
], MessageEditor.prototype, "onModelSelect", void 0);
__decorate([
property()
], MessageEditor.prototype, "onThinkingChange", void 0);
__decorate([
property()
], MessageEditor.prototype, "onFilesChange", void 0);
__decorate([
property()
], MessageEditor.prototype, "attachments", void 0);
__decorate([
property()
], MessageEditor.prototype, "maxFiles", void 0);
__decorate([
property()
], MessageEditor.prototype, "maxFileSize", void 0);
__decorate([
property()
], MessageEditor.prototype, "acceptedTypes", void 0);
__decorate([
state()
], MessageEditor.prototype, "processingFiles", void 0);
__decorate([
state()
], MessageEditor.prototype, "isDragging", void 0);
MessageEditor = __decorate([
customElement("message-editor")
], MessageEditor);
export { MessageEditor };
//# sourceMappingURL=MessageEditor.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,15 +0,0 @@
import type { AgentTool } from "@mariozechner/pi-ai";
import { LitElement, type TemplateResult } from "lit";
import type { AppMessage } from "./Messages.js";
export declare class MessageList extends LitElement {
messages: AppMessage[];
tools: AgentTool[];
pendingToolCalls?: Set<string>;
isStreaming: boolean;
onCostClick?: () => void;
protected createRenderRoot(): HTMLElement | DocumentFragment;
connectedCallback(): void;
private buildRenderItems;
render(): TemplateResult<1>;
}
//# sourceMappingURL=MessageList.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../src/components/MessageList.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,SAAS,EAGT,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAQ,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAG5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGhD,qBAAa,WAAY,SAAQ,UAAU;IACf,QAAQ,EAAE,UAAU,EAAE,CAAM;IAC5B,KAAK,EAAE,SAAS,EAAE,CAAM;IACvB,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,WAAW,EAAE,OAAO,CAAS;IAC1B,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;cAEtC,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAI5D,iBAAiB,IAAI,IAAI;IAKlC,OAAO,CAAC,gBAAgB;IAuDf,MAAM;CAUf"}

View File

@@ -1,104 +0,0 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { html, LitElement } from "lit";
import { property } from "lit/decorators.js";
import { repeat } from "lit/directives/repeat.js";
import { renderMessage } from "./message-renderer-registry.js";
export class MessageList extends LitElement {
constructor() {
super(...arguments);
this.messages = [];
this.tools = [];
this.isStreaming = false;
}
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.style.display = "block";
}
buildRenderItems() {
// Map tool results by call id for quick lookup
const resultByCallId = new Map();
for (const message of this.messages) {
if (message.role === "toolResult") {
resultByCallId.set(message.toolCallId, message);
}
}
const items = [];
let index = 0;
for (const msg of this.messages) {
// Skip artifact messages - they're for session persistence only, not UI display
if (msg.role === "artifact") {
continue;
}
// Try custom renderer first
const customTemplate = renderMessage(msg);
if (customTemplate) {
items.push({ key: `msg:${index}`, template: customTemplate });
index++;
continue;
}
// Fall back to built-in renderers
if (msg.role === "user") {
items.push({
key: `msg:${index}`,
template: html `<user-message .message=${msg}></user-message>`,
});
index++;
}
else if (msg.role === "assistant") {
const amsg = msg;
items.push({
key: `msg:${index}`,
template: html `<assistant-message
.message=${amsg}
.tools=${this.tools}
.isStreaming=${false}
.pendingToolCalls=${this.pendingToolCalls}
.toolResultsById=${resultByCallId}
.hideToolCalls=${false}
.onCostClick=${this.onCostClick}
></assistant-message>`,
});
index++;
}
else {
// Skip standalone toolResult messages; they are rendered via paired tool-message above
// Skip unknown roles
}
}
return items;
}
render() {
const items = this.buildRenderItems();
return html `<div class="flex flex-col gap-3">
${repeat(items, (it) => it.key, (it) => it.template)}
</div>`;
}
}
__decorate([
property({ type: Array })
], MessageList.prototype, "messages", void 0);
__decorate([
property({ type: Array })
], MessageList.prototype, "tools", void 0);
__decorate([
property({ type: Object })
], MessageList.prototype, "pendingToolCalls", void 0);
__decorate([
property({ type: Boolean })
], MessageList.prototype, "isStreaming", void 0);
__decorate([
property({ attribute: false })
], MessageList.prototype, "onCostClick", void 0);
// Register custom element
if (!customElements.get("message-list")) {
customElements.define("message-list", MessageList);
}
//# sourceMappingURL=MessageList.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"MessageList.js","sourceRoot":"","sources":["../../src/components/MessageList.ts"],"names":[],"mappings":"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"}

View File

@@ -1,64 +0,0 @@
import type { AgentTool, AssistantMessage as AssistantMessageType, ToolCall, ToolResultMessage as ToolResultMessageType, UserMessage as UserMessageType } from "@mariozechner/pi-ai";
import { LitElement, type TemplateResult } from "lit";
import type { Attachment } from "../utils/attachment-utils.js";
import "./ThinkingBlock.js";
export type UserMessageWithAttachments = UserMessageType & {
attachments?: Attachment[];
};
export interface ArtifactMessage {
role: "artifact";
action: "create" | "update" | "delete";
filename: string;
content?: string;
title?: string;
timestamp: string;
}
type BaseMessage = AssistantMessageType | UserMessageWithAttachments | ToolResultMessageType | ArtifactMessage;
export interface CustomMessages {
}
export type AppMessage = BaseMessage | CustomMessages[keyof CustomMessages];
export declare class UserMessage extends LitElement {
message: UserMessageWithAttachments;
protected createRenderRoot(): HTMLElement | DocumentFragment;
connectedCallback(): void;
render(): TemplateResult<1>;
}
export declare class AssistantMessage extends LitElement {
message: AssistantMessageType;
tools?: AgentTool<any>[];
pendingToolCalls?: Set<string>;
hideToolCalls: boolean;
toolResultsById?: Map<string, ToolResultMessageType>;
isStreaming: boolean;
onCostClick?: () => void;
protected createRenderRoot(): HTMLElement | DocumentFragment;
connectedCallback(): void;
render(): TemplateResult<1>;
}
export declare class ToolMessageDebugView extends LitElement {
callArgs: any;
result?: ToolResultMessageType;
hasResult: boolean;
protected createRenderRoot(): HTMLElement | DocumentFragment;
connectedCallback(): void;
private pretty;
render(): TemplateResult<1>;
}
export declare class ToolMessage extends LitElement {
toolCall: ToolCall;
tool?: AgentTool<any>;
result?: ToolResultMessageType;
pending: boolean;
aborted: boolean;
isStreaming: boolean;
protected createRenderRoot(): HTMLElement | DocumentFragment;
connectedCallback(): void;
render(): TemplateResult;
}
export declare class AbortedMessage extends LitElement {
protected createRenderRoot(): HTMLElement | DocumentFragment;
connectedCallback(): void;
protected render(): unknown;
}
export {};
//# sourceMappingURL=Messages.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"Messages.d.ts","sourceRoot":"","sources":["../../src/components/Messages.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,SAAS,EACT,gBAAgB,IAAI,oBAAoB,EACxC,QAAQ,EACR,iBAAiB,IAAI,qBAAqB,EAC1C,WAAW,IAAI,eAAe,EAC9B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAQ,UAAU,EAAE,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAG5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAG/D,OAAO,oBAAoB,CAAC;AAE5B,MAAM,MAAM,0BAA0B,GAAG,eAAe,GAAG;IAAE,WAAW,CAAC,EAAE,UAAU,EAAE,CAAA;CAAE,CAAC;AAG1F,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CAClB;AAGD,KAAK,WAAW,GAAG,oBAAoB,GAAG,0BAA0B,GAAG,qBAAqB,GAAG,eAAe,CAAC;AAS/G,MAAM,WAAW,cAAc;CAE9B;AAGD,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,cAAc,CAAC,MAAM,cAAc,CAAC,CAAC;AAE5E,qBACa,WAAY,SAAQ,UAAU;IACd,OAAO,EAAG,0BAA0B,CAAC;cAE9C,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAI5D,iBAAiB,IAAI,IAAI;IAKzB,MAAM;CAyBf;AAED,qBACa,gBAAiB,SAAQ,UAAU;IACnB,OAAO,EAAG,oBAAoB,CAAC;IAChC,KAAK,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC;IACxB,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,aAAa,UAAS;IACvB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC;IACpD,WAAW,EAAE,OAAO,CAAS;IAC1B,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;cAEtC,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAI5D,iBAAiB,IAAI,IAAI;IAKzB,MAAM;CA2Df;AAED,qBACa,oBAAqB,SAAQ,UAAU;IACvB,QAAQ,EAAE,GAAG,CAAC;IACd,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC9B,SAAS,EAAE,OAAO,CAAS;cAErC,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAI5D,iBAAiB,IAAI,IAAI;IAKlC,OAAO,CAAC,MAAM;IAYL,MAAM;CA2Bf;AAED,qBACa,WAAY,SAAQ,UAAU;IACd,QAAQ,EAAG,QAAQ,CAAC;IACpB,IAAI,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;IACtB,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC9B,OAAO,EAAE,OAAO,CAAS;IACzB,OAAO,EAAE,OAAO,CAAS;IACzB,WAAW,EAAE,OAAO,CAAS;cAEvC,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAI5D,iBAAiB,IAAI,IAAI;IAKzB,MAAM;CAiCf;AAED,qBACa,cAAe,SAAQ,UAAU;cAC1B,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAI5D,iBAAiB,IAAI,IAAI;cAKf,MAAM,IAAI,OAAO;CAGpC"}

View File

@@ -1,280 +0,0 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { renderTool } from "../tools/index.js";
import { formatUsage } from "../utils/format.js";
import { i18n } from "../utils/i18n.js";
import { formatClock, renderSurfaceChip } from "../utils/message-meta.js";
import "./ThinkingBlock.js";
let UserMessage = class UserMessage extends LitElement {
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.style.display = "block";
}
render() {
const content = typeof this.message.content === "string"
? this.message.content
: this.message.content.find((c) => c.type === "text")?.text || "";
return html `
<div class="px-4 mb-1 flex items-center gap-2 text-[11px] uppercase tracking-[0.08em] text-muted-foreground">
${renderSurfaceChip(this.message.surface, this.message.senderHost, this.message.senderIp)}
<span class="opacity-70">${formatClock(this.message.timestamp)}</span>
</div>
<div class="flex justify-start mx-4">
<div class="user-message-container py-2 px-4 rounded-xl">
<markdown-block .content=${content}></markdown-block>
${this.message.attachments && this.message.attachments.length > 0
? html `
<div class="mt-3 flex flex-wrap gap-2">
${this.message.attachments.map((attachment) => html ` <attachment-tile .attachment=${attachment}></attachment-tile> `)}
</div>
`
: ""}
</div>
</div>
`;
}
};
__decorate([
property({ type: Object })
], UserMessage.prototype, "message", void 0);
UserMessage = __decorate([
customElement("user-message")
], UserMessage);
export { UserMessage };
let AssistantMessage = class AssistantMessage extends LitElement {
constructor() {
super(...arguments);
this.hideToolCalls = false;
this.isStreaming = false;
}
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.style.display = "block";
}
render() {
// Render content in the order it appears
const orderedParts = [];
for (const chunk of this.message.content) {
if (chunk.type === "text" && chunk.text.trim() !== "") {
orderedParts.push(html `<markdown-block .content=${chunk.text}></markdown-block>`);
}
else if (chunk.type === "thinking" && chunk.thinking.trim() !== "") {
orderedParts.push(html `<thinking-block .content=${chunk.thinking} .isStreaming=${this.isStreaming}></thinking-block>`);
}
else if (chunk.type === "toolCall") {
if (!this.hideToolCalls) {
const tool = this.tools?.find((t) => t.name === chunk.name);
const pending = this.pendingToolCalls?.has(chunk.id) ?? false;
const result = this.toolResultsById?.get(chunk.id);
// A tool call is aborted if the message was aborted and there's no result for this tool call
const aborted = this.message.stopReason === "aborted" && !result;
orderedParts.push(html `<tool-message
.tool=${tool}
.toolCall=${chunk}
.result=${result}
.pending=${pending}
.aborted=${aborted}
.isStreaming=${this.isStreaming}
></tool-message>`);
}
}
}
return html `
<div>
${orderedParts.length ? html ` <div class="px-4 flex flex-col gap-3">${orderedParts}</div> ` : ""}
${this.message.usage && !this.isStreaming
? this.onCostClick
? html ` <div class="px-4 mt-2 text-xs text-muted-foreground cursor-pointer hover:text-foreground transition-colors" @click=${this.onCostClick}>${formatUsage(this.message.usage)}</div> `
: html ` <div class="px-4 mt-2 text-xs text-muted-foreground">${formatUsage(this.message.usage)}</div> `
: ""}
${this.message.stopReason === "error" && this.message.errorMessage
? html `
<div class="mx-4 mt-3 p-3 bg-destructive/10 text-destructive rounded-lg text-sm overflow-hidden">
<strong>${i18n("Error:")}</strong> ${this.message.errorMessage}
</div>
`
: ""}
${this.message.stopReason === "aborted"
? html `<span class="text-sm text-destructive italic">${i18n("Request aborted")}</span>`
: ""}
</div>
`;
}
};
__decorate([
property({ type: Object })
], AssistantMessage.prototype, "message", void 0);
__decorate([
property({ type: Array })
], AssistantMessage.prototype, "tools", void 0);
__decorate([
property({ type: Object })
], AssistantMessage.prototype, "pendingToolCalls", void 0);
__decorate([
property({ type: Boolean })
], AssistantMessage.prototype, "hideToolCalls", void 0);
__decorate([
property({ type: Object })
], AssistantMessage.prototype, "toolResultsById", void 0);
__decorate([
property({ type: Boolean })
], AssistantMessage.prototype, "isStreaming", void 0);
__decorate([
property({ attribute: false })
], AssistantMessage.prototype, "onCostClick", void 0);
AssistantMessage = __decorate([
customElement("assistant-message")
], AssistantMessage);
export { AssistantMessage };
let ToolMessageDebugView = class ToolMessageDebugView extends LitElement {
constructor() {
super(...arguments);
this.hasResult = false;
}
createRenderRoot() {
return this; // light DOM for shared styles
}
connectedCallback() {
super.connectedCallback();
this.style.display = "block";
}
pretty(value) {
try {
if (typeof value === "string") {
const maybeJson = JSON.parse(value);
return { content: JSON.stringify(maybeJson, null, 2), isJson: true };
}
return { content: JSON.stringify(value, null, 2), isJson: true };
}
catch {
return { content: typeof value === "string" ? value : String(value), isJson: false };
}
}
render() {
const textOutput = this.result?.content
?.filter((c) => c.type === "text")
.map((c) => c.text)
.join("\n") || "";
const output = this.pretty(textOutput);
const details = this.pretty(this.result?.details);
return html `
<div class="mt-3 flex flex-col gap-2">
<div>
<div class="text-xs font-medium mb-1 text-muted-foreground">${i18n("Call")}</div>
<code-block .code=${this.pretty(this.callArgs).content} language="json"></code-block>
</div>
<div>
<div class="text-xs font-medium mb-1 text-muted-foreground">${i18n("Result")}</div>
${this.hasResult
? html `<code-block .code=${output.content} language="${output.isJson ? "json" : "text"}"></code-block>
<code-block .code=${details.content} language="${details.isJson ? "json" : "text"}"></code-block>`
: html `<div class="text-xs text-muted-foreground">${i18n("(no result)")}</div>`}
</div>
</div>
`;
}
};
__decorate([
property({ type: Object })
], ToolMessageDebugView.prototype, "callArgs", void 0);
__decorate([
property({ type: Object })
], ToolMessageDebugView.prototype, "result", void 0);
__decorate([
property({ type: Boolean })
], ToolMessageDebugView.prototype, "hasResult", void 0);
ToolMessageDebugView = __decorate([
customElement("tool-message-debug")
], ToolMessageDebugView);
export { ToolMessageDebugView };
let ToolMessage = class ToolMessage extends LitElement {
constructor() {
super(...arguments);
this.pending = false;
this.aborted = false;
this.isStreaming = false;
}
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.style.display = "block";
}
render() {
const toolName = this.tool?.name || this.toolCall.name;
// Render tool content (renderer handles errors and styling)
const result = this.aborted
? {
role: "toolResult",
isError: true,
content: [],
toolCallId: this.toolCall.id,
toolName: this.toolCall.name,
timestamp: Date.now(),
}
: this.result;
const renderResult = renderTool(toolName, this.toolCall.arguments, result, !this.aborted && (this.isStreaming || this.pending));
// Handle custom rendering (no card wrapper)
if (renderResult.isCustom) {
return renderResult.content;
}
// Default: wrap in card
return html `
<div class="p-2.5 border border-border rounded-md bg-card text-card-foreground shadow-xs">
${renderResult.content}
</div>
`;
}
};
__decorate([
property({ type: Object })
], ToolMessage.prototype, "toolCall", void 0);
__decorate([
property({ type: Object })
], ToolMessage.prototype, "tool", void 0);
__decorate([
property({ type: Object })
], ToolMessage.prototype, "result", void 0);
__decorate([
property({ type: Boolean })
], ToolMessage.prototype, "pending", void 0);
__decorate([
property({ type: Boolean })
], ToolMessage.prototype, "aborted", void 0);
__decorate([
property({ type: Boolean })
], ToolMessage.prototype, "isStreaming", void 0);
ToolMessage = __decorate([
customElement("tool-message")
], ToolMessage);
export { ToolMessage };
let AbortedMessage = class AbortedMessage extends LitElement {
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.style.display = "block";
}
render() {
return html `<span class="text-sm text-destructive italic">${i18n("Request aborted")}</span>`;
}
};
AbortedMessage = __decorate([
customElement("aborted-message")
], AbortedMessage);
export { AbortedMessage };
//# sourceMappingURL=Messages.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,16 +0,0 @@
import { LitElement } from "lit";
export declare class ProviderKeyInput extends LitElement {
provider: string;
private keyInput;
private testing;
private failed;
private hasKey;
private inputChanged;
protected createRenderRoot(): this;
connectedCallback(): Promise<void>;
private checkKeyStatus;
private testApiKey;
private saveKey;
render(): import("lit-html").TemplateResult<1>;
}
//# sourceMappingURL=ProviderKeyInput.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"ProviderKeyInput.d.ts","sourceRoot":"","sources":["../../src/components/ProviderKeyInput.ts"],"names":[],"mappings":"AAIA,OAAO,EAAQ,UAAU,EAAE,MAAM,KAAK,CAAC;AAkBvC,qBACa,gBAAiB,SAAQ,UAAU;IACnC,QAAQ,SAAM;IACjB,OAAO,CAAC,QAAQ,CAAM;IACtB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,YAAY,CAAS;IAEtC,SAAS,CAAC,gBAAgB;IAIX,iBAAiB;YAKlB,cAAc;YASd,UAAU;YAgCV,OAAO;IAiCrB,MAAM;CAqCN"}

View File

@@ -1,170 +0,0 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { i18n } from "@mariozechner/mini-lit";
import { Badge } from "@mariozechner/mini-lit/dist/Badge.js";
import { Button } from "@mariozechner/mini-lit/dist/Button.js";
import { complete, getModel } from "@mariozechner/pi-ai";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { getAppStorage } from "../storage/app-storage.js";
import { applyProxyIfNeeded } from "../utils/proxy-utils.js";
import { Input } from "./Input.js";
// Test models for each provider
const TEST_MODELS = {
anthropic: "claude-3-5-haiku-20241022",
openai: "gpt-4o-mini",
google: "gemini-2.5-flash",
groq: "openai/gpt-oss-20b",
openrouter: "z-ai/glm-4.6",
cerebras: "gpt-oss-120b",
xai: "grok-4-fast-non-reasoning",
zai: "glm-4.5-air",
};
let ProviderKeyInput = class ProviderKeyInput extends LitElement {
constructor() {
super(...arguments);
this.provider = "";
this.keyInput = "";
this.testing = false;
this.failed = false;
this.hasKey = false;
this.inputChanged = false;
}
createRenderRoot() {
return this;
}
async connectedCallback() {
super.connectedCallback();
await this.checkKeyStatus();
}
async checkKeyStatus() {
try {
const key = await getAppStorage().providerKeys.get(this.provider);
this.hasKey = !!key;
}
catch (error) {
console.error("Failed to check key status:", error);
}
}
async testApiKey(provider, apiKey) {
try {
const modelId = TEST_MODELS[provider];
// Returning true here for Ollama and friends. Can' know which model to use for testing
if (!modelId)
return true;
let model = getModel(provider, modelId);
if (!model)
return false;
// Get proxy URL from settings (if available)
const proxyEnabled = await getAppStorage().settings.get("proxy.enabled");
const proxyUrl = await getAppStorage().settings.get("proxy.url");
// Apply proxy only if this provider/key combination requires it
model = applyProxyIfNeeded(model, apiKey, proxyEnabled ? proxyUrl || undefined : undefined);
const context = {
messages: [{ role: "user", content: "Reply with: ok", timestamp: Date.now() }],
};
const result = await complete(model, context, {
apiKey,
maxTokens: 200,
});
return result.stopReason === "stop";
}
catch (error) {
console.error(`API key test failed for ${provider}:`, error);
return false;
}
}
async saveKey() {
if (!this.keyInput)
return;
this.testing = true;
this.failed = false;
const success = await this.testApiKey(this.provider, this.keyInput);
this.testing = false;
if (success) {
try {
await getAppStorage().providerKeys.set(this.provider, this.keyInput);
this.hasKey = true;
this.inputChanged = false;
this.requestUpdate();
}
catch (error) {
console.error("Failed to save API key:", error);
this.failed = true;
setTimeout(() => {
this.failed = false;
this.requestUpdate();
}, 5000);
}
}
else {
this.failed = true;
setTimeout(() => {
this.failed = false;
this.requestUpdate();
}, 5000);
}
}
render() {
return html `
<div class="space-y-3">
<div class="flex items-center gap-2">
<span class="text-sm font-medium capitalize text-foreground">${this.provider}</span>
${this.testing
? Badge({ children: i18n("Testing..."), variant: "secondary" })
: this.hasKey
? html `<span class="text-green-600 dark:text-green-400"></span>`
: ""}
${this.failed ? Badge({ children: i18n("✗ Invalid"), variant: "destructive" }) : ""}
</div>
<div class="flex items-center gap-2">
${Input({
type: "password",
placeholder: this.hasKey ? "••••••••••••" : i18n("Enter API key"),
value: this.keyInput,
onInput: (e) => {
this.keyInput = e.target.value;
this.inputChanged = true;
this.requestUpdate();
},
className: "flex-1",
})}
${Button({
onClick: () => this.saveKey(),
variant: "default",
size: "sm",
disabled: !this.keyInput || this.testing || (this.hasKey && !this.inputChanged),
children: i18n("Save"),
})}
</div>
</div>
`;
}
};
__decorate([
property()
], ProviderKeyInput.prototype, "provider", void 0);
__decorate([
state()
], ProviderKeyInput.prototype, "keyInput", void 0);
__decorate([
state()
], ProviderKeyInput.prototype, "testing", void 0);
__decorate([
state()
], ProviderKeyInput.prototype, "failed", void 0);
__decorate([
state()
], ProviderKeyInput.prototype, "hasKey", void 0);
__decorate([
state()
], ProviderKeyInput.prototype, "inputChanged", void 0);
ProviderKeyInput = __decorate([
customElement("provider-key-input")
], ProviderKeyInput);
export { ProviderKeyInput };
//# sourceMappingURL=ProviderKeyInput.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"ProviderKeyInput.js","sourceRoot":"","sources":["../../src/components/ProviderKeyInput.ts"],"names":[],"mappings":"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"}

View File

@@ -1,85 +0,0 @@
import { LitElement } from "lit";
import { type MessageConsumer } from "./sandbox/RuntimeMessageRouter.js";
import type { SandboxRuntimeProvider } from "./sandbox/SandboxRuntimeProvider.js";
export interface SandboxFile {
fileName: string;
content: string | Uint8Array;
mimeType: string;
}
export interface SandboxResult {
success: boolean;
console: Array<{
type: string;
text: string;
}>;
files?: SandboxFile[];
error?: {
message: string;
stack: string;
};
returnValue?: any;
}
/**
* Function that returns the URL to the sandbox HTML file.
* Used in browser extensions to load sandbox.html via chrome.runtime.getURL().
*/
export type SandboxUrlProvider = () => string;
/**
* Configuration for prepareHtmlDocument
*/
export interface PrepareHtmlOptions {
/** True if this is an HTML artifact (inject into existing HTML), false if REPL (wrap in HTML) */
isHtmlArtifact: boolean;
/** True if this is a standalone download (no runtime bridge, no navigation interceptor) */
isStandalone?: boolean;
}
export declare class SandboxIframe extends LitElement {
private iframe?;
/**
* Optional: Provide a function that returns the sandbox HTML URL.
* If provided, the iframe will use this URL instead of srcdoc.
* This is required for browser extensions with strict CSP.
*/
sandboxUrlProvider?: SandboxUrlProvider;
createRenderRoot(): this;
connectedCallback(): void;
disconnectedCallback(): void;
/**
* Load HTML content into sandbox and keep it displayed (for HTML artifacts)
* @param sandboxId Unique ID
* @param htmlContent Full HTML content
* @param providers Runtime providers to inject
* @param consumers Message consumers to register (optional)
*/
loadContent(sandboxId: string, htmlContent: string, providers?: SandboxRuntimeProvider[], consumers?: MessageConsumer[]): void;
private loadViaSandboxUrl;
private loadViaSrcdoc;
/**
* Execute code in sandbox
* @param sandboxId Unique ID for this execution
* @param code User code (plain JS for REPL, or full HTML for artifacts)
* @param providers Runtime providers to inject
* @param consumers Additional message consumers (optional, execute has its own internal consumer)
* @param signal Abort signal
* @returns Promise resolving to execution result
*/
execute(sandboxId: string, code: string, providers?: SandboxRuntimeProvider[], consumers?: MessageConsumer[], signal?: AbortSignal, isHtmlArtifact?: boolean): Promise<SandboxResult>;
/**
* Validate HTML using DOMParser - returns error message if invalid, null if valid
* Note: JavaScript syntax validation is done in sandbox.js to avoid CSP restrictions
*/
private validateHtml;
/**
* Prepare complete HTML document with runtime + user code
* PUBLIC so HtmlArtifact can use it for download button
*/
prepareHtmlDocument(sandboxId: string, userCode: string, providers?: SandboxRuntimeProvider[], options?: PrepareHtmlOptions): string;
/**
* Generate runtime script from providers
* @param sandboxId Unique sandbox ID
* @param providers Runtime providers
* @param isStandalone If true, skip runtime bridge and navigation interceptor (for standalone downloads)
*/
private getRuntimeScript;
}
//# sourceMappingURL=SandboxedIframe.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"SandboxedIframe.d.ts","sourceRoot":"","sources":["../../src/components/SandboxedIframe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAIjC,OAAO,EAAE,KAAK,eAAe,EAA0B,MAAM,mCAAmC,CAAC;AACjG,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAElF,MAAM,WAAW,WAAW;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,KAAK,CAAC,EAAE,WAAW,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,WAAW,CAAC,EAAE,GAAG,CAAC;CAClB;AAED;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,MAAM,CAAC;AAE9C;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC,iGAAiG;IACjG,cAAc,EAAE,OAAO,CAAC;IACxB,2FAA2F;IAC3F,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB;AAWD,qBACa,aAAc,SAAQ,UAAU;IAC5C,OAAO,CAAC,MAAM,CAAC,CAAoB;IAEnC;;;;OAIG;IAC6B,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IAExE,gBAAgB;IAIP,iBAAiB;IAIjB,oBAAoB;IAQ7B;;;;;;OAMG;IACI,WAAW,CACjB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,SAAS,GAAE,sBAAsB,EAAO,EACxC,SAAS,GAAE,eAAe,EAAO,GAC/B,IAAI;IAkDP,OAAO,CAAC,iBAAiB;IAuEzB,OAAO,CAAC,aAAa;IAyBrB;;;;;;;;OAQG;IACU,OAAO,CACnB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,SAAS,GAAE,sBAAsB,EAAO,EACxC,SAAS,GAAE,eAAe,EAAO,EACjC,MAAM,CAAC,EAAE,WAAW,EACpB,cAAc,GAAE,OAAe,GAC7B,OAAO,CAAC,aAAa,CAAC;IAmKzB;;;OAGG;IACH,OAAO,CAAC,YAAY;IAiBpB;;;OAGG;IACI,mBAAmB,CACzB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,sBAAsB,EAAO,EACxC,OAAO,CAAC,EAAE,kBAAkB,GAC1B,MAAM;IAmFT;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;CAgGxB"}

View File

@@ -1,511 +0,0 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ConsoleRuntimeProvider } from "./sandbox/ConsoleRuntimeProvider.js";
import { RuntimeMessageBridge } from "./sandbox/RuntimeMessageBridge.js";
import { RUNTIME_MESSAGE_ROUTER } from "./sandbox/RuntimeMessageRouter.js";
/**
* Escape HTML special sequences in code to prevent premature tag closure
* @param code Code that will be injected into <script> tags
* @returns Escaped code safe for injection
*/
function escapeScriptContent(code) {
return code.replace(/<\/script/gi, "<\\/script");
}
let SandboxIframe = class SandboxIframe extends LitElement {
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
}
disconnectedCallback() {
super.disconnectedCallback();
// Note: We don't unregister the sandbox here for loadContent() mode
// because the caller (HtmlArtifact) owns the sandbox lifecycle.
// For execute() mode, the sandbox is unregistered in the cleanup function.
this.iframe?.remove();
}
/**
* Load HTML content into sandbox and keep it displayed (for HTML artifacts)
* @param sandboxId Unique ID
* @param htmlContent Full HTML content
* @param providers Runtime providers to inject
* @param consumers Message consumers to register (optional)
*/
loadContent(sandboxId, htmlContent, providers = [], consumers = []) {
// Unregister previous sandbox if exists
try {
RUNTIME_MESSAGE_ROUTER.unregisterSandbox(sandboxId);
}
catch {
// Sandbox might not exist, that's ok
}
providers = [new ConsoleRuntimeProvider(), ...providers];
RUNTIME_MESSAGE_ROUTER.registerSandbox(sandboxId, providers, consumers);
// loadContent is always used for HTML artifacts (not standalone)
const completeHtml = this.prepareHtmlDocument(sandboxId, htmlContent, providers, {
isHtmlArtifact: true,
isStandalone: false,
});
// Validate HTML before loading
const validationError = this.validateHtml(completeHtml);
if (validationError) {
console.error("HTML validation failed:", validationError);
// Show error in iframe instead of crashing
this.iframe?.remove();
this.iframe = document.createElement("iframe");
this.iframe.style.cssText = "width: 100%; height: 100%; border: none;";
this.iframe.srcdoc = `
<html>
<body style="font-family: monospace; padding: 20px; background: #fff; color: #000;">
<h3 style="color: #c00;">HTML Validation Error</h3>
<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto; white-space: pre-wrap;">${validationError}</pre>
</body>
</html>
`;
this.appendChild(this.iframe);
return;
}
// Remove previous iframe if exists
this.iframe?.remove();
if (this.sandboxUrlProvider) {
// Browser extension mode: use sandbox.html with postMessage
this.loadViaSandboxUrl(sandboxId, completeHtml);
}
else {
// Web mode: use srcdoc
this.loadViaSrcdoc(sandboxId, completeHtml);
}
}
loadViaSandboxUrl(sandboxId, completeHtml) {
// Create iframe pointing to sandbox URL
this.iframe = document.createElement("iframe");
this.iframe.sandbox.add("allow-scripts");
this.iframe.sandbox.add("allow-modals");
this.iframe.style.width = "100%";
this.iframe.style.height = "100%";
this.iframe.style.border = "none";
this.iframe.src = this.sandboxUrlProvider();
// Update router with iframe reference BEFORE appending to DOM
RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
// Listen for open-external-url messages from iframe
const externalUrlHandler = (e) => {
if (e.data.type === "open-external-url" && e.source === this.iframe?.contentWindow) {
// Use chrome.tabs API to open in new tab
const chromeAPI = globalThis.chrome;
if (chromeAPI?.tabs) {
chromeAPI.tabs.create({ url: e.data.url });
}
else {
// Fallback for non-extension context
window.open(e.data.url, "_blank");
}
}
};
window.addEventListener("message", externalUrlHandler);
// Listen for sandbox-ready and sandbox-error messages directly
const readyHandler = (e) => {
if (e.data.type === "sandbox-ready" && e.source === this.iframe?.contentWindow) {
window.removeEventListener("message", readyHandler);
window.removeEventListener("message", errorHandler);
// Send content to sandbox
this.iframe?.contentWindow?.postMessage({
type: "sandbox-load",
sandboxId,
code: completeHtml,
}, "*");
}
};
const errorHandler = (e) => {
if (e.data.type === "sandbox-error" && e.source === this.iframe?.contentWindow) {
window.removeEventListener("message", readyHandler);
window.removeEventListener("message", errorHandler);
// The sandbox.js already sent us the error via postMessage.
// We need to convert it to an execution-error message that the execute() consumer will handle.
// Simulate receiving an execution-error from the sandbox
window.postMessage({
sandboxId: sandboxId,
type: "execution-error",
error: { message: e.data.error, stack: e.data.stack },
}, "*");
}
};
window.addEventListener("message", readyHandler);
window.addEventListener("message", errorHandler);
this.appendChild(this.iframe);
}
loadViaSrcdoc(sandboxId, completeHtml) {
// Create iframe with srcdoc
this.iframe = document.createElement("iframe");
this.iframe.sandbox.add("allow-scripts");
this.iframe.sandbox.add("allow-modals");
this.iframe.style.width = "100%";
this.iframe.style.height = "100%";
this.iframe.style.border = "none";
this.iframe.srcdoc = completeHtml;
// Update router with iframe reference BEFORE appending to DOM
RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
// Listen for open-external-url messages from iframe
const externalUrlHandler = (e) => {
if (e.data.type === "open-external-url" && e.source === this.iframe?.contentWindow) {
// Fallback for non-extension context
window.open(e.data.url, "_blank");
}
};
window.addEventListener("message", externalUrlHandler);
this.appendChild(this.iframe);
}
/**
* Execute code in sandbox
* @param sandboxId Unique ID for this execution
* @param code User code (plain JS for REPL, or full HTML for artifacts)
* @param providers Runtime providers to inject
* @param consumers Additional message consumers (optional, execute has its own internal consumer)
* @param signal Abort signal
* @returns Promise resolving to execution result
*/
async execute(sandboxId, code, providers = [], consumers = [], signal, isHtmlArtifact = false) {
if (signal?.aborted) {
throw new Error("Execution aborted");
}
const consoleProvider = new ConsoleRuntimeProvider();
providers = [consoleProvider, ...providers];
RUNTIME_MESSAGE_ROUTER.registerSandbox(sandboxId, providers, consumers);
// Notify providers that execution is starting
for (const provider of providers) {
provider.onExecutionStart?.(sandboxId, signal);
}
const files = [];
let completed = false;
return new Promise((resolve, reject) => {
// 4. Create execution consumer for lifecycle messages
const executionConsumer = {
async handleMessage(message) {
if (message.type === "file-returned") {
files.push({
fileName: message.fileName,
content: message.content,
mimeType: message.mimeType,
});
}
else if (message.type === "execution-complete") {
completed = true;
cleanup();
resolve({
success: true,
console: consoleProvider.getLogs(),
files,
returnValue: message.returnValue,
});
}
else if (message.type === "execution-error") {
completed = true;
cleanup();
resolve({ success: false, console: consoleProvider.getLogs(), error: message.error, files });
}
},
};
RUNTIME_MESSAGE_ROUTER.addConsumer(sandboxId, executionConsumer);
const cleanup = () => {
// Notify providers that execution has ended
for (const provider of providers) {
provider.onExecutionEnd?.(sandboxId);
}
RUNTIME_MESSAGE_ROUTER.unregisterSandbox(sandboxId);
signal?.removeEventListener("abort", abortHandler);
clearTimeout(timeoutId);
this.iframe?.remove();
this.iframe = undefined;
};
// Abort handler
const abortHandler = () => {
if (!completed) {
completed = true;
cleanup();
reject(new Error("Execution aborted"));
}
};
if (signal) {
signal.addEventListener("abort", abortHandler);
}
// Timeout handler (30 seconds)
const timeoutId = setTimeout(() => {
if (!completed) {
completed = true;
cleanup();
resolve({
success: false,
console: consoleProvider.getLogs(),
error: { message: "Execution timeout (120s)", stack: "" },
files,
});
}
}, 120000);
// 4. Prepare HTML and create iframe
const completeHtml = this.prepareHtmlDocument(sandboxId, code, providers, {
isHtmlArtifact,
isStandalone: false,
});
// 5. Validate HTML before sending to sandbox
const validationError = this.validateHtml(completeHtml);
if (validationError) {
reject(new Error(`HTML validation failed: ${validationError}`));
return;
}
if (this.sandboxUrlProvider) {
// Browser extension mode: wait for sandbox-ready
this.iframe = document.createElement("iframe");
this.iframe.sandbox.add("allow-scripts", "allow-modals");
this.iframe.style.cssText = "width: 100%; height: 100%; border: none;";
this.iframe.src = this.sandboxUrlProvider();
// Update router with iframe reference BEFORE appending to DOM
RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
// Listen for sandbox-ready and sandbox-error messages
const readyHandler = (e) => {
if (e.data.type === "sandbox-ready" && e.source === this.iframe?.contentWindow) {
window.removeEventListener("message", readyHandler);
window.removeEventListener("message", errorHandler);
// Send content to sandbox
this.iframe?.contentWindow?.postMessage({
type: "sandbox-load",
sandboxId,
code: completeHtml,
}, "*");
}
};
const errorHandler = (e) => {
if (e.data.type === "sandbox-error" && e.source === this.iframe?.contentWindow) {
window.removeEventListener("message", readyHandler);
window.removeEventListener("message", errorHandler);
// Convert sandbox-error to execution-error for the execution consumer
window.postMessage({
sandboxId: sandboxId,
type: "execution-error",
error: { message: e.data.error, stack: e.data.stack },
}, "*");
}
};
window.addEventListener("message", readyHandler);
window.addEventListener("message", errorHandler);
this.appendChild(this.iframe);
}
else {
// Web mode: use srcdoc
this.iframe = document.createElement("iframe");
this.iframe.sandbox.add("allow-scripts", "allow-modals");
this.iframe.style.cssText = "width: 100%; height: 100%; border: none; display: none;";
this.iframe.srcdoc = completeHtml;
// Update router with iframe reference BEFORE appending to DOM
RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
this.appendChild(this.iframe);
}
});
}
/**
* Validate HTML using DOMParser - returns error message if invalid, null if valid
* Note: JavaScript syntax validation is done in sandbox.js to avoid CSP restrictions
*/
validateHtml(html) {
try {
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
// Check for parser errors
const parserError = doc.querySelector("parsererror");
if (parserError) {
return parserError.textContent || "Unknown parse error";
}
return null;
}
catch (error) {
return error.message || "Unknown validation error";
}
}
/**
* Prepare complete HTML document with runtime + user code
* PUBLIC so HtmlArtifact can use it for download button
*/
prepareHtmlDocument(sandboxId, userCode, providers = [], options) {
// Default options
const opts = {
isHtmlArtifact: false,
isStandalone: false,
...options,
};
// Runtime script that will be injected
const runtime = this.getRuntimeScript(sandboxId, providers, opts.isStandalone || false);
// Only check for HTML tags if explicitly marked as HTML artifact
// For javascript_repl, userCode is JavaScript that may contain HTML in string literals
if (opts.isHtmlArtifact) {
// HTML Artifact - inject runtime into existing HTML
const headMatch = userCode.match(/<head[^>]*>/i);
if (headMatch) {
const index = headMatch.index + headMatch[0].length;
return userCode.slice(0, index) + runtime + userCode.slice(index);
}
const htmlMatch = userCode.match(/<html[^>]*>/i);
if (htmlMatch) {
const index = htmlMatch.index + htmlMatch[0].length;
return userCode.slice(0, index) + runtime + userCode.slice(index);
}
// Fallback: prepend runtime
return runtime + userCode;
}
else {
// REPL - wrap code in HTML with runtime and call complete() when done
// Escape </script> in user code to prevent premature tag closure
const escapedUserCode = escapeScriptContent(userCode);
return `<!DOCTYPE html>
<html>
<head>
${runtime}
</head>
<body>
<script type="module">
(async () => {
try {
// Wrap user code in async function to capture return value
const userCodeFunc = async () => {
${escapedUserCode}
};
const returnValue = await userCodeFunc();
// Call completion callbacks before complete()
if (window.__completionCallbacks && window.__completionCallbacks.length > 0) {
try {
await Promise.all(window.__completionCallbacks.map(cb => cb(true)));
} catch (e) {
console.error('Completion callback error:', e);
}
}
await window.complete(null, returnValue);
} catch (error) {
// Call completion callbacks before complete() (error path)
if (window.__completionCallbacks && window.__completionCallbacks.length > 0) {
try {
await Promise.all(window.__completionCallbacks.map(cb => cb(false)));
} catch (e) {
console.error('Completion callback error:', e);
}
}
await window.complete({
message: error?.message || String(error),
stack: error?.stack || new Error().stack
});
}
})();
</script>
</body>
</html>`;
}
}
/**
* Generate runtime script from providers
* @param sandboxId Unique sandbox ID
* @param providers Runtime providers
* @param isStandalone If true, skip runtime bridge and navigation interceptor (for standalone downloads)
*/
getRuntimeScript(sandboxId, providers = [], isStandalone = false) {
// Collect all data from providers
const allData = {};
for (const provider of providers) {
Object.assign(allData, provider.getData());
}
// Generate bridge code (skip if standalone)
const bridgeCode = isStandalone
? ""
: RuntimeMessageBridge.generateBridgeCode({
context: "sandbox-iframe",
sandboxId,
});
// Collect all runtime functions - pass sandboxId as string literal
const runtimeFunctions = [];
for (const provider of providers) {
runtimeFunctions.push(`(${provider.getRuntime().toString()})(${JSON.stringify(sandboxId)});`);
}
// Build script with HTML escaping
// Escape </script> to prevent premature tag closure in HTML parser
const dataInjection = Object.entries(allData)
.map(([key, value]) => {
const jsonStr = JSON.stringify(value).replace(/<\/script/gi, "<\\/script");
return `window.${key} = ${jsonStr};`;
})
.join("\n");
// TODO the font-size is needed, as chrome seems to inject a stylesheet into iframes
// found in an extension context like sidepanel, settin body { font-size: 75% }. It's
// definitely not our code doing that.
// See https://stackoverflow.com/questions/71480433/chrome-is-injecting-some-stylesheet-in-popup-ui-which-reduces-the-font-size-to-7
// Navigation interceptor (only if NOT standalone)
const navigationInterceptor = isStandalone
? ""
: `
// Navigation interceptor: prevent all navigation and open externally
(function() {
// Intercept link clicks
document.addEventListener('click', function(e) {
const link = e.target.closest('a');
if (link && link.href) {
// Check if it's an external link (not javascript: or #hash)
if (link.href.startsWith('http://') || link.href.startsWith('https://')) {
e.preventDefault();
e.stopPropagation();
window.parent.postMessage({ type: 'open-external-url', url: link.href }, '*');
}
}
}, true);
// Intercept form submissions
document.addEventListener('submit', function(e) {
const form = e.target;
if (form && form.action) {
e.preventDefault();
e.stopPropagation();
window.parent.postMessage({ type: 'open-external-url', url: form.action }, '*');
}
}, true);
// Prevent window.location changes (only if not already redefined)
try {
const originalLocation = window.location;
Object.defineProperty(window, 'location', {
get: function() { return originalLocation; },
set: function(url) {
window.parent.postMessage({ type: 'open-external-url', url: url.toString() }, '*');
}
});
} catch (e) {
// Already defined, skip
}
})();
`;
return `<style>
html, body {
font-size: initial;
}
</style>
<script>
window.sandboxId = ${JSON.stringify(sandboxId)};
${dataInjection}
${bridgeCode}
${runtimeFunctions.join("\n")}
${navigationInterceptor}
</script>`;
}
};
__decorate([
property({ attribute: false })
], SandboxIframe.prototype, "sandboxUrlProvider", void 0);
SandboxIframe = __decorate([
customElement("sandbox-iframe")
], SandboxIframe);
export { SandboxIframe };
//# sourceMappingURL=SandboxedIframe.js.map

File diff suppressed because one or more lines are too long

View File

@@ -1,18 +0,0 @@
import type { AgentTool, Message, ToolResultMessage } from "@mariozechner/pi-ai";
import { LitElement } from "lit";
export declare class StreamingMessageContainer extends LitElement {
tools: AgentTool[];
isStreaming: boolean;
pendingToolCalls?: Set<string>;
toolResultsById?: Map<string, ToolResultMessage>;
onCostClick?: () => void;
private _message;
private _pendingMessage;
private _updateScheduled;
private _immediateUpdate;
protected createRenderRoot(): HTMLElement | DocumentFragment;
connectedCallback(): void;
setMessage(message: Message | null, immediate?: boolean): void;
render(): import("lit-html").TemplateResult<1> | undefined;
}
//# sourceMappingURL=StreamingMessageContainer.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"StreamingMessageContainer.d.ts","sourceRoot":"","sources":["../../src/components/StreamingMessageContainer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,EAAQ,UAAU,EAAE,MAAM,KAAK,CAAC;AAGvC,qBAAa,yBAA0B,SAAQ,UAAU;IAC7B,KAAK,EAAE,SAAS,EAAE,CAAM;IACtB,WAAW,UAAS;IACrB,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC/B,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAC7C,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IAEhD,OAAO,CAAC,QAAQ,CAAwB;IACjD,OAAO,CAAC,eAAe,CAAwB;IAC/C,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,gBAAgB,CAAS;cAEd,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAI5D,iBAAiB,IAAI,IAAI;IAM3B,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,EAAE,SAAS,UAAQ;IAmCnD,MAAM;CAmCf"}

View File

@@ -1,117 +0,0 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { html, LitElement } from "lit";
import { property, state } from "lit/decorators.js";
export class StreamingMessageContainer extends LitElement {
constructor() {
super(...arguments);
this.tools = [];
this.isStreaming = false;
this._message = null;
this._pendingMessage = null;
this._updateScheduled = false;
this._immediateUpdate = false;
}
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.style.display = "block";
}
// Public method to update the message with batching for performance
setMessage(message, immediate = false) {
// Store the latest message
this._pendingMessage = message;
// If this is an immediate update (like clearing), apply it right away
if (immediate || message === null) {
this._immediateUpdate = true;
this._message = message;
this.requestUpdate();
// Cancel any pending updates since we're clearing
this._pendingMessage = null;
this._updateScheduled = false;
return;
}
// Otherwise batch updates for performance during streaming
if (!this._updateScheduled) {
this._updateScheduled = true;
requestAnimationFrame(async () => {
// Only apply the update if we haven't been cleared
if (!this._immediateUpdate && this._pendingMessage !== null) {
// Deep clone the message to ensure Lit detects changes in nested properties
// (like toolCall.arguments being mutated during streaming)
this._message = JSON.parse(JSON.stringify(this._pendingMessage));
this.requestUpdate();
}
// Reset for next batch
this._pendingMessage = null;
this._updateScheduled = false;
this._immediateUpdate = false;
});
}
}
render() {
// Show loading indicator if loading but no message yet
if (!this._message) {
if (this.isStreaming)
return html `<div class="flex flex-col gap-3 mb-3">
<span class="mx-4 inline-block w-2 h-4 bg-muted-foreground animate-pulse"></span>
</div>`;
return html ``; // Empty until a message is set
}
const msg = this._message;
if (msg.role === "toolResult") {
// Skip standalone tool result in streaming; the stable list will render paired tool-message
return html ``;
}
else if (msg.role === "user") {
// Skip standalone tool result in streaming; the stable list will render it immediiately
return html ``;
}
else if (msg.role === "assistant") {
// Assistant message - render inline tool messages during streaming
return html `
<div class="flex flex-col gap-3 mb-3">
<assistant-message
.message=${msg}
.tools=${this.tools}
.isStreaming=${this.isStreaming}
.pendingToolCalls=${this.pendingToolCalls}
.toolResultsById=${this.toolResultsById}
.hideToolCalls=${false}
.onCostClick=${this.onCostClick}
></assistant-message>
${this.isStreaming ? html `<span class="mx-4 inline-block w-2 h-4 bg-muted-foreground animate-pulse"></span>` : ""}
</div>
`;
}
}
}
__decorate([
property({ type: Array })
], StreamingMessageContainer.prototype, "tools", void 0);
__decorate([
property({ type: Boolean })
], StreamingMessageContainer.prototype, "isStreaming", void 0);
__decorate([
property({ type: Object })
], StreamingMessageContainer.prototype, "pendingToolCalls", void 0);
__decorate([
property({ type: Object })
], StreamingMessageContainer.prototype, "toolResultsById", void 0);
__decorate([
property({ attribute: false })
], StreamingMessageContainer.prototype, "onCostClick", void 0);
__decorate([
state()
], StreamingMessageContainer.prototype, "_message", void 0);
// Register custom element
if (!customElements.get("streaming-message-container")) {
customElements.define("streaming-message-container", StreamingMessageContainer);
}
//# sourceMappingURL=StreamingMessageContainer.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"StreamingMessageContainer.js","sourceRoot":"","sources":["../../src/components/StreamingMessageContainer.ts"],"names":[],"mappings":"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"}

View File

@@ -1,11 +0,0 @@
import { LitElement } from "lit";
export declare class ThinkingBlock extends LitElement {
content: string;
isStreaming: boolean;
private isExpanded;
protected createRenderRoot(): HTMLElement | DocumentFragment;
connectedCallback(): void;
private toggleExpanded;
render(): import("lit-html").TemplateResult<1>;
}
//# sourceMappingURL=ThinkingBlock.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"ThinkingBlock.d.ts","sourceRoot":"","sources":["../../src/components/ThinkingBlock.ts"],"names":[],"mappings":"AACA,OAAO,EAAQ,UAAU,EAAE,MAAM,KAAK,CAAC;AAIvC,qBACa,aAAc,SAAQ,UAAU;IAChC,OAAO,EAAG,MAAM,CAAC;IACA,WAAW,UAAS;IACxC,OAAO,CAAC,UAAU,CAAS;cAEjB,gBAAgB,IAAI,WAAW,GAAG,gBAAgB;IAI5D,iBAAiB,IAAI,IAAI;IAKlC,OAAO,CAAC,cAAc;IAIb,MAAM;CAkBf"}

View File

@@ -1,58 +0,0 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { icon } from "@mariozechner/mini-lit";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ChevronRight } from "lucide";
let ThinkingBlock = class ThinkingBlock extends LitElement {
constructor() {
super(...arguments);
this.isStreaming = false;
this.isExpanded = false;
}
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.style.display = "block";
}
toggleExpanded() {
this.isExpanded = !this.isExpanded;
}
render() {
const shimmerClasses = this.isStreaming
? "animate-shimmer bg-gradient-to-r from-muted-foreground via-foreground to-muted-foreground bg-[length:200%_100%] bg-clip-text text-transparent"
: "";
return html `
<div class="thinking-block">
<div
class="thinking-header cursor-pointer select-none flex items-center gap-2 py-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
@click=${this.toggleExpanded}
>
<span class="transition-transform inline-block ${this.isExpanded ? "rotate-90" : ""}">${icon(ChevronRight, "sm")}</span>
<span class="${shimmerClasses}">Thinking...</span>
</div>
${this.isExpanded ? html `<markdown-block .content=${this.content} .isThinking=${true}></markdown-block>` : ""}
</div>
`;
}
};
__decorate([
property()
], ThinkingBlock.prototype, "content", void 0);
__decorate([
property({ type: Boolean })
], ThinkingBlock.prototype, "isStreaming", void 0);
__decorate([
state()
], ThinkingBlock.prototype, "isExpanded", void 0);
ThinkingBlock = __decorate([
customElement("thinking-block")
], ThinkingBlock);
export { ThinkingBlock };
//# sourceMappingURL=ThinkingBlock.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"ThinkingBlock.js","sourceRoot":"","sources":["../../src/components/ThinkingBlock.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAC9C,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAG/B,IAAM,aAAa,GAAnB,MAAM,aAAc,SAAQ,UAAU;IAAtC;;QAEuB,gBAAW,GAAG,KAAK,CAAC;QAChC,eAAU,GAAG,KAAK,CAAC;IAiCrC,CAAC;IA/BmB,gBAAgB;QAClC,OAAO,IAAI,CAAC;IACb,CAAC;IAEQ,iBAAiB;QACzB,KAAK,CAAC,iBAAiB,EAAE,CAAC;QAC1B,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,OAAO,CAAC;IAC9B,CAAC;IAEO,cAAc;QACrB,IAAI,CAAC,UAAU,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC;IACpC,CAAC;IAEQ,MAAM;QACd,MAAM,cAAc,GAAG,IAAI,CAAC,WAAW;YACtC,CAAC,CAAC,+IAA+I;YACjJ,CAAC,CAAC,EAAE,CAAC;QAEN,OAAO,IAAI,CAAA;;;;cAIC,IAAI,CAAC,cAAc;;sDAEqB,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC;oBACjG,cAAc;;MAE5B,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAA,4BAA4B,IAAI,CAAC,OAAO,gBAAgB,IAAI,oBAAoB,CAAC,CAAC,CAAC,EAAE;;GAE9G,CAAC;IACH,CAAC;CACD,CAAA;AAnCY;IAAX,QAAQ,EAAE;8CAAkB;AACA;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;kDAAqB;AAChC;IAAhB,KAAK,EAAE;iDAA4B;AAHxB,aAAa;IADzB,aAAa,CAAC,gBAAgB,CAAC;GACnB,aAAa,CAoCzB"}

View File

@@ -1,12 +0,0 @@
import type { TemplateResult } from "lit";
import type { AppMessage } from "./Messages.js";
export type MessageRole = AppMessage["role"];
export interface MessageRenderer<TMessage extends AppMessage = AppMessage> {
render(message: TMessage): TemplateResult;
}
export declare function registerMessageRenderer<TRole extends MessageRole>(role: TRole, renderer: MessageRenderer<Extract<AppMessage, {
role: TRole;
}>>): void;
export declare function getMessageRenderer(role: MessageRole): MessageRenderer | undefined;
export declare function renderMessage(message: AppMessage): TemplateResult | undefined;
//# sourceMappingURL=message-renderer-registry.d.ts.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"message-renderer-registry.d.ts","sourceRoot":"","sources":["../../src/components/message-renderer-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,KAAK,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGhD,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;AAG7C,MAAM,WAAW,eAAe,CAAC,QAAQ,SAAS,UAAU,GAAG,UAAU;IACxE,MAAM,CAAC,OAAO,EAAE,QAAQ,GAAG,cAAc,CAAC;CAC1C;AAKD,wBAAgB,uBAAuB,CAAC,KAAK,SAAS,WAAW,EAChE,IAAI,EAAE,KAAK,EACX,QAAQ,EAAE,eAAe,CAAC,OAAO,CAAC,UAAU,EAAE;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC,CAAC,GAC7D,IAAI,CAEN;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,GAAG,eAAe,GAAG,SAAS,CAEjF;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,UAAU,GAAG,cAAc,GAAG,SAAS,CAE7E"}

Some files were not shown because too many files have changed in this diff Show More