c61712d8c4
- Add shell terminal: new tmux window spawned at project cwd, opened from the Terminal icon in the ChatView header; ShellTerminalView lets you navigate back to chat or jump to the Claude tmux terminal mode - Fix system keyboard appearing on first terminal launch: ghostty-web calls focus() on a contenteditable div inside open(), triggering iOS keyboard before we can suppress it; patch HTMLElement.prototype.focus as a no-op for the synchronous duration of terminal.open() - Add keyboard font-size slider in Settings - Fix virtual keyboard button corner radius and depth scaling with key height - Fix keybar gap/padding proportionality via heightScale factor - Cap keybar font size so it does not grow when bar height is increased Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
145 lines
4.4 KiB
TypeScript
145 lines
4.4 KiB
TypeScript
export type FrontendLogLevel = "debug" | "info" | "warn" | "error";
|
|
|
|
export interface WebtermScopedLogger {
|
|
debug: (text: string, extra?: Record<string, unknown>) => void;
|
|
info: (text: string, extra?: Record<string, unknown>) => void;
|
|
warn: (text: string, extra?: Record<string, unknown>) => void;
|
|
error: (text: string, extra?: Record<string, unknown>) => void;
|
|
}
|
|
|
|
declare global {
|
|
interface Window {
|
|
webtermClientLog?: (text: string, extra?: Record<string, unknown>) => Promise<void>;
|
|
webtermClientLogSessionID?: string;
|
|
webtermClientBuildVersion?: string;
|
|
webtermLogs?: {
|
|
bug: WebtermScopedLogger;
|
|
ui: WebtermScopedLogger;
|
|
voice: WebtermScopedLogger;
|
|
ws: WebtermScopedLogger;
|
|
};
|
|
}
|
|
}
|
|
|
|
export const FRONTEND_BUILD_VERSION = "clawtap";
|
|
|
|
function createFrontendLogSessionID(): string {
|
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
return crypto.randomUUID();
|
|
}
|
|
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
let out = "";
|
|
for (let i = 0; i < 24; i += 1) {
|
|
out += chars[Math.floor(Math.random() * chars.length)];
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export const frontendLogSessionID = createFrontendLogSessionID();
|
|
|
|
function normalizeFrontendLogValue(value: unknown): unknown {
|
|
if (value instanceof Error) {
|
|
return {
|
|
name: value.name,
|
|
message: value.message,
|
|
stack: value.stack,
|
|
};
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return value.map((item) => normalizeFrontendLogValue(item));
|
|
}
|
|
if (value && typeof value === "object") {
|
|
return Object.fromEntries(
|
|
Object.entries(value as Record<string, unknown>).map(([key, item]) => [key, normalizeFrontendLogValue(item)])
|
|
);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function serializeFrontendLogContext(extra: Record<string, unknown> = {}): string {
|
|
const entries = Object.entries(extra);
|
|
if (entries.length === 0) {
|
|
return "";
|
|
}
|
|
try {
|
|
return JSON.stringify(Object.fromEntries(entries.map(([key, value]) => [key, normalizeFrontendLogValue(value)])));
|
|
} catch {
|
|
return String(extra);
|
|
}
|
|
}
|
|
|
|
export async function postFrontendLog(
|
|
level: FrontendLogLevel,
|
|
text: string,
|
|
extra: Record<string, unknown> = {}
|
|
): Promise<void> {
|
|
const message = typeof text === "string" ? text.trim() : "";
|
|
if (!message) return;
|
|
try {
|
|
await fetch("/api/client-log", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
credentials: "same-origin",
|
|
body: JSON.stringify({
|
|
session_id: frontendLogSessionID,
|
|
text: message,
|
|
frontend_version: FRONTEND_BUILD_VERSION,
|
|
level,
|
|
context: serializeFrontendLogContext(extra),
|
|
}),
|
|
keepalive: true,
|
|
});
|
|
} catch (error) {
|
|
console.warn("[webterm] Failed to post client log:", error);
|
|
}
|
|
}
|
|
|
|
function logFrontend(level: FrontendLogLevel, text: string, extra: Record<string, unknown> = {}): void {
|
|
void postFrontendLog(level, text, extra);
|
|
}
|
|
|
|
function logFrontendDebug(text: string, extra: Record<string, unknown> = {}): void {
|
|
logFrontend("debug", text, extra);
|
|
}
|
|
|
|
function logFrontendInfo(text: string, extra: Record<string, unknown> = {}): void {
|
|
logFrontend("info", text, extra);
|
|
}
|
|
|
|
function logFrontendWarn(text: string, extra: Record<string, unknown> = {}): void {
|
|
logFrontend("warn", text, extra);
|
|
}
|
|
|
|
function logFrontendError(text: string, extra: Record<string, unknown> = {}): void {
|
|
logFrontend("error", text, extra);
|
|
}
|
|
|
|
export function createScopedFrontendLogger(scope: string): WebtermScopedLogger {
|
|
const withScope = (extra: Record<string, unknown> = {}): Record<string, unknown> => ({
|
|
scope,
|
|
...extra,
|
|
});
|
|
return {
|
|
debug: (text, extra = {}) => logFrontendDebug(text, withScope(extra)),
|
|
info: (text, extra = {}) => logFrontendInfo(text, withScope(extra)),
|
|
warn: (text, extra = {}) => logFrontendWarn(text, withScope(extra)),
|
|
error: (text, extra = {}) => logFrontendError(text, withScope(extra)),
|
|
};
|
|
}
|
|
|
|
export const bugLog = createScopedFrontendLogger("bug");
|
|
export const uiLog = createScopedFrontendLogger("ui");
|
|
export const voiceLog = createScopedFrontendLogger("voice");
|
|
export const wsLog = createScopedFrontendLogger("ws");
|
|
|
|
window.webtermClientLog = (text: string, extra: Record<string, unknown> = {}) =>
|
|
postFrontendLog("info", text, extra);
|
|
window.webtermClientLogSessionID = frontendLogSessionID;
|
|
window.webtermClientBuildVersion = FRONTEND_BUILD_VERSION;
|
|
window.webtermLogs = {
|
|
bug: bugLog,
|
|
ui: uiLog,
|
|
voice: voiceLog,
|
|
ws: wsLog,
|
|
};
|