diff --git a/docs/index.es.html b/docs/index.es.html index e2e4f0cc5..c62a4c2c4 100644 --- a/docs/index.es.html +++ b/docs/index.es.html @@ -119,28 +119,31 @@

Un ADE de terminal para los agentes de código que ya usas

+ -

Una tarea, un worktree, una sesión. Escribe el número de un issue: lich nombra la rama a partir de su título, crea el worktree desde tu base y le da el issue al agente como primer prompt.

@@ -201,6 +222,7 @@

New worktree session

A separate checkout on its own branc

Lee lo que escribió el agente, justo donde lo escribió. Selecciona líneas, deja un comentario y todo el lote vuelve a esa sesión como un solo prompt, que envías tú.

Los pull requests del repositorio, filtrados con los mismos calificadores de GitHub. Haz checkout de uno en un worktree, revísalo sin salir de lich y fusiónalo con los métodos que permita la rama base.

Una sesión le pasa una tarea a otra. Va por lich send o por las herramientas MCP de lich, y la respuesta vuelve a la sesión que la pidió.

+

El agente usa un navegador que ves. Cuando abre uno, la sesión gana una pestaña con su terminal junto a la página; el cursor de lich muestra cada clic, desplazamiento y tecla que envía, y puedes tomar la página cuando quieras.

@@ -231,10 +253,7 @@

Todo lo que rodea al agente

Grupos que se acomodan solos

Pon sesiones una al lado de la otra y lich elige la cuadrícula según cuántas haya y el espacio que tenga la ventana. Ponle nombre a un grupo y ten todos los que quieras.

-

Una terminal de verdad

Shells sobre un PTY renderizadas en la GPU, con un historial en el que puedes buscar y que sobrevive al cierre de la ventana. Dale un comando de entrada y se abre directamente en lazygit o pnpm dev.

+

Services que siguen corriendo

Guarda una vez por proyecto un servidor de desarrollo, un contenedor o un túnel. lich lo arranca con el proyecto y lo mantiene corriendo; la lista al pie de la barra lateral muestra cada uno, abre su consola y lo detiene.

Tu plan en el pie de la ventana

La ventana de contexto, cuánto te queda de la ventana móvil de tu plan y cuánto lleva gastado la sesión, leído de lo que cada agente deja registrado.

Varias cuentas, una tarjeta

Ten tu login personal y el del trabajo lado a lado, como perfiles. Una sesión que pasas a otra cuenta sigue en la misma tarjeta.

Bifurca una conversación

Bifurca la conversación de una sesión a un worktree nuevo, o déjala en su propio checkout, y prueba la otra idea sin perder la primera.

diff --git a/docs/index.html b/docs/index.html index f14645fef..f91434a32 100644 --- a/docs/index.html +++ b/docs/index.html @@ -119,28 +119,31 @@

A terminal-first ADE for the coding agents you already use

+ -

One task, one worktree, one session. Type an issue number: lich names the branch after its title, creates the worktree from your base and gives the issue to the agent as its first prompt.

@@ -201,6 +222,7 @@

New worktree session

A separate checkout on its own branc

Read what the agent wrote, where it wrote it. Select lines, leave a comment, and the batch goes back to that session as one prompt you send yourself.

The repository's pull requests, filtered with GitHub's own qualifiers. Check one out into a worktree, review it inline and merge it with the methods the base branch allows.

One session hands another a task. It goes through lich send or lich's MCP tools, and the answer comes back to the session that asked.

+

The agent drives a browser you can see. When it opens one, the session gets a tab with its terminal beside the page; lich's own cursor shows each click, scroll and key it sends, and you can take the page over at any time.

@@ -231,10 +253,7 @@

Everything around the agent

Walls that lay themselves out

Put sessions side by side and lich picks the grid from how many there are and the room the window has. Name a wall, keep as many as you like.

-

A real terminal

PTY shells rendered on the GPU, with searchable scrollback that survives closing the window. Give one an entrypoint and it opens straight into lazygit or pnpm dev.

+

Services that keep running

Save a dev server, a container or a tunnel once per project. lich starts it with the project and keeps it running; the list at the foot of the sidebar shows each one, opens its console and stops it.

Your plan in the footer

The context window, how much of your plan's rolling window is left and what the session has spent, read from what each agent writes down.

Several accounts, one card

Keep a personal and a work login side by side as profiles. A session moved to another account carries on in the same card.

Fork a conversation

Branch a session's conversation into a new worktree, or keep it in its own checkout, and try the other idea without losing the first.

diff --git a/docs/index.ja.html b/docs/index.ja.html index d777c2465..0403c6763 100644 --- a/docs/index.ja.html +++ b/docs/index.ja.html @@ -119,28 +119,31 @@

いま使っているコーディングエージェントのための、タ + -

1つのタスクに、1つのワークツリーと1つのセッション。Issue 番号を入力すると、lich がそのタイトルからブランチ名を付け、ベースブランチからワークツリーを作り、Issue をエージェントへの最初のプロンプトとして渡します。

@@ -201,6 +222,7 @@

New worktree session

A separate checkout on its own branc

エージェントの変更を、書かれたその場所で確認できます。行を選んでコメントを付けると、まとめて1つのプロンプトとしてそのセッションに戻ります。送信は手動です。

リポジトリのプルリクエストを、GitHub と同じ検索修飾子で絞り込めます。ワークツリーにチェックアウトしてインラインでレビューし、ベースブランチで許可されているマージ方法でマージできます。

セッションから別のセッションへタスクを渡せます。lich send か lich の MCP ツールで渡し、回答は依頼したセッションに戻ってきます。

+

エージェントは、あなたにも見えるブラウザを操作します。ブラウザを開くとセッションにタブが追加され、ターミナルとページが並びます。lich のカーソルがクリック、スクロール、キー入力のたびにその位置を示し、いつでもページを自分で操作できます。

@@ -231,10 +253,7 @@

エージェントのまわりは、すべて lich に

自動で整うレイアウト

セッションを並べると、その数とウィンドウの広さに合わせて lich がグリッドを決めます。レイアウトには名前を付けられ、いくつでも保存できます。

-

本物のターミナル

GPU で描画する PTY シェルです。スクロールバックは検索でき、ウィンドウを閉じても残ります。エントリポイントを指定すれば、lazygit や pnpm dev がそのまま立ち上がります。

+

動かし続ける Services

開発サーバー、コンテナ、トンネルをプロジェクトごとに一度登録すれば、lich がプロジェクトと一緒に起動し、動かし続けます。サイドバー下部の一覧で状態を確認し、コンソールを開いたり停止したりできます。

プランの使用量はフッターに

コンテキストウィンドウ、プランの利用枠(ローリングウィンドウ)の残り、セッションの使用額を、各エージェント自身が書き出す記録から読み取って表示します。

複数のアカウントを1枚のカードで

個人用と仕事用のログインを、プロファイルとして並べて使えます。別のアカウントに移したセッションも、同じカードのまま続きます。

会話をフォーク

セッションの会話を新しいワークツリーに分岐させることも、同じチェックアウトのまま分けることもできます。最初の案を残したまま、別の案を試せます。

diff --git a/docs/index.ko.html b/docs/index.ko.html index f6ff728a1..1d3bc4d8b 100644 --- a/docs/index.ko.html +++ b/docs/index.ko.html @@ -119,28 +119,31 @@

이미 쓰고 있는 코딩 에이전트를 위한 터미널 중심 ADE

+ -

작업 하나, worktree 하나, 세션 하나. 이슈 번호를 입력하면 lich가 이슈 제목으로 브랜치 이름을 짓고, 지정한 베이스에서 worktree를 만든 다음, 이슈를 에이전트의 첫 프롬프트로 넘깁니다.

@@ -201,6 +222,7 @@

New worktree session

A separate checkout on its own branc

에이전트가 작성한 코드를 작성한 그 자리에서 읽으세요. 줄을 선택해 코멘트를 남기면 모아 둔 코멘트가 프롬프트 하나로 묶여 해당 세션으로 돌아갑니다. 전송은 직접 하면 됩니다.

