Files
clawtap/src/lib/api.ts
T
izackp c61712d8c4 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>
2026-06-19 18:04:44 -04:00

180 lines
5.6 KiB
TypeScript

import { STORAGE } from './storage-keys';
const BASE = '';
function getToken(): string | null {
return localStorage.getItem(STORAGE.TOKEN);
}
export function setToken(token: string) {
localStorage.setItem(STORAGE.TOKEN, token);
}
export function clearToken() {
localStorage.removeItem(STORAGE.TOKEN);
}
/** Fired when the server rejects the stored token (expired/invalid). */
export const AUTH_EXPIRED_EVENT = 'clawtap:auth-expired';
function handleAuthExpired() {
clearToken();
window.dispatchEvent(new Event(AUTH_EXPIRED_EVENT));
}
export function isAuthenticated(): boolean {
return !!getToken();
}
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
const token = getToken();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...((options.headers as Record<string, string>) || {}),
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const res = await fetch(`${BASE}${path}`, { ...options, headers });
const refreshed = res.headers.get('X-Refreshed-Token');
if (refreshed) {
setToken(refreshed);
}
if (!res.ok) {
if ((res.status === 401 || res.status === 403) && token) {
handleAuthExpired();
}
const body = await res.json().catch(() => ({}));
throw new Error(body.error || `HTTP ${res.status}`);
}
return res.json();
}
export const api = {
login: (password: string) =>
request<{ token: string }>('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ password }),
}),
sessions: (dir?: string, limit?: number) => {
const params = new URLSearchParams();
if (dir) params.set('dir', dir);
if (limit) params.set('limit', String(limit));
const qs = params.toString();
return request<any[]>(`/api/sessions${qs ? `?${qs}` : ''}`);
},
sessionMessages: (id: string, dir?: string) => {
const qs = dir ? `?dir=${encodeURIComponent(dir)}` : '';
return request<{ messages: any[]; lastModified: string | null }>(`/api/sessions/${id}/messages${qs}`);
},
uploadImage: async (file: File): Promise<{ path: string; filename: string }> => {
const token = getToken();
const form = new FormData();
form.append('image', file);
const res = await fetch('/api/upload', {
method: 'POST',
headers: token ? { Authorization: `Bearer ${token}` } : {},
body: form,
});
if (!res.ok) {
if ((res.status === 401 || res.status === 403) && token) {
handleAuthExpired();
}
throw new Error('Upload failed');
}
return res.json();
},
browse: (path?: string) => {
const qs = path ? `?path=${encodeURIComponent(path)}` : '';
return request<{ name: string; path: string; hasChildren: boolean }[]>(
`/api/browse${qs}`
);
},
adapters: () =>
request<{ id: string; displayName: string; available: boolean }[]>('/api/adapters'),
adapterConfig: (name: string) =>
request<{
models: { value: string; label: string; contextWindow: number }[];
permissionModes: { value: string; label: string }[];
effortLevels: { value: string; label: string }[];
effortLabel: string;
capabilities?: {
supportsPermissionModes: boolean;
permissionModeType?: 'cycle' | 'toggle';
};
}>(`/api/adapter/${name}/config`),
activeSessions: () =>
request<any[]>('/api/active-sessions'),
destroySession: (sessionId: string, adapter?: string) => {
const qs = adapter ? `?adapter=${adapter}` : '';
return request<{ ok: boolean }>(`/api/active-sessions/${sessionId}${qs}`, { method: 'DELETE' });
},
vapidPublicKey: () =>
request<{ publicKey: string }>('/api/push/vapid-public-key'),
pushSubscribe: (subscription: PushSubscriptionJSON, deviceId: string) =>
request<{ ok: boolean }>('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify({ subscription, deviceId }),
}),
pushUnsubscribe: (endpoint: string) =>
request<{ ok: boolean }>('/api/push/unsubscribe', {
method: 'POST',
body: JSON.stringify({ endpoint }),
}),
pushPending: () =>
request<Record<string, number>>('/api/push/pending'),
registerReview: (params: { reviewId: string; parentCliSessionId: string; childSessionId: string; targetAdapter: string; anchorMessageId: string; prompt: string; title: string }) =>
request<{ reviewId: string }>('/api/reviews/register', {
method: 'POST',
body: JSON.stringify(params),
}),
endReview: (reviewId: string, endAnchorMessageId?: string) =>
request<{ ok: boolean }>(`/api/reviews/${reviewId}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ endAnchorMessageId }),
}),
sendBackToParent: (reviewId: string, message: string) =>
request<{ ok: boolean }>(`/api/reviews/${reviewId}/send-back`, { method: 'POST', body: JSON.stringify({ message }) }),
getReviews: (parentCliSessionId: string) =>
request<any[]>(`/api/reviews?parentCliSessionId=${encodeURIComponent(parentCliSessionId)}`),
getInstructions: () =>
request<{ id: string; label: string; instruction: string; created_at: string }[]>('/api/instructions'),
createInstruction: (label: string, instruction: string) =>
request<{ id: string; label: string; instruction: string }>('/api/instructions', {
method: 'POST',
body: JSON.stringify({ label, instruction }),
}),
deleteInstruction: (id: string) =>
request<void>(`/api/instructions/${id}`, { method: 'DELETE' }),
newShellTerminal: (cwd: string) =>
request<{ sessionId: string }>('/api/terminal/shell', {
method: 'POST',
body: JSON.stringify({ cwd }),
}),
};