mezame 0.8.42

An ACP client that bridges a local agent (Kiro CLI, Claude Agent CLI, Gemini CLI, Codex, ...) to a browser UI over WebSockets.
import { MenuIcon } from 'lucide-react';
import { useEffect, useState } from 'react';
import { Button } from '@/components/ui/button';
import { InputRow } from '@/features/InputRow';
import { LogPane } from '@/features/LogPane';
import { NewSessionDialog } from '@/features/NewSessionDialog';
import { NotificationsPrompt } from '@/features/NotificationsPrompt';
import { SideBar } from '@/features/SideBar';
import { useAttentionBadge } from '@/hooks/useAttentionBadge';
import { useKeyboardInset } from '@/hooks/useKeyboardInset';
import { useNotifications } from '@/hooks/useNotifications';
import { mezameActions, useMezame } from '@/hooks/useMezame';
import { initSettings } from '@/lib/settings';

export const App = () => {
  const { sessions, closed, activeId, activeSession } = useMezame();
  const [newSessionOpen, setNewSessionOpen] = useState(false);
  // Mobile-only: drawer state for the sidebar. Desktop ignores it
  // (the sidebar is always rendered and the transform-based hiding
  // is overridden at `md:`).
  const [sidebarOpen, setSidebarOpen] = useState(false);

  useAttentionBadge();
  useKeyboardInset();
  useNotifications();

  // Mirror the browser tab's visibility onto
  // `<html data-visibility="visible|hidden">` so CSS can pause
  // animations when the user has switched away. See
  // `.tab-busy-border` in index.css.
  useEffect(() => {
    const onVisibility = () => {
      document.documentElement.dataset.visibility = document.visibilityState;
    };
    document.addEventListener('visibilitychange', onVisibility);
    onVisibility();
    return () => document.removeEventListener('visibilitychange', onVisibility);
  }, []);

  useEffect(() => {
    void mezameActions.init();
    void initSettings();
  }, []);

  return (
    <div
      className="flex h-full h-[100dvh] min-h-0"
      style={{
        // Top/left/right safe-area padding on the shell is handled
        // per-region: the sidebar owns its own top/bottom/left safe
        // area, and the main column owns top/right.
        paddingRight: 'var(--mz-safe-right)'
      }}
    >
      <SideBar
        sessions={sessions}
        activeId={activeId}
        closed={closed}
        onActivate={mezameActions.activate}
        onClose={mezameActions.closeSession}
        onRename={mezameActions.renameSession}
        onRestore={mezameActions.restoreFromHistory}
        onForget={mezameActions.forgetHistory}
        onNewTab={() => setNewSessionOpen(true)}
        isOpen={sidebarOpen}
        onRequestClose={() => setSidebarOpen(false)}
      />

      {/* Main column: the chat pane. Centred and width-capped so long
       * lines stay readable on ultra-wide monitors. Relative so the
       * floating composer inside can anchor to it. The desktop left
       * margin reserves space for the floating sidebar (it lives on
       * `position: fixed` so the chat column would otherwise slide
       * under it). The custom property is updated live by
       * `useSidebarWidth` while the user drags. Mobile keeps the
       * full width since the sidebar is a drawer there. */}
      <main
        className="relative flex min-h-0 flex-1 flex-col"
        style={{
          paddingTop: 'calc(20px + var(--mz-safe-top))',
          paddingRight: '20px',
          paddingBottom: '20px',
          marginLeft: 'var(--mz-main-left, 0)'
        }}
      >
        {/* Mobile burger: pinned top-left of the main column, hidden on
         * desktop where the sidebar is always visible. */}
        <div
          className="absolute left-3 top-3 z-20 md:hidden"
          style={{ top: 'calc(0.75rem + var(--mz-safe-top))' }}
        >
          <Button
            size="icon"
            variant="outline"
            className="size-10 rounded-full text-[color:var(--primary)]"
            onClick={() => setSidebarOpen(true)}
            aria-label="Open sidebar"
          >
            <MenuIcon className="size-5" />
          </Button>
        </div>

        <div className="mx-auto flex w-full min-h-0 flex-1 flex-col">
          {sessions.map((s) => (
            <LogPane key={s.id} session={s} isActive={s.id === activeId} />
          ))}

          <InputRow session={activeSession} onSubmit={mezameActions.sendPrompt} />
        </div>
      </main>

      <NewSessionDialog
        open={newSessionOpen}
        onOpenChange={setNewSessionOpen}
        onCreate={(cwd, name) => mezameActions.newSession(cwd, name)}
      />
      <NotificationsPrompt />
    </div>
  );
};