feat: shell terminal, virtual keyboard fixes, settings font slider
- 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>
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user