저장소의 풀 리퀘스트를 GitHub 검색 한정자를 그대로 써서 걸러 보세요. PR을 worktree에 체크아웃해 인라인으로 리뷰하고, 베이스 브랜치가 허용하는 방식으로 병합하세요.

세션이 다른 세션에 작업을 넘깁니다. lich send나 lich의 MCP 도구를 거쳐 전달되고, 답은 요청한 세션으로 돌아옵니다.

+

에이전트가 여러분도 보는 브라우저를 조작합니다. 브라우저를 열면 세션에 탭이 생겨 터미널과 페이지가 나란히 놓이고, lich의 커서가 클릭, 스크롤, 키 입력마다 위치를 보여 줍니다. 언제든 페이지를 직접 넘겨받을 수 있습니다.

@@ -231,10 +253,7 @@

에이전트 곁에 필요한 모든 것

알아서 배치되는 레이아웃

세션을 나란히 놓으면 lich가 세션 수와 창의 공간에 맞춰 그리드를 고릅니다. 레이아웃에 이름을 붙여 원하는 만큼 둘 수 있습니다.

-

진짜 터미널

GPU로 렌더링하는 PTY 셸에, 창을 닫아도 남는 검색 가능한 스크롤백까지 갖췄습니다. 진입점을 지정하면 lazygit이나 pnpm dev로 바로 열립니다.

+

계속 실행되는 Services

개발 서버, 컨테이너, 터널을 프로젝트마다 한 번 저장해 두면 lich가 프로젝트와 함께 실행하고 계속 유지합니다. 사이드바 아래쪽 목록에서 각 항목을 보고, 콘솔을 열고, 멈출 수 있습니다.

하단 바에서 보는 플랜

컨텍스트 윈도, 플랜의 롤링 윈도에 남은 사용량, 세션이 쓴 비용을 각 에이전트가 기록하는 내용에서 읽어 옵니다.

계정은 여러 개, 카드는 하나

개인 계정과 회사 계정을 프로필로 나란히 둘 수 있습니다. 다른 계정으로 옮긴 세션도 같은 카드에서 그대로 이어집니다.

대화 포크

세션의 대화를 새 worktree로 갈라내거나 같은 체크아웃에 그대로 두고, 처음 아이디어를 잃지 않은 채 다른 아이디어를 시도해 보세요.

diff --git a/docs/index.pt-BR.html b/docs/index.pt-BR.html index b32bfc457..e89ed75f6 100644 --- a/docs/index.pt-BR.html +++ b/docs/index.pt-BR.html @@ -119,28 +119,31 @@

Um ADE para quem trabalha no terminal, com os agentes que você já usa

+ -

Uma tarefa, uma worktree, uma sessão. Digite o número de uma issue: o lich dá à branch o nome do título dela, cria a worktree a partir da sua base e entrega a issue ao agente como primeiro prompt.

@@ -201,6 +222,7 @@

New worktree session

A separate checkout on its own branc

Leia o que o agente mudou, direto no diff. Selecione as linhas e comente. Os comentários voltam juntos para a sessão num prompt só, que você envia quando quiser.

Os pull requests do repo, com os mesmos filtros de busca do GitHub. Faça checkout de um PR numa worktree, revise inline e faça o merge com os métodos que a branch base aceita.

Uma sessão passa tarefa para outra. O pedido vai pelo lich send ou pelas ferramentas MCP do lich, e a resposta volta para a sessão que pediu.

+

O agente usa um navegador que você vê. Quando ele abre um, a sessão ganha uma aba com o terminal ao lado da página; o cursor do lich mostra cada clique, rolagem e tecla que ele envia, e você assume a página quando quiser.

@@ -231,10 +253,7 @@

Tudo em volta do agente

Layouts que se montam sozinhos

Coloque sessões lado a lado e o lich escolhe a grade pela quantidade delas e pelo espaço que a janela tem. Dê um nome ao layout e guarde quantos quiser.

-

Um terminal de verdade

Shells em PTY renderizados na GPU, com histórico pesquisável que sobrevive ao fechamento da janela. Defina um comando inicial e o terminal já abre direto no lazygit ou no pnpm dev.

+

Services que ficam rodando

Salve uma vez por projeto um servidor de dev, um container ou um túnel. O lich sobe junto com o projeto e mantém rodando; a lista no pé da sidebar mostra cada um, abre o console e para quando você quiser.

Seu plano no rodapé

A janela de contexto, quanto ainda resta da janela de uso do seu plano e quanto a sessão já gastou, lidos do que cada agente registra.

Várias contas, um card

Mantenha o login pessoal e o do trabalho lado a lado, como perfis. Uma sessão que muda de conta continua no mesmo card.

Faça fork de uma conversa

Leve a conversa de uma sessão para uma worktree nova, ou mantenha no mesmo checkout, e teste a outra ideia sem perder a primeira.

diff --git a/docs/index.ru.html b/docs/index.ru.html index 03e085eae..e3b3c8e24 100644 --- a/docs/index.ru.html +++ b/docs/index.ru.html @@ -119,28 +119,31 @@

Терминальная ADE для ИИ-агентов, которыми в + -

Одна задача, один worktree, одна сессия. Введите номер issue: lich назовёт ветку по её заголовку, создаст worktree от вашей базовой ветки и передаст issue агенту первым промптом.

@@ -201,6 +222,7 @@

New worktree session

A separate checkout on its own branc

Читайте правки агента там же, где он их сделал. Выделите строки и прокомментируйте их: все комментарии уйдут в сессию одним промптом, отправите его сами.

Pull request'ы репозитория с фильтрами из поиска GitHub. Откройте любой в отдельном worktree, оставьте ревью прямо в diff и смёржите тем способом, который разрешён для базовой ветки.

Одна сессия ставит задачу другой. Задача уходит через lich send или инструменты MCP в lich, а ответ возвращается в ту сессию, которая спрашивала.

+

Агент управляет браузером, который вы видите. Когда он открывает браузер, у сессии появляется вкладка: терминал рядом со страницей. Курсор lich показывает каждый клик, прокрутку и нажатие клавиши, и вы можете перехватить страницу в любой момент.

@@ -231,10 +253,7 @@

Всё, что вокруг агента

Раскладки, которые выстраиваются сами

Поставьте сессии рядом, и lich подберёт сетку по их числу и размеру окна. Дайте раскладке имя и держите их сколько угодно.

-

Настоящий терминал

Оболочки в PTY с отрисовкой на GPU и историей с поиском, которая переживает закрытие окна. Задайте команду запуска, и терминал откроется сразу в lazygit или pnpm dev.

+

Services, которые работают сами

Сохраните dev-сервер, контейнер или туннель один раз для проекта. lich запускает его вместе с проектом и держит запущенным; список внизу боковой панели показывает каждый, открывает консоль и останавливает его.

Подписка в строке состояния

Заполненность контекста, остаток скользящего лимита подписки и расходы сессии. lich берёт их из того, что записывает сам агент.

Несколько аккаунтов, одна карточка

Держите личный и рабочий логин рядом как профили. Сессия, перенесённая в другой аккаунт, продолжает работу в той же карточке.

Форк диалога

Ответвите диалог сессии в новый worktree или оставьте его в том же checkout и проверьте другую идею, не теряя первую.

diff --git a/docs/index.zh-CN.html b/docs/index.zh-CN.html index b17d3efeb..9afe0dbe0 100644 --- a/docs/index.zh-CN.html +++ b/docs/index.zh-CN.html @@ -119,28 +119,31 @@

终端优先的 ADE,接入你正在用的 AI 编程智能体

+ -

一个任务,一个 worktree,一个会话。输入 issue 编号,lich 会用 issue 标题给分支命名,从你选的基础分支创建 worktree,再把这个 issue 作为第一条 prompt 交给智能体。

@@ -201,6 +222,7 @@

New worktree session

A separate checkout on its own branc

在智能体改动的原位置直接阅读。选中几行、写条评论,这批评论会合成一条 prompt 交回给那个会话,由你自己发出。

仓库里的 Pull Request,直接用 GitHub 自带的搜索限定符筛选。把 PR 检出到 worktree,直接在 lich 里审查,再用基础分支允许的方式合并。

一个会话把任务交给另一个会话。通过 lich send 或 lich 的 MCP 工具发出,答复会回到发起请求的那个会话。

+

智能体操作一个你看得见的浏览器。它打开浏览器时,会话会多出一个标签页,终端与页面并排;lich 自己的光标标出它发出的每次点击、滚动和按键,你随时可以接管页面。

@@ -231,10 +253,7 @@

智能体之外的一切

自动排布的分屏墙

把会话并排放好,lich 会根据会话数量和窗口空间自动选择网格。分屏墙可以命名,想留几面就留几面。

-

真正的终端

GPU 渲染的 PTY shell,滚动历史可搜索,关掉窗口也不会丢。给终端设一个启动命令,打开就直接进入 lazygit 或 pnpm dev。

+

一直运行的 Services

每个项目保存一次开发服务器、容器或隧道。lich 随项目启动并保持运行;侧边栏底部的列表显示每一项,可以打开控制台,也可以停止。

套餐用量就在底栏

上下文窗口、套餐滚动周期还剩多少、这个会话花了多少钱,都从各智能体自己写下的记录里读取。

多个账号,同一张卡片

个人账号和工作账号可以作为配置档案并存。会话换到另一个账号后,仍在同一张卡片里继续。

分叉对话

把会话的对话分叉到新的 worktree,或留在原来的检出目录里,换个思路试试,原来那条也不会丢。

diff --git a/docs/style.css b/docs/style.css index 0492738d8..bf0cbb7ef 100644 --- a/docs/style.css +++ b/docs/style.css @@ -264,7 +264,10 @@ h1.busy:not(.done) .caret { animation: none; } .gt .ic { width: 12px; height: 12px; color: inherit; transform: rotate(90deg); } .gt span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .grp > i { flex: 1; height: 1px; margin-left: 2px; background: var(--seam); } -.scard { position: relative; display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; padding: 8px 10px; border-radius: 6px; transition: background-color .2s; } +.scard { position: relative; display: grid; grid-template-columns: minmax(0, 1fr); gap: 2px; padding: 8px 10px; border-radius: 6px; transition: background-color .2s; } +/* The second row is the path, or the status line in its place while a tool runs or the agent waits. */ +.scard .rung:empty { display: none; } +.scard .rung:not(:empty) + .path { display: none; } .scard.on { background: var(--raised); } .scard .t { display: flex; align-items: center; gap: 6px; padding-right: 44px; } .scard .t b { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; } @@ -289,10 +292,21 @@ h1.busy:not(.done) .caret { animation: none; } .beh { display: inline-flex; align-items: center; gap: 2px; margin-left: 4px; } .scard .ds { margin-left: 4px; } /* The profile row sits under a 1px rule with 6 between them, as lich draws it. */ -.prof { display: flex; align-items: center; gap: 8px; height: 39px; padding: 6px 8px 0; border-top: 1px solid var(--seam); } +/* Services sit between the session list and the profile, under their own hairline. */ +.svc { display: flex; align-items: center; gap: 8px; height: 39px; padding: 6px 8px 0; border-top: 1px solid var(--seam); } +.svc b { font-weight: 400; } +.svc .mono { font-size: 12px; } +.svc .ic { width: 14px; height: 14px; } +.sdot { position: relative; width: 14px; height: 14px; flex: none; } +/* A 7px dot (radius 3.5dp in sidebar_services.go); a running one beats a ring out to twice its radius every 2.4s. */ +.sdot::before { content: ""; position: absolute; inset: 3.5px; border-radius: 50%; box-shadow: inset 0 0 0 1.5px var(--muted); } +.sdot.up::before { background: var(--pass); box-shadow: none; } +.sdot.up::after { content: ""; position: absolute; inset: 0; border-radius: 50%; box-shadow: inset 0 0 0 1.5px var(--pass); opacity: 0; animation: sdot-pulse 2.4s ease-out infinite; } +@keyframes sdot-pulse { 0% { opacity: .7; transform: scale(.5); } 33% { opacity: 0; transform: scale(1); } 100% { opacity: 0; } } +.prof { display: flex; align-items: center; gap: 8px; height: 32px; padding: 0 8px; } .prof b { flex: 1; font-weight: 400; } .prof .ic { width: 14px; height: 14px; } -.av { display: inline-grid; place-items: center; width: 16px; height: 16px; border-radius: 50%; background: oklch(0.62 0.17 255); color: #fff; font-size: 12px; font-weight: 600; } +.av { display: inline-grid; place-items: center; flex: none; width: 16px; height: 16px; border-radius: 50%; background: oklch(0.62 0.17 255); color: #fff; font-size: 12px; line-height: 16px; font-weight: 600; } @keyframes spin { to { transform: rotate(360deg); } } /* A running check turns its loader-circle once a second, as lich's Spinner does. */ .app .ic.spin { animation: spin 1s linear infinite; } @@ -310,7 +324,8 @@ h1.busy:not(.done) .caret { animation: none; } flex: 1; min-height: 0; display: flex; flex-direction: column; justify-content: flex-end; margin: 0; padding: 6px 6px 6px 10px; overflow: hidden; font: 14px/1.2 var(--w-mono); color: var(--fg); } -.tile pre .l, .rv-term pre .l, .solo pre .l { flex: none; min-height: 1.2em; white-space: pre; overflow: hidden; text-overflow: ellipsis; } +/* A long line wraps at the pane's edge, as the terminal does. */ +.tile pre .l, .rv-term pre .l, .solo pre .l { flex: none; min-height: 1.2em; white-space: pre-wrap; overflow-wrap: anywhere; } .app .dim { color: var(--faint); } .app .mut { color: var(--muted); } .app .amber { color: var(--wait); } @@ -319,7 +334,19 @@ h1.busy:not(.done) .caret { animation: none; } .app .bad { color: var(--del); } .app .rule { display: flex; gap: .5em; color: var(--faint); } .app .rule::before { content: ""; flex: 1; height: 0; margin-top: .6em; border-top: 1px solid var(--faint); opacity: .6; } -.app .box { padding: 0 .5em; border-left: 2px solid oklch(0.72 0.12 250); background: var(--raised); } +.app .rule:empty { gap: 0; } +.app .box { display: block; padding: 0 1.2em; border-left: 2px solid oklch(0.72 0.14 350); background: var(--raised); white-space: pre-wrap; } +.app .pink { color: oklch(0.72 0.14 350); } +.app .thought { color: color-mix(in oklab, var(--wait) 70%, var(--muted)); } +.app .oc-foot { display: flex; justify-content: space-between; padding: 0 .3em; } +/* Crush: the message under a violet bar, the reply's model line closing on a rule, the YOLO prompt and its keys. */ +.app .cbar { display: block; padding-left: .6em; border-left: 2px solid oklch(0.62 0.2 285); } +.app .cmeta { display: flex; gap: .5em; } +.app .cmeta::after { content: ""; flex: 1; height: 0; margin-top: .6em; border-top: 1px solid var(--faint); opacity: .6; } +.app .yb { padding: 0 .4em; background: oklch(0.9 0.19 115); color: #18181b; font-weight: 600; } +.app .yl { color: oklch(0.9 0.19 115); } +/* Crush cuts its key help at the pane's width instead of wrapping it. */ +.app .clip { display: block; white-space: nowrap; overflow: hidden; } .app .cur { display: inline-block; width: .6em; height: 1.1em; vertical-align: text-bottom; background: var(--fg); } /* Footer: no padding of its own, its items line up with the island; each item is 32 tall with 8 of padding. */ @@ -376,12 +403,83 @@ h1.busy:not(.done) .caret { animation: none; } .dft { display: flex; justify-content: flex-end; gap: 8px; margin-top: 24px; } .dft span { height: 36px; } +/* Drive a browser: the session's tab splits its terminal and the browser pane (a 32 header, a 36 toolbar with + back, forward, reload, the address and Open in a window, then the page). lich's own arrow, in the theme's + primary with a badge naming the provider, glides to what the agent acts on. */ +.brw { flex: 1; min-height: 0; grid-template-columns: minmax(0, 1fr); gap: 6px; padding: 6px; } +.brw.split { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.brw .bpane { display: none; } +.brw.split .bpane { display: flex; } +.bpane { position: relative; flex-direction: column; min-width: 0; overflow: hidden; border-radius: 8px; box-shadow: inset 0 0 0 1px var(--seam); } +.bp-h { display: flex; align-items: center; gap: 8px; height: 32px; flex: none; padding: 0 8px 0 10px; box-shadow: 0 1px 0 var(--seam); font-size: 12px; } +.bp-h > .ic { width: 14px; height: 14px; } +.bp-h b { font-weight: 500; } +.bp-t { display: flex; align-items: center; height: 36px; flex: none; padding: 0 6px; box-shadow: 0 1px 0 var(--seam); } +.bp-t .ia.flash { background: var(--raised); } +.bp-t .ia.flash .ic { color: var(--fg); } +.b-url { flex: 1; min-width: 0; display: flex; align-items: center; height: 24px; margin: 0 6px; padding: 0 8px; border-radius: 6px; box-shadow: inset 0 0 0 1px var(--input); background: color-mix(in oklab, var(--input) 30%, transparent); font-size: 12px; white-space: nowrap; overflow: hidden; } +.bp-page { flex: 1; min-height: 0; overflow: hidden; background: #fff; color: #18181b; } +.pg { padding: 0 28px 28px; font-family: var(--sans); transition: transform .56s cubic-bezier(.2, 0, 0, 1); } +.pg-nav { display: flex; gap: 18px; align-items: center; height: 48px; margin: 0 -28px 24px; padding: 0 28px; border-bottom: 1px solid #e4e4e7; font-size: 13px; color: #52525b; } +.pg-nav b { margin-right: auto; color: #18181b; } +.pg h1 { margin: 0; font-size: 26px; letter-spacing: -.02em; } +.pg p { margin: 8px 0 0; color: #52525b; font-size: 14px; } +.pg-form { max-width: 320px; margin: 0 auto; } +.pg-form label { display: block; margin-top: 16px; font-size: 13px; font-weight: 500; } +.pg-in { display: flex; align-items: center; height: 36px; margin-top: 6px; padding: 0 10px; border: 1px solid #d4d4d8; border-radius: 6px; font-size: 14px; } +.pg-in.focus { border-color: #18181b; box-shadow: 0 0 0 3px rgb(24 24 27 / .08); } +.pg-in.focus .pg-tx::after { content: ""; display: inline-block; width: 1px; height: 1em; margin-left: 1px; vertical-align: text-bottom; background: #18181b; } +.pg-in.bad { border-color: #dc2626; } +.pg-err { display: none; margin-top: 6px; color: #dc2626; font-size: 12px; } +.pg-err.show { display: block; } +.pg-btn { display: block; margin-top: 22px; padding: 10px 14px; border-radius: 6px; background: #18181b; color: #fff; font-size: 14px; font-weight: 500; text-align: center; } +.pg-btn.hit { background: #3f3f46; } +.pg-done { display: none; max-width: 320px; margin: 40px auto 0; text-align: center; } +.pg.done .pg-form { display: none; } +.pg.done .pg-done { display: block; } +.acur { position: absolute; left: 0; top: 0; z-index: 3; pointer-events: none; opacity: 0; transition: transform .28s cubic-bezier(.2, 0, 0, 1), opacity .3s; } +.acur.on { opacity: 1; } +.acur svg { width: 22px; height: 22px; margin: -2px 0 0 -2px; overflow: visible; filter: drop-shadow(0 1px 1px rgb(0 0 0 / .35)); } +.acur path { fill: var(--primary); stroke: var(--background); stroke-width: 1.6; stroke-linejoin: round; } +.acur.dip svg { transform: scale(.88); transform-origin: 0 0; transition: transform .085s; } +.abadge { position: absolute; left: 12px; top: 15px; display: inline-flex; align-items: center; gap: 4px; height: 20px; padding: 0 8px 0 5px; border-radius: 10px; background: var(--primary); box-shadow: 0 0 0 1.5px var(--background); color: var(--primary-fg); font-size: 12px; font-weight: 500; white-space: nowrap; } +.abadge .glyph { width: 12px; height: 12px; color: inherit; } +.aring { position: absolute; z-index: 2; width: 34px; height: 34px; margin: -17px 0 0 -17px; border-radius: 50%; box-shadow: 0 0 0 2px var(--primary); opacity: 0; pointer-events: none; } +.aring.go { animation: aring .52s ease-out; } +@keyframes aring { from { opacity: .95; transform: scale(.2); } to { opacity: 0; transform: scale(1); } } +.btoast { + position: absolute; right: 16px; bottom: 48px; z-index: 5; display: none; align-items: center; gap: 12px; width: 356px; padding: 14px 16px; + border-radius: 8px; background: var(--island); box-shadow: 0 0 0 1px var(--seam), 0 10px 30px -10px rgb(0 0 0 / .6); font-size: 13px; animation: rise .25s ease-out; +} +.btoast.show { display: flex; } +.btoast > div { flex: 1; min-width: 0; } +.btoast b { display: block; font-weight: 500; } +.btoast small { color: var(--muted); font: 12px var(--w-mono); } +.bt-ic .ic { width: 16px; height: 16px; color: var(--fg); } +.bt-btn { display: inline-flex; align-items: center; height: 24px; padding: 0 8px; border-radius: 4px; background: var(--raised); font-size: 12px; font-weight: 500; white-space: nowrap; } +.bt-open { background: var(--fg); color: var(--island); } +.bt-open.press { background: color-mix(in oklab, var(--fg) 80%, var(--background)); } +.scard .t > .web { width: 12px; height: 12px; } +.app .tab .tg { display: inline-flex; align-items: center; gap: 8px; overflow: hidden; } +.app .tab .tg .ic { width: 14px; height: 14px; } + +/* The tour's own clicks: a ring that grows and fades where it pressed, the text color edged in the background so + it reads on a light button as on a dark one. */ +.tap { + position: absolute; z-index: 6; width: 28px; height: 28px; margin: -14px 0 0 -14px; border-radius: 50%; pointer-events: none; + box-shadow: 0 0 0 2px var(--fg), 0 0 0 4px color-mix(in oklab, var(--background) 70%, transparent); animation: tap .36s ease-out forwards; +} +@keyframes tap { from { opacity: 1; transform: scale(.3); } to { opacity: 0; transform: scale(1); } } + /* Buttons, as the kit sizes them: SM is 32 tall with 10 of padding, text 14 medium, radius 6. */ -.btn-g, .btn-s, .btn-p { display: inline-flex; align-items: center; gap: 4px; height: 32px; padding: 0 10px; border-radius: 6px; font-weight: 500; white-space: nowrap; transition: transform .1s, background-color .15s; } +.btn-g, .btn-s, .btn-p { display: inline-flex; align-items: center; gap: 4px; height: 32px; padding: 0 10px; border-radius: 6px; font-weight: 500; white-space: nowrap; transition: background-color .15s; } .btn-g .ic, .btn-s .ic, .btn-p .ic { color: inherit; } .btn-s { background: var(--raised); } .btn-p { background: var(--primary); color: var(--primary-fg); } -.btn-g.press, .btn-s.press, .btn-p.press { transform: scale(.96); } +/* A pressed button changes color and does not move, as lich's do (#877). */ +.btn-g.press { background: var(--raised); } +.btn-s.press { background: color-mix(in oklab, var(--raised) 70%, var(--fg)); } +.btn-p.press { background: color-mix(in oklab, var(--primary) 80%, var(--background)); } .btn-p.off { opacity: .5; } /* Review: the terminal and the dock are two islands. The dock slides in from the right at its own width, on the @@ -438,9 +536,9 @@ h1.busy:not(.done) .caret { animation: none; } .composer.typing .ph { display: none; } .composer.typing .tx::after { content: ""; display: inline-block; width: 1px; height: 1em; margin-left: 1px; vertical-align: text-bottom; background: var(--fg); } .composer .bt { display: flex; gap: 8px; margin-top: 8px; } -.composer .add-b { display: inline-flex; align-items: center; height: 32px; padding: 0 10px; border-radius: 6px; background: var(--primary); color: var(--primary-fg); font-weight: 500; opacity: .5; transition: transform .1s; } +.composer .add-b { display: inline-flex; align-items: center; height: 32px; padding: 0 10px; border-radius: 6px; background: var(--primary); color: var(--primary-fg); font-weight: 500; opacity: .5; transition: background-color .15s; } .composer.typing .add-b { opacity: 1; } -.composer .add-b.press { transform: scale(.96); } +.composer .add-b.press { background: color-mix(in oklab, var(--primary) 80%, var(--background)); } .batch { display: none; flex: none; box-shadow: 0 -1px 0 var(--seam); } .batch.has { display: block; } .cmts { padding: 4px; } @@ -448,6 +546,7 @@ h1.busy:not(.done) .caret { animation: none; } .cmts .at { color: var(--muted); font-family: var(--w-mono); white-space: nowrap; } .cmts .c > span:nth-child(2) { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .cmts .c .ic { width: 12px; height: 12px; margin: 0 6px; } +.cmts .c .ic + .ic { margin-left: 0; } .note { display: flex; align-items: center; height: 28px; margin: 4px 8px 0; padding: 0 10px; border-radius: 6px; box-shadow: inset 0 0 0 1px var(--input); background: color-mix(in oklab, var(--input) 30%, transparent); color: var(--muted); font-size: 12px; } .dk-send { display: flex; align-items: center; gap: 4px; padding: 4px 8px 8px; color: var(--muted); font-size: 12px; } .dk-send .btn-g { color: var(--fg); font-size: 14px; } @@ -460,21 +559,33 @@ h1.busy:not(.done) .caret { animation: none; } .pl-h { display: flex; align-items: center; gap: 6px; padding: 8px; } .flt { flex: 1; min-width: 0; display: flex; align-items: center; gap: 6px; height: 28px; padding: 0 10px; border-radius: 6px; box-shadow: inset 0 0 0 1px var(--input); background: color-mix(in oklab, var(--input) 30%, transparent); color: var(--muted); font-size: 12px; } .flt span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.segm { display: flex; gap: 0; height: 36px; margin: 0 8px; padding: 3px; border-radius: 8px; background: var(--raised); } -.segm span { flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; height: 29px; padding: 0 8px; border-radius: 6px; color: var(--muted); font-weight: 500; white-space: nowrap; } -.segm span.on { background: color-mix(in oklab, var(--input) 30%, transparent); box-shadow: inset 0 0 0 1px var(--input); color: var(--fg); } -.segm i { color: var(--muted); font-size: 12px; font-weight: 400; } .pstate { display: flex; align-items: center; gap: 4px; padding: 8px 12px; color: var(--muted); font-size: 12px; line-height: 16px; } .pstate .ic { width: 12px; height: 12px; } .prs { display: flex; flex-direction: column; gap: 2px; padding: 0 6px 12px; } -.pr { display: flex; gap: 8px; padding: 8px; border-radius: 6px; transition: background-color .2s; } +/* Pulls quick filters: ghost tabs sharing the list's width. */ +.ftabs { display: grid; grid-auto-columns: minmax(0, 1fr); grid-auto-flow: column; gap: 4px; margin: 0 8px; } +.ftabs span { display: flex; align-items: center; justify-content: center; gap: 4px; height: 28px; padding: 0 8px; border-radius: 6px; color: var(--muted); font-size: 12px; font-weight: 500; white-space: nowrap; } +.ftabs span.on { background: var(--raised); color: var(--fg); } +.ftabs i { color: var(--muted); font-weight: 400; } +.ftabs .failing i { color: var(--del); } +.pr { display: block; padding: 8px; border-radius: 6px; transition: background-color .2s; } .pr.on { background: var(--raised); } -.pr > .ic { width: 14px; height: 14px; margin-top: 3px; } .pr div { min-width: 0; } -.pr p { margin: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } +.pr p { margin: 0; line-height: 20px; } +.pr .tl { display: inline; } .pr .num-l { color: var(--muted); } -.pr small { display: flex; align-items: center; gap: 8px; margin-top: 2px; color: var(--muted); font-size: 12px; line-height: 16px; } -.dot { width: 6px; height: 6px; border-radius: 50%; background: var(--pass); } +.pr small { display: flex; align-items: center; gap: 8px; margin-top: 2px; color: var(--muted); font-size: 12px; line-height: 16px; white-space: nowrap; } +.pchk { display: inline-flex; align-items: center; gap: 4px; margin-left: 8px; font: 12px/16px var(--w-mono); vertical-align: 1px; } +.pchk .ic { width: 12px; height: 12px; color: inherit; } +.pchk.ok { color: var(--pass); } +.pchk.bad { color: var(--del); } +.pchk.run { color: var(--wait); } +.pchk .sring { width: 12px; height: 12px; } +.pfold { display: inline-flex; align-items: center; gap: 4px; margin-left: auto; overflow: hidden; } +.pfold .ic { width: 12px; height: 12px; } +/* A running check: a faint track under a quarter arc that turns once a second. */ +.sring { position: relative; display: inline-block; flex: none; width: 16px; height: 16px; border-radius: 50%; box-shadow: inset 0 0 0 1.5px color-mix(in oklab, currentColor 25%, transparent); } +.sring::after { content: ""; position: absolute; inset: 0; border-radius: 50%; border: 1.5px solid transparent; border-top-color: currentColor; animation: spin 1s linear infinite; } .pd { position: relative; display: flex; flex-direction: column; min-width: 0; overflow: hidden; border-radius: 8px; background: var(--background); } .pd-bar { display: flex; justify-content: flex-end; align-items: center; gap: 8px; padding: 8px; } .pd-ttl { padding: 2px 16px 0; } @@ -502,6 +613,7 @@ h1.busy:not(.done) .caret { animation: none; } .ck { display: flex; align-items: center; gap: 12px; padding: 8px 0; } .ck .ic { width: 16px; height: 16px; } .ck .run { color: var(--wait); } +.cks .sring { width: 14px; height: 14px; } .ck .done { display: none; color: var(--pass); } .ck.passed .run { display: none; } .ck.passed .done { display: block; } @@ -554,14 +666,14 @@ h1.busy:not(.done) .caret { animation: none; } .cap { display: none; } .cap p { margin: 1.1rem auto 0; max-width: 46rem; text-align: center; color: var(--muted); } .cap p b { color: var(--fg); font-weight: 500; } -#t1:checked ~ .seg [for="t1"], #t2:checked ~ .seg [for="t2"], #t3:checked ~ .seg [for="t3"], #t4:checked ~ .seg [for="t4"], #t5:checked ~ .seg [for="t5"] { +#t1:checked ~ .seg [for="t1"], #t2:checked ~ .seg [for="t2"], #t3:checked ~ .seg [for="t3"], #t4:checked ~ .seg [for="t4"], #t5:checked ~ .seg [for="t5"], #t6:checked ~ .seg [for="t6"] { background: var(--raised); color: var(--fg); } -#t1:checked ~ .c1, #t2:checked ~ .c2, #t3:checked ~ .c3, #t4:checked ~ .c4, #t5:checked ~ .c5 { display: block; } +#t1:checked ~ .c1, #t2:checked ~ .c2, #t3:checked ~ .c3, #t4:checked ~ .c4, #t5:checked ~ .c5, #t6:checked ~ .c6 { display: block; } .view { display: none; } -#t1:checked ~ #wall .v1, #t2:checked ~ #wall .v2, #t3:checked ~ #wall .v3, #t4:checked ~ #wall .v4, #t5:checked ~ #wall .v5 { display: grid; } +#t1:checked ~ #wall .v1, #t2:checked ~ #wall .v2, #t3:checked ~ #wall .v3, #t4:checked ~ #wall .v4, #t5:checked ~ #wall .v5, #t6:checked ~ #wall .v6 { display: grid; } #t1:checked ~ #wall .v1, #t3:checked ~ #wall .v3 { display: flex; } -#t1:focus-visible ~ .seg [for="t1"], #t2:focus-visible ~ .seg [for="t2"], #t3:focus-visible ~ .seg [for="t3"], #t4:focus-visible ~ .seg [for="t4"], #t5:focus-visible ~ .seg [for="t5"] { +#t1:focus-visible ~ .seg [for="t1"], #t2:focus-visible ~ .seg [for="t2"], #t3:focus-visible ~ .seg [for="t3"], #t4:focus-visible ~ .seg [for="t4"], #t5:focus-visible ~ .seg [for="t5"], #t6:focus-visible ~ .seg [for="t6"] { outline: 2px solid var(--fg); outline-offset: 2px; } @@ -607,6 +719,12 @@ section.block { padding-block: 5.5rem; } .app.mini .ic.wait, .app.mini b.wait { color: var(--wait); } .app.mini .scard .t > .ic { width: 12px; height: 12px; } .app.mini .sub { margin-left: 22px; } +/* The Services list opened upward, as it sits at the foot of the sidebar. */ +.app.mini .svcs { padding: 8px; border-radius: 8px; background: var(--island); } +.svr { display: flex; align-items: center; gap: 8px; height: 26px; padding: 0 6px 0 8px; font-size: 14px; } +.svr .mono { font-size: 12px; } +.svr .ic { width: 14px; height: 14px; } +.app.mini .svcs .svc { margin-top: 6px; } .app.mini.subs .path { display: none; } .mwall { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-rows: repeat(2, 1fr); gap: 6px; height: 100%; padding: 6px; border-radius: 8px; background: var(--background); } .mt { border-radius: 8px; box-shadow: inset 0 0 0 1px var(--seam); overflow: hidden; } diff --git a/docs/tour.js b/docs/tour.js index 5a8b5391b..1c98d68b2 100644 --- a/docs/tour.js +++ b/docs/tour.js @@ -9,7 +9,7 @@ var shown = false; var span = function (cls) { return function (t) { return '' + t + ""; }; }; - var ok = span("ok"), bad = span("bad"), dim = span("dim"), blue = span("blue"), amber = span("amber"), mut = span("mut"); + var ok = span("ok"), bad = span("bad"), dim = span("dim"), blue = span("blue"), amber = span("amber"), mut = span("mut"), pink = span("pink"); var jitter = function (lo, hi) { return lo + Math.random() * (hi - lo); }; function active() { return radios.findIndex(function (r) { return r.checked; }); } @@ -35,9 +35,9 @@ // A story awaits wait(ms) between its beats. The wait holds while its tab is closed or the window is off screen, // and throws once the tab is opened again so the story starts over. - var generation = [0, 0, 0, 0, 0]; + var generation = [0, 0, 0, 0, 0, 0]; // wait.end() marks the story told once; the tab moves on only after that, whatever its bar says. - var told = [false, false, false, false, false], barDone = false; + var told = [false, false, false, false, false, false], barDone = false; function waiter(v) { var mine = generation[v]; var wait = function (ms) { @@ -54,10 +54,19 @@ })(); }); }; + // end marks the story told and then holds it on its last frame until its tab opens again or another does, + // so a tab whose bar outlasts its story never shows the story start over and get cut off. wait.end = function () { - if (generation[v] !== mine) return; - told[v] = true; - advance(); + if (generation[v] === mine) { + told[v] = true; + advance(); + } + return new Promise(function (done, restart) { + (function hold() { + if (generation[v] !== mine) return restart(); + setTimeout(hold, 200); + })(); + }); }; return wait; } @@ -67,8 +76,9 @@ claude: { asked: function (p) { return mut("❯ " + p); }, bottom: function (input, t) { - return ['' + (t.rule || "docs-mcp-matrix-68c5") + ' ─', mut("❯") + " " + input, - amber("⏵⏵ auto mode on") + " " + dim("(shift+tab to cycle)")]; + // Claude Code's input box: a rule naming the worktree, the prompt, a closing rule, then its mode, indented. + return ['' + (t.rule || "docs-mcp-matrix-68c5") + ' ─', mut("❯") + " " + input, '', + " " + amber("⏵⏵ auto mode on") + " " + dim("(shift+tab to cycle)")]; } }, codex: { @@ -78,16 +88,23 @@ amber("gpt-5-codex medium") + " · " + blue("Workspace") + " · Ask for approval"]; } }, + // opencode: the message in a box with a pink left bar, a blank row above and below it; the input box holds + // the prompt row and the agent's status row, with the path and the token count under it. opencode: { - asked: function (p) { return '' + p + ""; }, + asked: function (p) { return '\n' + p + "\n"; }, bottom: function (input) { - return ['' + (input || dim("Ask anything…")) + "", - '' + blue("Build") + " · claude-sonnet-4.5 " + dim("Anthropic") + "", dim("ctrl+p commands")]; + return ['\n' + input + "\n\n" + pink("Build") + " " + mut("auto") + " · claude-sonnet-4.5 " + dim("Anthropic") + + " · " + 'high', + '' + mut("~/…/fix-relay-deadline") + "" + mut("12.8K (6%)") + " ctrl+p " + mut("commands") + ""]; } }, crush: { - asked: function (p) { return mut("> " + p); }, - bottom: function (input) { return ["> " + (input || dim("Ready for instructions")), "", dim("ctrl+p commands · ctrl+j newline")]; } + asked: function (p) { return '' + p + ""; }, + bottom: function (input, t) { + var keys = [["tab", "focus chat"], ["shift+tab", "mode"], ["/ or ctrl+p", "commands"], ["ctrl+m", "models"]]; + return ['! ' + input + (t.input ? "" : dim("Go crazy")), ':::', ':::', "", + 'YOLO MODE ' + keys.map(function (k) { return k[0] + " " + dim(k[1]); }).join(dim(" • ")) + ""]; + } } }; function terminal(pre, provider, lines) { @@ -129,19 +146,22 @@ "• Ran go test ./internal/auth -run Expires", " └ ok internal/auth 0.38s", dim("Worked for 22s • 8:33 PM")] }] }, { provider: "opencode", model: "claude-sonnet-4.5", runs: [ { prompt: "the relay deadline outlives its context", tool: "Edit · ticket.go", add: 9, del: 3, out: [ - blue("▣") + " Build " + dim("· claude-sonnet-4.5 · 17.6s"), " → Read internal/relay/ticket.go", + "", " " + span("thought")("+ Thought: 1.8s"), "", " → Read internal/relay/ticket.go", " ← Edit internal/relay/ticket.go", " " + ok("+ ctx, cancel := context.WithTimeout(ctx, d)"), - " " + ok("+ defer cancel()"), " The deadline comes from the caller now;", " all 31 tests pass."] }, + " " + ok("+ defer cancel()"), "", " The deadline comes from the caller now;", " all 31 tests pass.", "", + " " + pink("▣") + " Build " + dim("· claude-sonnet-4.5 · 17.6s")] }, { prompt: "why did the old one never fire?", tool: "Read · relay.go", add: 9, del: 3, out: [ - blue("▣") + " Build " + dim("· claude-sonnet-4.5 · 6.2s"), " → Read internal/relay/relay.go", - " The timer was armed with context.Background(),", " so cancelling the request never reached it."] }] }, + "", " " + span("thought")("+ Thought: 0.9s"), "", " → Read internal/relay/relay.go", "", + " The timer was armed with context.Background(),", " so cancelling the request never reached it.", "", + " " + pink("▣") + " Build " + dim("· claude-sonnet-4.5 · 6.2s")] }] }, { provider: "crush", model: "glm-4.6", runs: [ { prompt: "document what a broken theme does", tool: "Write · docs/themes.md", add: 24, del: 0, out: [ - blue("◇") + " View docs/themes.md", blue("◇") + " Edit docs/themes.md", - " " + ok("+ A theme that fails the schema stays listed"), " " + ok("+ in Settings › Appearance, marked unloadable."), - "Documented the rule and the error it shows."] }, + "", "Thought for " + dim("2s"), "", ok("✓") + " View " + dim("docs/themes.md"), ok("✓") + " Edit " + dim("docs/themes.md"), + " " + ok("+ A theme that fails the schema stays listed in Settings › Appearance, marked unloadable."), "", + "Documented the rule and the error it shows.", "", '' + dim("◇") + "GLM 4.6 " + dim("via Z.ai in 12s") + ""] }, { prompt: "link the schema from the README", tool: "Edit · README.md", add: 26, del: 0, out: [ - blue("◇") + " Edit README.md", " " + ok("+ [theme schema](themes/lich-theme.schema.json)"), "Linked it under Themes."] }] } + "", "Thought for " + dim("1s"), "", ok("✓") + " Edit " + dim("README.md"), " " + ok("+ [theme schema](themes/lich-theme.schema.json)"), "", + "Linked it under Themes.", "", '' + dim("◇") + "GLM 4.6 " + dim("via Z.ai in 6s") + ""] }] } ]; var wallTiles = wall.querySelectorAll(".v2 .tile"); var wallTerms = wallSessions.map(function (s, k) { @@ -169,9 +189,10 @@ setFooter(cards[k].querySelector(".t b").textContent, s.provider, s.model, run.add, run.del); await t.stream(wait, run.out); setRing(k, "done"); + cards[k].querySelector(".rung").innerHTML = ""; await wait(1600); setRing(k, "read"); - if (k === 1) wait.end(); + if (k === 1) await wait.end(); } } @@ -197,7 +218,16 @@ cmts.innerHTML = ""; batch.classList.remove("has"); send.classList.remove("press"); menu.classList.remove("show"); } - async function press(wait, el) { el.classList.add("press"); await wait(220); el.classList.remove("press"); } + // tap draws a ring where the tour clicks, so a viewer sees what was pressed; every simulated click goes through it. + function tap(el) { + var w = wall.getBoundingClientRect(), r = el.getBoundingClientRect(), ring = document.createElement("span"); + ring.className = "tap"; + ring.style.left = r.left - w.left + r.width / 2 + "px"; + ring.style.top = r.top - w.top + r.height / 2 + "px"; + wall.appendChild(ring); + setTimeout(function () { ring.remove(); }, 400); + } + async function press(wait, el) { tap(el); el.classList.add("press"); await wait(220); el.classList.remove("press"); } async function reviewDiff(wait) { for (;;) { resetReview(); @@ -206,7 +236,7 @@ reviewTerm.draw(true); setCard(0); setFooter("docs/mcp-matrix", "claude", "opus 5.5 · medium", 7, 3); await wait(1200); - chip.classList.add("on"); rv.classList.add("open"); + tap(chip); chip.classList.add("on"); rv.classList.add("open"); await wait(1100); await press(wait, full); setFull(true); for (var n = 0; n < comments.length; n++) { @@ -222,6 +252,7 @@ await wait(450); menu.querySelector(".m-cmt").classList.add("hover"); await wait(300); + tap(menu.querySelector(".m-cmt")); menu.querySelector(".m-cmt").classList.remove("hover"); menu.classList.remove("show"); var box = document.createElement("div"); @@ -239,25 +270,25 @@ box.remove(); c.rows.forEach(function (i) { diffRows[i].classList.remove("sel"); }); cmts.insertAdjacentHTML("beforeend", '
README.md:' + c.lines + "" + c.text + - '
'); + '
'); batch.classList.add("has"); count.textContent = (n + 1) + (n ? " comments" : " comment"); } await wait(1200); + // Send submits the batch as the session's prompt; the strip stays until the session has taken it. await press(wait, send); + reviewTerm.submit("[Pasted text #1 +4 lines]"); + await wait(700); resetReview(); - reviewTerm.input = '[Pasted text #1 +3 lines]'; - reviewTerm.draw(true); - await wait(800); + await wait(700); await press(wait, full); setFull(false); await wait(900); await press(wait, close); rv.classList.remove("open"); chip.classList.remove("on"); - await wait(900); - reviewTerm.submit("[Pasted text #1 +3 lines]"); + await wait(500); await reviewTerm.stream(wait, ["● Update(README.md)", " ⎿ " + ok("+ Run Claude Code, Codex and six more agent CLIs"), " ⎿ " + ok("+ | [MCP server](docs/cli.md#mcp) | yes | yes | yes |"), "● Both review comments are in.", dim("✻ Crunched for 26s · done 20:41")]); await wait(2200); - wait.end(); + await wait.end(); } } @@ -271,9 +302,15 @@ tabOv.classList.toggle("on", !checksTab); tabCk.classList.toggle("on", checksTab); bodyOv.classList.toggle("show", !checksTab); bodyCk.classList.toggle("show", checksTab); } + // The list row's checks chip follows the open pull request's checks. + function rowChip(k, done) { + var c = prRows[k].querySelector(".pchk"), total = checks.length; + c.className = done < total ? "pchk run" : "pchk ok"; + c.innerHTML = (done < total ? '' : icon("check")) + done + "/" + total; + } function running(done) { cks.className = "cks running"; - cks.innerHTML = '' + (checks.length - done) + " of " + checks.length + " checks running"; + cks.innerHTML = '' + (checks.length - done) + " of " + checks.length + " checks running"; } function openPr(k) { var row = prRows[k]; @@ -285,19 +322,21 @@ checks.forEach(function (c) { c.classList.remove("passed"); c.querySelector(".dur").textContent = ""; }); state.className = "state"; state.innerHTML = icon("git-pull-request-arrow") + "Open"; merge.classList.add("off"); mmenu.classList.remove("show"); - running(0); showTab(false); + running(0); rowChip(k, 0); showTab(false); } async function pullRequests(wait) { for (var k = 0; ; k = (k + 1) % prRows.length) { + if (prRows[k].querySelector(".pchk.bad")) continue; setCard(-1); openPr(k); await wait(1600); - showTab(true); + tap(tabCk); showTab(true); for (var i = 0; i < checks.length; i++) { await wait(jitter(350, 700)); checks[i].classList.add("passed"); checks[i].querySelector(".dur").textContent = Math.round(jitter(40, 240)) + "s"; running(i + 1); + rowChip(k, i + 1); } cks.className = "cks passed"; cks.innerHTML = icon("check") + checks.length + " checks passed"; @@ -308,12 +347,12 @@ await wait(600); mmenu.querySelector(".m-sq").classList.add("hover"); await wait(450); + tap(mmenu.querySelector(".m-sq")); mmenu.querySelector(".m-sq").classList.remove("hover"); mmenu.classList.remove("show"); state.className = "state merged"; state.innerHTML = icon("git-merge") + "Merged"; - prRows[k].querySelector(".ic use").setAttribute("href", "#i-git-merge"); await wait(2200); - wait.end(); + await wait.end(); } } @@ -356,7 +395,7 @@ await asker.stream(wait, ["● Update(docs/auth.md)", " ⎿ " + ok("+ A token is expired at its exp instant."), dim("✻ Crunched for 1m 2s · done 20:35")]); await wait(2600); - wait.end(); + await wait.end(); } } @@ -406,10 +445,11 @@ resetTask(); setCard(0); setFooter("docs/mcp-matrix", "claude", "opus 5.5 · medium", 12, 1); await wait(1000); - ns.classList.add("open"); nmenu.classList.add("show"); + tap(ns); ns.classList.add("open"); nmenu.classList.add("show"); await wait(700); nmenu.querySelector(".m-wt").classList.add("hover"); await wait(350); + tap(nmenu.querySelector(".m-wt")); ns.classList.remove("open"); nmenu.classList.remove("show"); dlg.classList.add("show"); nameIn.classList.add("focus"); await wait(700); @@ -452,16 +492,138 @@ card.querySelector(".ring-st").className = "ring-st done"; setFooter(issue.branch, "claude", "opus 5.5 · medium", 9, 1); await wait(2600); - wait.end(); + await wait.end(); + } + } + + // 5. Drive a browser: the agent's browser_open raises a toast whose Open gives the session a tab, its terminal + // beside the browser pane; lich's arrow then glides to each action the agent sends, the toolbar's included. + // The story's session is a signup-form fix, so the first card wears that name while it plays. + var brw = wall.querySelector(".v6"), bpane = brw.querySelector(".bpane"), page = brw.querySelector(".pg"); + var acur = brw.querySelector(".acur"), aring = brw.querySelector(".aring"), url = brw.querySelector(".b-url"); + var btoast = wall.querySelector(".btoast"), tabs = wall.querySelector(".tabs"); + var email = page.querySelector(".pg-email"), pass = page.querySelector(".pg-pass"), err = page.querySelector(".pg-err"); + var signup = page.querySelector(".pg-btn"); + var browserTerm = terminal(brw.querySelector(".tile pre"), "claude", [dim("✻ Crunched for 12s · done 20:58")]); + var BRANCH = "fix/signup-email"; + var first = { title: cards[0].querySelector(".t b").textContent, path: cards[0].querySelector(".path span").textContent, + ds: cards[0].querySelector(".ds").innerHTML }; + // nameCard dresses the first card as the given session: its title, path, branch and diffstat. + function nameCard(title, path, ds) { + cards[0].querySelector(".t b").textContent = title; + cards[0].querySelector(".brn").textContent = title; + cards[0].querySelector(".path span").textContent = path; + cards[0].querySelector(".ds").innerHTML = ds; + } + var signupPath = "\u200e~/.local/share/lich/worktrees/e48f04f46a4a/fix-signup-email\u200e"; + function resetBrowser() { + brw.classList.remove("split"); btoast.classList.remove("show"); acur.classList.remove("on"); + page.className = "pg"; url.textContent = "localhost:5173/signup"; + [email, pass].forEach(function (i) { i.classList.remove("focus", "bad"); i.querySelector(".pg-tx").textContent = ""; }); + err.classList.remove("show"); signup.classList.remove("hit"); + var tab = tabs.querySelector(".btab"), web = cards[0].querySelector(".web"); + if (tab) tab.remove(); + if (web) web.remove(); + tabs.querySelector(".tab").classList.add("on"); + nameCard(first.title, first.path, first.ds); + browserTerm.lines = [dim("✻ Crunched for 12s · done 20:58")]; + browserTerm.input = ""; browserTerm.rule = "fix-signup-email-2b7e"; + browserTerm.draw(true); + } + // glide moves the arrow to a point inside el (its center by default), in the pane's coordinates. + function glide(el, fx, fy) { + var p = bpane.getBoundingClientRect(), r = el.getBoundingClientRect(); + var x = r.left - p.left + r.width * (fx === undefined ? .5 : fx), y = r.top - p.top + r.height * (fy === undefined ? .5 : fy); + acur.style.transform = "translate(" + x + "px," + y + "px)"; + return [x, y]; + } + async function land(wait, at) { + acur.classList.add("dip"); + aring.style.left = at[0] + "px"; aring.style.top = at[1] + "px"; + aring.classList.remove("go"); void aring.offsetWidth; aring.classList.add("go"); + await wait(90); + acur.classList.remove("dip"); + } + async function flash(wait, btn) { btn.classList.add("flash"); await wait(220); btn.classList.remove("flash"); } + // fill clicks into a field and types text into it, as browser_type does. + async function fill(wait, field, text) { + var at = glide(field, .15); + await wait(320); + await land(wait, at); + [email, pass].forEach(function (i) { i.classList.toggle("focus", i === field); }); + for (var ch of text) { field.querySelector(".pg-tx").textContent += ch; await wait(jitter(35, 70)); } + } + async function clickSignup(wait) { + [email, pass].forEach(function (i) { i.classList.remove("focus"); }); + var at = glide(signup); + await wait(380); + await land(wait, at); + signup.classList.add("hit"); + await wait(250); + signup.classList.remove("hit"); + } + async function driveBrowser(wait) { + for (;;) { + resetBrowser(); + setCard(0); nameCard(BRANCH, signupPath, ""); setFooter(BRANCH, "claude", "opus 5.5 · medium", 0, 0); + await wait(900); + var ask = 'the signup form says "invalid email" for a valid one'; + await browserTerm.type(wait, ask, 25, 55); + await wait(350); + browserTerm.submit(ask); + setRing(0, "busy"); + await browserTerm.stream(wait, ['● lich - browser_open (MCP)(url: "http://localhost:5173/signup")', " ⎿ Opened · Create your account"]); + btoast.classList.add("show"); + await wait(1500); + await press(wait, btoast.querySelector(".bt-open")); + btoast.classList.remove("show"); + tabs.querySelectorAll(".tab").forEach(function (t) { t.classList.remove("on"); }); + tabs.querySelector(".tab").insertAdjacentHTML("afterend", '' + BRANCH + "" + + ''); + cards[0].querySelector(".ring-st").insertAdjacentHTML("afterend", ''); + brw.classList.add("split"); + await wait(600); + glide(page, .5, .2); acur.classList.add("on"); + await wait(500); + browserTerm.lines.push('● lich - browser_type (MCP)(field: "Email", text: "ana@shop.dev")'); browserTerm.draw(true); + await fill(wait, email, "ana@shop.dev"); + browserTerm.lines.push('● lich - browser_type (MCP)(field: "Password", text: "••••••••")'); browserTerm.draw(true); + await fill(wait, pass, "••••••••"); + browserTerm.lines.push('● lich - browser_click (MCP)(text: "Sign up")'); browserTerm.draw(true); + await clickSignup(wait); + email.classList.add("bad"); err.classList.add("show"); + await browserTerm.stream(wait, [' ⎿ "Invalid email" under Email', "● Read(src/validate.ts)", "● Update(src/validate.ts)", + " ⎿ " + bad("- /^[a-z]+@[a-z]+\\.com$/"), " ⎿ " + ok("+ /^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/")]); + setFooter(BRANCH, "claude", "opus 5.5 · medium", 1, 1); + nameCard(BRANCH, signupPath, '+1-1'); + browserTerm.lines.push("● lich - browser_reload (MCP)"); browserTerm.draw(true); + var reload = brw.querySelector(".b-rel"); + await land(wait, glide(reload)); + await flash(wait, reload); + [email, pass].forEach(function (i) { i.classList.remove("focus", "bad"); i.querySelector(".pg-tx").textContent = ""; }); + err.classList.remove("show"); + await wait(500); + browserTerm.lines.push("● lich - browser_type (MCP)(Email, Password)"); browserTerm.draw(true); + await fill(wait, email, "ana@shop.dev"); + await fill(wait, pass, "••••••••"); + browserTerm.lines.push('● lich - browser_click (MCP)(text: "Sign up")'); browserTerm.draw(true); + await clickSignup(wait); + page.classList.add("done"); + await browserTerm.stream(wait, [" ⎿ Welcome, ana", "● The email check only took .com addresses; it now takes", " any domain. Signed up as ana@shop.dev to confirm.", + dim("✻ Crunched for 41s · done 21:03")]); + setRing(0, "done"); + await wait(2600); + await wait.end(); } } - var stories = [startTask, mixAgents, reviewDiff, pullRequests, handOff]; + var stories = [startTask, mixAgents, reviewDiff, pullRequests, handOff, driveBrowser]; function start(v) { generation[v]++; told[v] = false; barDone = false; chip.classList.remove("on"); resetTask(); + resetBrowser(); wall.setAttribute("aria-label", labels[v]); stories[v](waiter(v)).catch(function () {}); } @@ -474,7 +636,7 @@ // The open tab fills a bar under its label; the next tab opens once the bar is full and the story told. // Measured story lengths, rounded up; a story that runs longer still holds its tab until it is told. - var durations = [16000, 15000, 21000, 10000, 16000]; + var durations = [17000, 16000, 22000, 10000, 16000, 21000]; function runBar() { radios.forEach(function (r, i) { var label = seg.querySelector('[for="' + r.id + '"]');