Files
2026-07-13 13:00:08 +08:00

1801 lines
106 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Reasonix</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
--bg:oklch(17% .005 280);--bg-2:oklch(20% .006 275);
--panel:oklch(21.5% .007 275);--panel-2:oklch(24.5% .008 275);
--card:oklch(23.5% .007 275);--card-hover:oklch(26.5% .008 275);
--border:oklch(30% .008 275);--border-strong:oklch(38% .01 275);
--fg:oklch(96% .004 280);--fg-2:oklch(81% .005 280);
--muted:oklch(63% .006 280);--muted-2:oklch(49% .006 280);
--accent:oklch(68% .16 38);--accent-soft:oklch(68% .16 38/.15);--accent-strong:oklch(72% .18 36);
--success:oklch(70% .14 145);--success-soft:oklch(70% .14 145/.14);
--warning:oklch(78% .14 85);--warning-soft:oklch(78% .14 85/.16);
--danger:oklch(66% .18 25);--danger-soft:oklch(66% .18 25/.16);
--violet:oklch(72% .14 295);--violet-soft:oklch(72% .14 295/.16);
--shadow-sm:0 1px 0 oklch(0% 0 0/.35);
--shadow-md:0 8px 24px -8px oklch(0% 0 0/.5);
--shadow-lg:0 24px 60px -16px oklch(0% 0 0/.65);
--radius:8px;--radius-lg:12px;
--sans:'DM Sans',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
--heading:'Space Grotesk',var(--sans);
--mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
color-scheme:dark;
}
html,body{height:100%;overflow:hidden}
body{font:14px/1.6 var(--sans);color:var(--fg);background:var(--bg);-webkit-font-smoothing:antialiased}
::-webkit-scrollbar{width:6px;height:6px}
::-webkit-scrollbar-thumb{background:var(--border);border-radius:99px}
::-webkit-scrollbar-track{background:transparent}
:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
button{cursor:pointer;font:inherit;border:none;background:none;color:inherit}
input,textarea{font:inherit;color:inherit}
.app{display:grid;grid-template-columns:220px 1fr;grid-template-rows:1fr auto;height:100vh}
.sidebar{grid-row:1/3;background:var(--bg-2);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden}
.transcript{overflow-y:auto;padding:24px 28px;scroll-behavior:smooth}
.footer{border-top:1px solid var(--border);background:var(--bg);padding:12px 28px 16px;display:flex;flex-direction:column;gap:8px;position:relative}
.footer::before{content:'';position:absolute;inset:-20px 0 auto 0;height:20px;background:linear-gradient(to top,var(--bg),transparent);pointer-events:none}
.sidebar__brand{display:flex;align-items:center;gap:9px;padding:16px 14px 12px;border-bottom:1px solid var(--border)}
.sidebar__logo{width:26px;height:26px;border-radius:7px;background:linear-gradient(135deg,var(--accent),var(--violet));display:flex;align-items:center;justify-content:center;font-family:var(--heading);font-weight:700;font-size:13px;color:oklch(99% 0 0)}
.sidebar__name{font-family:var(--heading);font-size:14px;font-weight:600}
.brand-wordmark{display:block;object-fit:contain;filter:brightness(0) invert(1);opacity:.94}
.brand-wordmark--sidebar{width:136px;height:32px;object-position:left center}
.brand-wordmark--welcome{width:240px;height:56px}
.sidebar__nav{flex:1;padding:8px;display:flex;flex-direction:column;gap:1px;overflow-y:auto}
.sidebar__item{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:6px;color:var(--fg-2);font-size:13px;transition:background .18s ease,color .18s ease;cursor:pointer}
.sidebar__item:hover{background:var(--panel);color:var(--fg)}
.sidebar__item--active{background:var(--accent-soft);color:var(--accent)}
.sidebar__item--accent{background:var(--accent);color:oklch(99% 0 0);font-weight:500;box-shadow:var(--shadow-sm)}
.sidebar__item--accent:hover{background:var(--accent-strong);color:oklch(99% 0 0)}
.sidebar__item--disabled{opacity:.45;cursor:not-allowed}
.sidebar__item--disabled:hover{background:transparent;color:var(--fg-2)}
.sidebar__item svg{width:15px;height:15px;flex-shrink:0;opacity:.7}
.sidebar__item--active svg,.sidebar__item--accent svg{opacity:1}
.sidebar__section{padding:8px;border-top:1px solid var(--border)}
.sidebar__sep{height:1px;background:var(--border);margin:4px 8px}
.sidebar__label{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:600;padding:4px 10px}
.sidebar__label-row{display:flex;align-items:center;justify-content:space-between;padding:8px 10px 4px}
.sidebar__ctx{padding:6px 10px;display:flex;flex-direction:column;gap:5px}
.ctx-bar{height:3px;border-radius:99px;background:var(--panel-2);overflow:hidden}
.ctx-bar__fill{height:100%;border-radius:99px;background:var(--accent);transition:width .3s ease}
.ctx-label{display:flex;justify-content:space-between;font-family:var(--mono);font-size:11px;color:var(--muted-2)}
/* status metrics */
.status-metrics{display:flex;gap:0;padding:5px 8px;border-top:1px dashed var(--border);font-family:var(--mono);font-size:10.5px;color:var(--muted-2)}
.status-metrics .sm-item{flex:1;display:flex;flex-direction:column;align-items:center;gap:2px}
.status-metrics .sm-val{color:var(--fg-2);font-weight:500;font-size:11.5px}
.status-metrics .sm-val.acc{color:var(--accent)}
.msg{max-width:760px;margin:0 auto 14px;animation:msg-in .25s ease}
@keyframes msg-in{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}
.msg--user{display:flex;gap:9px;align-items:baseline}
.msg__caret{color:var(--accent);font-family:var(--mono);font-weight:600;flex-shrink:0;font-size:15px}
.msg__text{white-space:pre-wrap;word-break:break-word;font-weight:500;line-height:1.65}
.msg--assistant{color:var(--fg);line-height:1.7}
.msg--assistant .msg__text{font-weight:400}
.msg--system{font-size:13px;color:var(--fg-2);border-left:2px solid var(--border);padding:4px 12px;margin:8px auto}
.msg--error{font-size:13px;color:var(--danger);border-left:2px solid var(--danger);padding:6px 12px;margin:8px auto;background:var(--danger-soft);border-radius:0 var(--radius) var(--radius) 0}
.reasoning{margin-bottom:6px}
.reasoning__toggle{display:inline-flex;align-items:center;gap:4px;background:none;border:none;color:var(--muted);font-size:12px;cursor:pointer;padding:2px 0;transition:color .18s ease}
.reasoning__toggle:hover{color:var(--fg-2)}
.reasoning__chevron{transition:transform .15s;display:inline-block}
.reasoning__chevron--open{transform:rotate(90deg)}
.reasoning__body{margin-top:5px;padding:8px 12px;border-left:2px solid var(--border);color:var(--fg-2);font-size:13px;white-space:pre-wrap;line-height:1.6;background:var(--bg-2);border-radius:0 var(--radius) var(--radius) 0;max-height:280px;overflow-y:auto}
.cursor{display:inline-block;width:2px;height:1.1em;background:var(--accent);vertical-align:text-bottom;margin-left:2px;animation:blink 1s steps(2,start) infinite}
@keyframes blink{to{visibility:hidden}}
/* card (v1 pattern) */
.card{background:var(--card);border-radius:var(--radius-lg);overflow:hidden;font-size:14px;box-shadow:var(--shadow-sm);margin:8px auto;transition:border-color .18s ease}
.card-head{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;align-items:center;gap:8px;padding:7px 12px;font-size:13px;color:var(--fg-2);cursor:pointer;user-select:none;background:var(--card);transition:background .18s ease}
.card-head:hover{background:var(--card-hover)}
.card-head .ico{width:18px;height:18px;border-radius:5px;display:inline-flex;align-items:center;justify-content:center;background:var(--panel-2);color:var(--fg-2);flex-shrink:0;font-size:10px}
.card-main{display:grid;gap:2px;min-width:0}
.card-title{display:flex;align-items:center;gap:7px;min-width:0}
.card-head .name{color:var(--fg);font-weight:500;font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.card-head .subject{font-family:var(--mono);font-size:11.5px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.card-meta{display:flex;align-items:center;gap:8px;min-width:0;font-family:var(--mono);font-size:10.5px;color:var(--muted-2)}
.card-badge{display:inline-flex;align-items:center;height:20px;padding:0 7px;border-radius:999px;border:1px solid var(--border);font-size:10.5px;color:var(--muted);white-space:nowrap}
.card-actions{display:flex;align-items:center;gap:4px}
.card-action{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border:1px solid var(--border);border-radius:6px;background:var(--bg-2);color:var(--muted);transition:background .15s ease,color .15s ease,border-color .15s ease}
.card-action:hover{background:var(--panel);color:var(--fg);border-color:var(--border-strong)}
.card-head .chev{color:var(--muted-2);transition:transform .15s;display:inline-flex}
.card[data-open="false"] .card-head .chev{transform:rotate(-90deg)}
.card-body{border-top:1px solid var(--border);background:var(--bg-2);padding:8px 12px;font-family:var(--mono);font-size:12px;color:var(--fg-2);white-space:pre-wrap;word-break:break-word;max-height:240px;overflow-y:auto;line-height:1.55}
.card-body:empty{display:none}
.card[data-tone="success"] .card-head .ico{background:var(--success-soft);color:var(--success)}
.card[data-tone="danger"] .card-head .ico{background:var(--danger-soft);color:var(--danger)}
.card[data-tone="accent"] .card-head .ico{background:var(--accent-soft);color:var(--accent)}
.card[data-tone="success"] .card-badge{background:var(--success-soft);border-color:transparent;color:var(--success)}
.card[data-tone="danger"] .card-badge{background:var(--danger-soft);border-color:transparent;color:var(--danger)}
.card[data-tone="accent"] .card-badge{background:var(--accent-soft);border-color:transparent;color:var(--accent)}
.card .err-body{padding:6px 12px;color:var(--danger);background:var(--danger-soft);font-family:var(--mono);font-size:12px;white-space:pre-wrap;border-top:1px solid var(--border)}
.spin{animation:spin .9s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
/* metric strip (v1) */
.metric-strip{display:flex;gap:0;padding:5px 0;border-top:1px dashed var(--border);border-bottom:1px dashed var(--border);margin:8px auto;max-width:760px;font-family:var(--mono);font-size:11px;color:var(--muted)}
.metric-strip .item{padding:0 10px;display:flex;align-items:center;gap:5px;border-right:1px solid var(--border)}
.metric-strip .item:last-child{border-right:none}
.metric-strip .v{color:var(--fg)}
.metric-strip .v.acc{color:var(--accent)}
.metric-strip .v.ok{color:var(--success)}
.notice{max-width:760px;margin:5px auto;font-size:13px;color:var(--fg-2);padding:5px 12px;border-left:2px solid var(--border);border-radius:0 var(--radius) var(--radius) 0;white-space:pre-wrap}
.notice--warn{color:var(--warning);border-left-color:var(--warning);background:var(--warning-soft)}
.phase{max-width:760px;margin:10px auto;text-align:center;font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted-2);font-family:var(--mono)}
.compaction{max-width:760px;margin:8px auto;border:1px dashed var(--border);border-radius:var(--radius);padding:8px 12px;font-size:13px;color:var(--fg-2);background:var(--bg-2)}
.compaction__head{display:flex;align-items:center;gap:7px;cursor:pointer}
.compaction__title{font-weight:600;color:var(--fg)}
.compaction__body{margin-top:6px;padding-top:6px;border-top:1px solid var(--border);white-space:pre-wrap;font-family:var(--mono);font-size:11.5px;line-height:1.5;max-height:180px;overflow-y:auto;color:var(--fg-2);display:none}
.compaction__body--open{display:block}
/* approval */
.approval{background:var(--panel);border:1px solid var(--accent);border-radius:var(--radius-lg);padding:12px 14px;margin:8px 0;box-shadow:var(--shadow-md);animation:msg-in .2s ease}
.approval__header{display:flex;align-items:center;gap:7px;margin-bottom:8px}
.approval__icon{color:var(--warning);width:16px;height:16px}
.approval__title{font-weight:600;font-size:13px}
.approval__subject{font-family:var(--mono);font-size:11.5px;color:var(--fg-2);padding:7px 10px;background:var(--bg-2);border:1px solid var(--border);border-radius:6px;margin-bottom:10px;white-space:pre-wrap;word-break:break-word;max-height:100px;overflow-y:auto}
.approval__actions{display:flex;gap:6px;flex-wrap:wrap}
.approval__btn{display:inline-flex;align-items:center;gap:5px;padding:6px 12px;border-radius:6px;font-size:12px;font-weight:500;transition:all .18s ease;border:1px solid var(--border)}
.approval__btn--allow{background:var(--accent-soft);color:var(--accent);border-color:var(--accent)}
.approval__btn--allow:hover{background:oklch(68% .16 38/.25)}
.approval__btn--deny{background:var(--bg-2);color:var(--fg-2)}
.approval__btn--deny:hover{background:var(--panel);color:var(--fg)}
.approval__key{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border:1px solid var(--border);border-radius:4px;font-family:var(--mono);font-size:9px;font-weight:700;color:var(--muted);background:var(--bg)}
/* ask card */
.ask{background:var(--panel);border:1px solid var(--border-strong);border-radius:var(--radius-lg);padding:14px;margin:8px 0;box-shadow:var(--shadow-md);animation:msg-in .2s ease}
.ask__prompt{font-weight:600;margin-bottom:10px;font-size:13px;line-height:1.5}
.ask__options{display:flex;flex-direction:column;gap:5px}
.ask__opt{display:flex;align-items:flex-start;gap:8px;width:100%;text-align:left;padding:8px 10px;background:var(--bg-2);border:1px solid var(--border);border-radius:6px;color:var(--fg);cursor:pointer;transition:all .18s ease}
.ask__opt:hover{border-color:var(--border-strong);background:var(--panel)}
.ask__opt--selected{border-color:var(--accent);background:var(--accent-soft)}
.ask__opt-label{font-size:12.5px;font-weight:500}
.ask__opt-desc{font-size:11.5px;color:var(--fg-2);margin-top:1px}
.ask__submit{margin-top:8px;width:100%;padding:7px;background:var(--accent);color:oklch(99% 0 0);border-radius:6px;font-weight:600;font-size:12px;transition:background .18s ease}
.ask__submit:hover{background:var(--accent-strong)}
.ask__submit:disabled{background:var(--panel-2);color:var(--muted)}
/* rewind picker */
.rewind-overlay{position:fixed;inset:0;background:oklch(0% 0 0/.55);display:flex;align-items:center;justify-content:center;z-index:100;animation:msg-in .15s ease}
.rewind-picker{background:var(--panel);border:1px solid var(--border-strong);border-radius:var(--radius-lg);width:min(480px,90vw);max-height:70vh;display:flex;flex-direction:column;box-shadow:var(--shadow-lg);overflow:hidden}
.rewind-picker__head{padding:12px 14px;border-bottom:1px solid var(--border);font-family:var(--heading);font-weight:600;font-size:14px;display:flex;align-items:center;gap:8px}
.rewind-picker__list{flex:1;overflow-y:auto;padding:6px}
.rewind-picker__item{display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:6px;cursor:pointer;font-size:13px;color:var(--fg-2);transition:background .15s ease}
.rewind-picker__item:hover,.rewind-picker__item--active{background:var(--card-hover);color:var(--fg)}
.rewind-picker__item--active{background:var(--accent-soft);color:var(--accent)}
.rewind-picker__turn{font-family:var(--mono);font-size:11px;color:var(--muted);flex-shrink:0;min-width:24px}
.rewind-picker__prompt{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rewind-picker__files{font-family:var(--mono);font-size:11px;color:var(--muted-2);flex-shrink:0}
.rewind-picker__scopes{padding:8px;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:3px}
.rewind-picker__scope{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:6px;cursor:pointer;font-size:12.5px;color:var(--fg-2);transition:background .15s ease}
.rewind-picker__scope:hover,.rewind-picker__scope--active{background:var(--card-hover);color:var(--fg)}
.rewind-picker__scope--active{background:var(--accent-soft);color:var(--accent)}
.rewind-picker__scope-key{font-family:var(--mono);font-size:10px;color:var(--muted);background:var(--panel-2);border:1px solid var(--border);border-radius:3px;padding:1px 5px;flex-shrink:0}
.rewind-picker__foot{padding:8px 14px;border-top:1px solid var(--border);display:flex;justify-content:space-between;font-size:11px;color:var(--muted-2)}
/* slash menu */
.slash-menu{position:absolute;bottom:calc(100% + 6px);left:0;right:0;max-height:min(360px,56vh);overflow:hidden;background:var(--panel);border:1px solid var(--border-strong);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);z-index:50;display:flex;flex-direction:column}
.slash-menu__head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 10px;border-bottom:1px solid var(--border);font-size:11.5px;color:var(--muted)}
.slash-menu__query{font-family:var(--mono);color:var(--fg-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.slash-menu__list{overflow-y:auto;padding:4px;min-height:0}
.slash-menu__group{padding:8px 8px 3px;font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted-2);font-weight:700}
.slash-menu__item{display:grid;grid-template-columns:minmax(92px,auto) minmax(0,1fr) auto minmax(90px,auto);align-items:center;gap:8px;width:100%;padding:7px 9px;background:none;border:none;border-radius:6px;color:inherit;font:inherit;text-align:left;cursor:pointer;transition:background .12s ease}
.slash-menu__item:hover,.slash-menu__item--active{background:var(--accent-soft)}
.slash-menu__name{font-family:var(--mono);font-size:12.5px;color:var(--accent);flex-shrink:0;font-weight:500}
.slash-menu__desc{font-size:12px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.slash-menu__sig{font-family:var(--mono);font-size:10.5px;color:var(--muted-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.slash-menu__pill{display:inline-flex;align-items:center;height:18px;padding:0 6px;border-radius:999px;background:var(--bg-2);border:1px solid var(--border);font-size:10px;color:var(--muted);white-space:nowrap}
.slash-menu__pill--danger{background:var(--danger-soft);border-color:transparent;color:var(--danger)}
.slash-menu__foot{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 10px;border-top:1px solid var(--border);font-size:10.5px;color:var(--muted-2)}
/* composer */
.composer{display:flex;gap:7px;align-items:center;background:var(--card);border:1px solid var(--border-strong);border-radius:14px;padding:4px 4px 4px 12px;box-shadow:var(--shadow-md);transition:border-color .18s ease,box-shadow .18s ease;overflow:hidden}
.composer:focus-within{border-color:var(--accent);box-shadow:var(--shadow-md),0 0 0 3px var(--accent-soft)}
.composer__caret{color:var(--accent);font-family:var(--mono);font-weight:600;font-size:16px;flex-shrink:0}
.composer__input{flex:1;border:none;background:none;color:var(--fg);font-size:14px;line-height:1.5;padding:9px 0;outline:none;resize:none;min-height:22px;max-height:140px;field-sizing:content}
.composer__input::placeholder{color:var(--muted-2)}
.composer__btn{display:flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:9px;flex-shrink:0;transition:background .18s ease,transform .15s ease}
.composer__btn--send{background:var(--accent);color:oklch(99% 0 0)}
.composer__btn--send:hover{transform:scale(1.05)}
.composer__btn--send:disabled{background:var(--panel-2);color:var(--muted);cursor:default;transform:none}
.composer__btn--stop{background:var(--panel-2);color:var(--danger)}
.composer__btn--stop:hover{background:var(--danger-soft)}
/* goal chip */
.goal-chip{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border-radius:99px;background:var(--accent-soft);border:1px solid var(--accent);font-size:12px;color:var(--accent);font-weight:500;cursor:pointer;transition:all .18s ease;max-width:320px;overflow:hidden}
.goal-chip:hover{background:oklch(68% .16 38/.25)}
.goal-chip__icon{flex-shrink:0;width:12px;height:12px}
.goal-chip__text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.goal-chip__close{flex-shrink:0;margin-left:2px;opacity:.6;font-size:14px;line-height:1}
.goal-chip__close:hover{opacity:1}
/* todo panel */
.todos{max-width:760px;margin:0 auto 8px;border:1px solid var(--border);border-radius:var(--radius-lg);background:var(--panel);overflow:hidden;animation:msg-in .2s ease;display:none}
.todos--visible{display:block}
.todos__head{display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;user-select:none;font-size:13px;color:var(--fg-2);transition:background .15s ease}
.todos__head:hover{background:var(--card-hover)}
.todos__title{font-weight:600;color:var(--fg);font-size:12.5px}
.todos__badge{font-family:var(--mono);font-size:11px;color:var(--accent);background:var(--accent-soft);padding:1px 7px;border-radius:99px}
.todos__summary{flex:1;font-size:11px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
.todos__chev{color:var(--muted-2);transition:transform .15s;display:inline-flex}
.todos--collapsed .todos__chev{transform:rotate(-90deg)}
.todos--collapsed .todos__list{display:none}
.todos__list{list-style:none;margin:0;padding:0 12px 8px;display:grid;gap:5px;max-height:220px;overflow-y:auto;border-top:1px solid var(--border);padding-top:8px}
.todos__item{display:grid;grid-template-columns:auto 1fr;align-items:flex-start;gap:8px;font-size:12.5px;line-height:1.5;padding:3px 0}
.todos__item--sub{padding-left:16px}
.todos__status{display:inline-flex;align-items:center;justify-content:center;padding:1px 7px;border:1px solid var(--border);border-radius:99px;background:var(--bg-2);color:var(--muted);font-size:10.5px;line-height:1.35;white-space:nowrap;min-width:0}
.todos__status--completed{border-color:var(--success-soft);color:var(--success);background:var(--success-soft)}
.todos__status--in_progress{border-color:var(--accent-soft);color:var(--accent);background:var(--accent-soft)}
.todos__text{min-width:0;color:var(--fg-2)}
.todos__item--completed .todos__text{color:var(--muted-2);text-decoration:line-through}
.todos__item--in_progress .todos__text{color:var(--fg);font-weight:500}
.todos__dismiss{flex-shrink:0;padding:2px 6px;color:var(--muted-2);font-size:14px;line-height:1;cursor:pointer;border-radius:4px}
.todos__dismiss:hover{color:var(--danger);background:var(--danger-soft)}
.composer__btn svg{width:17px;height:17px}
/* toolbar */
.toolbar{display:flex;align-items:center;gap:5px;flex-wrap:wrap}
.toolbar__btn{display:inline-flex;align-items:center;gap:4px;height:26px;padding:0 9px;border:1px solid var(--border);border-radius:6px;background:var(--bg-2);color:var(--muted);font-size:11.5px;transition:all .18s ease}
.toolbar__btn:hover{background:var(--panel);color:var(--fg);border-color:var(--border-strong)}
.toolbar__btn--active{background:var(--accent-soft);color:var(--accent);border-color:transparent}
.toolbar__btn--ok{background:var(--success-soft);color:var(--success);border-color:transparent}
.toolbar__btn--danger{background:var(--danger-soft);color:var(--danger);border-color:transparent}
.toolbar__btn--goal{background:var(--violet-soft);color:var(--violet);border-color:transparent}
.toolbar__btn--goal-active{background:var(--accent-soft);color:var(--accent);border-color:var(--accent)}
.toolbar__sep{width:1px;height:16px;background:var(--border)}
.toolbar__spacer{flex:1}
.status{font-family:var(--mono);font-size:11px;color:var(--muted);display:flex;align-items:center;gap:6px}
.status__dot{width:5px;height:5px;border-radius:50%;background:var(--muted-2);flex-shrink:0}
.status__dot--busy{background:var(--accent);animation:pulse 1.2s ease-in-out infinite}
@keyframes pulse{50%{opacity:.35}}
/* welcome */
.welcome{height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;position:relative}
.welcome::before{content:'';position:absolute;top:40%;left:50%;transform:translate(-50%,-50%);width:360px;height:360px;background:radial-gradient(circle,oklch(68% .16 38/.08) 0%,transparent 70%);pointer-events:none}
.welcome__brand{display:inline-flex;align-items:center;justify-content:center;margin-bottom:16px;position:relative}
.welcome__logo{width:52px;height:52px;border-radius:13px;background:linear-gradient(135deg,var(--accent),var(--violet));display:flex;align-items:center;justify-content:center;font-family:var(--heading);font-weight:700;font-size:20px;color:oklch(99% 0 0);margin-bottom:16px;box-shadow:0 4px 18px oklch(68% .16 38/.2);position:relative}
.welcome__title{font-family:var(--heading);font-size:24px;font-weight:700;letter-spacing:-.03em;position:relative}
.welcome__tag{margin-top:6px;font-size:13.5px;color:var(--fg-2);position:relative}
.welcome__meta{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin-top:14px;max-width:680px;position:relative}
.welcome__pill{display:inline-flex;align-items:center;gap:6px;max-width:320px;height:28px;padding:0 10px;border:1px solid var(--border);border-radius:999px;background:var(--bg-2);color:var(--fg-2);font-size:12px}
.welcome__pill strong{font-weight:600;color:var(--fg)}
.welcome__pill span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.welcome__hints{display:flex;gap:18px;margin-top:20px;font-size:12px;color:var(--muted);position:relative}
.welcome__hints span{display:inline-flex;align-items:center;gap:5px}
.welcome__hints kbd{font-family:var(--mono);font-size:10px;background:var(--panel-2);border:1px solid var(--border);border-radius:4px;padding:1px 6px;color:var(--fg-2)}
.welcome__examples{display:flex;flex-direction:column;gap:7px;margin-top:24px;width:100%;max-width:400px;position:relative}
.welcome__ex{text-align:left;background:var(--bg-2);border:1px solid var(--border);border-left:3px solid var(--accent);padding:9px 12px;border-radius:6px;color:var(--fg-2);font-size:12.5px;cursor:pointer;transition:all .18s ease}
.welcome__ex:hover{color:var(--fg);border-color:var(--accent);background:var(--panel);transform:translateY(-1px)}
.loading{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:12px}
.spinner{width:22px;height:22px;border:2.5px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite}
.loading__text{font-size:12.5px;color:var(--muted)}
/* session list */
.session-list{flex:1;overflow-y:auto;padding:4px 6px;display:flex;flex-direction:column;gap:1px}
.session-search{padding:0 8px 4px}
.session-search__input{width:100%;height:28px;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--fg);padding:0 9px;font-size:12px;outline:none}
.session-search__input::placeholder{color:var(--muted-2)}
.session-search__input:focus{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}
.session-item{display:flex;align-items:center;gap:7px;padding:7px 8px;border-radius:6px;cursor:pointer;font-size:12px;color:var(--fg-2);transition:background .15s ease,color .15s ease;overflow:hidden}
.session-item:hover{background:var(--panel);color:var(--fg)}
.session-item--active{background:var(--accent-soft);color:var(--accent)}
.session-item__icon{flex-shrink:0;opacity:.5}
.session-item__body{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
.session-item__title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500;font-size:12.5px;line-height:1.3}
.session-item__meta{font-family:var(--mono);font-size:10px;color:var(--muted-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.session-item__path{font-family:var(--mono);font-size:10px;color:var(--muted-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.session-del{flex-shrink:0;padding:2px 6px;color:var(--muted-2);font-size:14px;line-height:1;display:none}
.session-item:hover .session-del,.session-del:focus-visible{display:inline-flex}
.session-del:hover{color:var(--danger)}
@media(max-width:768px){
.app{grid-template-columns:1fr}
.sidebar{position:fixed;inset:0 auto 0 0;width:260px;z-index:100;transform:translateX(-100%);transition:transform .25s ease;box-shadow:var(--shadow-lg)}
.sidebar--open{transform:translateX(0)}
.sidebar-overlay{position:fixed;inset:0;background:oklch(0% 0 0/.45);z-index:99;opacity:0;pointer-events:none;transition:opacity .25s ease}
.sidebar-overlay--visible{opacity:1;pointer-events:auto}
.menu-btn{display:flex;align-items:center;justify-content:center;position:fixed;top:8px;left:8px;z-index:98;width:34px;height:34px;border-radius:var(--radius);background:var(--panel);border:1px solid var(--border);color:var(--fg-2);transition:background .15s}
.menu-btn:hover{background:var(--card-hover);color:var(--fg)}
.menu-btn svg{width:16px;height:16px}
.footer{padding:10px 16px 12px}
.transcript{padding:16px}
.slash-menu__item{grid-template-columns:minmax(78px,auto) 1fr auto}
.slash-menu__sig{grid-column:2 / -1}
.slash-menu__foot{display:none}
.card-head{grid-template-columns:auto minmax(0,1fr) auto}
.card-actions{grid-column:3}
.card-badge{display:none}
}
@media(min-width:769px){
.menu-btn,.sidebar-overlay{display:none}
}
/* stats modal */
.modal-overlay{position:fixed;inset:0;background:oklch(0% 0 0/.55);display:flex;align-items:center;justify-content:center;z-index:200;animation:msg-in .15s ease}
.modal{background:var(--panel);border:1px solid var(--border-strong);border-radius:var(--radius-lg);width:min(520px,90vw);max-height:80vh;overflow-y:auto;box-shadow:var(--shadow-lg)}
.modal__head{padding:14px 16px;border-bottom:1px solid var(--border);font-family:var(--heading);font-weight:600;font-size:15px;display:flex;align-items:center;gap:8px}
.modal__body{padding:14px 16px}
.modal__close{margin-left:auto;cursor:pointer;color:var(--muted-2);width:24px;height:24px;display:flex;align-items:center;justify-content:center;border-radius:6px;transition:background .15s}
.modal__close:hover{background:var(--card-hover);color:var(--fg)}
.stat-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.stat-card{background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius);padding:12px}
.stat-card__label{font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);font-weight:600}
.stat-card__value{font-family:var(--mono);font-size:18px;font-weight:600;color:var(--fg);margin-top:4px}
.stat-card__value.acc{color:var(--accent)}
.stat-card__value.ok{color:var(--success)}
.stat-card--wide{grid-column:1/-1}
.sidebar__item--danger{color:var(--danger)}
.branch-tree{font-family:var(--mono);font-size:12px;line-height:1.6;color:var(--fg-2);white-space:pre-wrap;background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius);padding:10px;max-height:220px;overflow:auto}
.branch-list{display:grid;gap:6px;margin-top:10px}
.branch-item{display:grid;grid-template-columns:1fr auto;gap:8px;align-items:center;padding:8px 10px;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-2)}
.branch-item__title{font-weight:600;font-size:12.5px;color:var(--fg);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.branch-item__meta{font-family:var(--mono);font-size:10.5px;color:var(--muted-2);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.branch-item__btn{height:26px;padding:0 9px;border:1px solid var(--border);border-radius:6px;background:var(--panel);color:var(--fg-2);font-size:11.5px}
.branch-item__btn:hover{border-color:var(--border-strong);color:var(--fg)}
.empty-note{font-size:12.5px;color:var(--muted);padding:10px;border:1px dashed var(--border);border-radius:var(--radius);background:var(--bg-2)}
.model-list{display:grid;gap:6px}
.model-item{display:grid;grid-template-columns:1fr auto;gap:8px;align-items:center;padding:9px 10px;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-2)}
.model-item--active{border-color:var(--accent);background:var(--accent-soft)}
.model-item__title{font-family:var(--mono);font-size:12.5px;color:var(--fg);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.model-item__meta{font-size:11px;color:var(--muted);margin-top:2px}
.model-item__status{display:inline-flex;align-items:center;height:22px;padding:0 8px;border-radius:999px;border:1px solid var(--border);color:var(--muted);font-size:11px;white-space:nowrap}
.model-item__status--active{border-color:transparent;background:var(--success-soft);color:var(--success)}
.dialog-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}
.dialog-btn{height:30px;padding:0 12px;border:1px solid var(--border);border-radius:6px;background:var(--bg-2);color:var(--fg-2);font-size:12px}
.dialog-btn:hover{border-color:var(--border-strong);color:var(--fg)}
.dialog-btn--danger{background:var(--danger-soft);border-color:transparent;color:var(--danger)}
</style>
</head>
<body>
<button class="menu-btn" id="menu-btn" aria-label="Toggle sidebar">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
<div class="sidebar-overlay" id="sidebar-overlay"></div>
<div class="app">
<aside class="sidebar">
<div class="sidebar__brand">
<img src="/assets/logo-wordmark.svg" alt="Reasonix" class="brand-wordmark brand-wordmark--sidebar" draggable="false">
</div>
<nav class="sidebar__nav" style="border-top:none;padding-top:0">
<div class="sidebar__item sidebar__item--accent" id="btn-new">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>
<span>__('new_session')</span>
</div>
<div class="sidebar__item" id="btn-compact">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" y1="10" x2="21" y2="3"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
<span>__('compact')</span>
</div>
<div class="sidebar__item" id="btn-rewind">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
<span>__('rewind')</span>
</div>
<div class="sidebar__item" id="btn-tree">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>
<span>__('branches')</span>
</div>
<div class="sidebar__item" id="btn-models">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M9 1v3"/><path d="M15 1v3"/><path d="M9 20v3"/><path d="M15 20v3"/><path d="M20 9h3"/><path d="M20 14h3"/><path d="M1 9h3"/><path d="M1 14h3"/></svg>
<span>__('models')</span>
</div>
<div class="sidebar__sep"></div>
<div class="sidebar__item" id="btn-stats">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 20V10"/><path d="M12 20V4"/><path d="M6 20v-6"/></svg>
<span>__('stats')</span>
</div>
</nav>
<div class="sidebar__label-row"><span class="sidebar__label" style="padding:0">__('sessions')</span><span id="session-count" class="session-item__meta"></span></div>
<div class="session-search">
<input class="session-search__input" id="session-search" type="search" placeholder="__('search_sessions')" aria-label="__('search_sessions')">
</div>
<div class="session-list" id="session-list">
<div style="padding:10px;color:var(--muted-2);font-size:12px">__('loading')</div>
</div>
<div class="sidebar__section">
<div class="sidebar__label">__('status')</div>
<div class="sidebar__ctx">
<div class="ctx-bar"><div class="ctx-bar__fill" id="ctx-fill" style="width:0%"></div></div>
<div class="ctx-label"><span id="ctx-used">0 tok</span><span id="ctx-window">0 tok</span></div>
</div>
<div class="status-metrics" id="status-metrics">
<div class="sm-item"><span class="sm-val" id="sm-cache"></span><span>__('cache')</span></div>
<div class="sm-item"><span class="sm-val" id="sm-cost"></span><span>__('cost')</span></div>
<div class="sm-item"><span class="sm-val acc" id="sm-balance"></span><span>__('balance')</span></div>
</div>
<div style="padding:4px 10px">
<div class="status" id="sidebar-status"><span class="status__dot" id="status-dot"></span><span id="status-model">-</span></div>
</div>
</div>
</aside>
<section class="transcript" id="log">
<div class="welcome" id="welcome">
<div class="welcome__brand"><img src="/assets/logo-wordmark.svg" alt="Reasonix" class="brand-wordmark brand-wordmark--welcome" draggable="false"></div>
<div class="welcome__tag">__('welcome_tag')</div>
<div class="welcome__meta">
<div class="welcome__pill"><strong>__('model')</strong><span id="welcome-model">-</span></div>
<div class="welcome__pill"><strong>__('workspace')</strong><span id="welcome-cwd">-</span></div>
</div>
<div class="welcome__hints">
<span><kbd>/</kbd> __('hint_commands')</span>
<span><kbd>Shift+Tab</kbd> __('hint_mode')</span>
<span><kbd>Ctrl+Y</kbd> __('hint_yolo')</span>
<span><kbd>Esc&times;2</kbd> __('hint_rewind')</span>
</div>
<div class="welcome__examples">
<button class="welcome__ex" data-prompt="">__('example_explain')</button>
<button class="welcome__ex" data-prompt="">__('example_fix')</button>
<button class="welcome__ex" data-prompt="">__('example_test')</button>
</div>
</div>
</section>
<footer class="footer">
<div id="slash-menu-anchor"></div>
<div id="goal-active-bar" style="margin-bottom:6px;display:none">
<span class="goal-chip" id="goal-chip" title="__('goal_exit')">
<svg class="goal-chip__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/></svg>
<span class="goal-chip__text" id="goal-chip-text"></span>
<span class="goal-chip__close">&times;</span>
</span>
</div>
<div id="todo-panel" class="todos">
<div class="todos__head" id="todos-head">
<span class="todos__title" id="todos-title">To-dos</span>
<span class="todos__badge" id="todos-badge"></span>
<span class="todos__summary" id="todos-summary"></span>
<span class="todos__dismiss" id="todos-dismiss" title="Close" style="display:none">&times;</span>
<span class="todos__chev"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg></span>
</div>
<ul class="todos__list" id="todos-list"></ul>
</div>
<div class="toolbar">
<button class="toolbar__btn toolbar__btn--ok" id="btn-auto" title="__('auto_mode')">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M8 12l2 2 4-4"/></svg>
__('auto')
</button>
<button class="toolbar__btn" id="btn-plan" title="__('plan_mode')">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
__('plan')
</button>
<button class="toolbar__btn" id="btn-bypass" title="__('yolo_mode')">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
__('yolo')
</button>
<button class="toolbar__btn" id="btn-goal" title="__('goal_mode_desc')">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><line x1="12" y1="2" x2="12" y2="4"/><line x1="12" y1="20" x2="12" y2="22"/><line x1="2" y1="12" x2="4" y2="12"/><line x1="20" y1="12" x2="22" y2="12"/></svg>
__('goal_btn')
</button>
<div class="toolbar__sep"></div>
<div class="status">
<span class="status__dot" id="status-dot-footer"></span>
<span id="status-text">__('ready')</span>
</div>
<div class="toolbar__spacer"></div>
<div class="status" id="turn-info"></div>
<div class="status" id="balance-info"></div>
</div>
<div class="composer">
<span class="composer__caret">&#8250;</span>
<textarea class="composer__input" id="in" placeholder="__('placeholder')" rows="1" autofocus></textarea>
<button class="composer__btn composer__btn--send" id="btn-send" title="__('send')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>
</button>
<button class="composer__btn composer__btn--stop" id="btn-stop" title="__('cancel')" style="display:none">
<svg viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
</button>
</div>
</footer>
</div>
<!-- stats modal -->
<div class="modal-overlay" id="stats-modal" style="display:none">
<div class="modal">
<div class="modal__head">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 20V10"/><path d="M12 20V4"/><path d="M6 20v-6"/></svg>
__('statistics')
<span class="modal__close" id="stats-modal-close">&times;</span>
</div>
<div class="modal__body">
<div class="stat-grid">
<div class="stat-card">
<div class="stat-card__label">__('model')</div>
<div class="stat-card__value" id="stats-model">-</div>
</div>
<div class="stat-card">
<div class="stat-card__label">__('sessions')</div>
<div class="stat-card__value" id="stats-sessions">0</div>
</div>
<div class="stat-card">
<div class="stat-card__label">__('total_tokens')</div>
<div class="stat-card__value acc" id="stats-total-tokens">0</div>
</div>
<div class="stat-card">
<div class="stat-card__label">__('cache_hit_rate')</div>
<div class="stat-card__value ok" id="stats-cache-rate">0%</div>
</div>
<div class="stat-card">
<div class="stat-card__label">__('total_cost')</div>
<div class="stat-card__value" id="stats-total-cost">-</div>
</div>
<div class="stat-card">
<div class="stat-card__label">__('balance')</div>
<div class="stat-card__value" id="stats-balance">-</div>
</div>
<div class="stat-card stat-card--wide">
<div class="stat-card__label">__('context_usage')</div>
<div class="ctx-bar" style="margin-top:8px"><div class="ctx-bar__fill" id="stats-ctx-fill" style="width:0%"></div></div>
<div class="ctx-label" style="margin-top:4px"><span id="stats-ctx-used">0 tok</span><span id="stats-ctx-window">0 tok</span></div>
</div>
</div>
</div>
</div>
</div>
<!-- branches modal -->
<div class="modal-overlay" id="branches-modal" style="display:none">
<div class="modal">
<div class="modal__head">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>
__('branches')
<span class="modal__close" id="branches-modal-close">&times;</span>
</div>
<div class="modal__body">
<div class="branch-tree" id="branches-tree">__('loading')</div>
<div class="branch-list" id="branches-list"></div>
</div>
</div>
</div>
<!-- models modal -->
<div class="modal-overlay" id="models-modal" style="display:none">
<div class="modal">
<div class="modal__head">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/></svg>
__('models')
<span class="modal__close" id="models-modal-close">&times;</span>
</div>
<div class="modal__body">
<div class="model-list" id="models-list"></div>
</div>
</div>
</div>
<!-- delete confirmation modal -->
<div class="modal-overlay" id="delete-modal" style="display:none">
<div class="modal">
<div class="modal__head">
__('delete_session')
<span class="modal__close" id="delete-modal-close">&times;</span>
</div>
<div class="modal__body">
<div class="empty-note" id="delete-session-title"></div>
<div class="dialog-actions">
<button class="dialog-btn" id="delete-cancel" type="button">__('cancel_plain')</button>
<button class="dialog-btn dialog-btn--danger" id="delete-confirm" type="button">__('delete')</button>
</div>
</div>
</div>
</div>
<script>
// ── i18n ──
const __LANG_PREF = '__LANG__';
const __LANG = (__LANG_PREF === 'zh' || __LANG_PREF === 'en') ? __LANG_PREF : ((navigator.language || '').startsWith('zh') ? 'zh' : 'en');
document.documentElement.lang = __LANG;
const __T = {
en: {
'new_session': 'New Session',
'compact': 'Compact',
'rewind': 'Rewind',
'branches': 'Branches',
'models': 'Models',
'sessions': 'Sessions',
'status': 'Status',
'loading': 'Loading...',
'cache': 'Cache',
'cost': 'Cost',
'balance': 'Balance',
'ready': 'Ready',
'thinking': 'Thinking...',
'retrying_status': 'Retrying ({attempt}/{max})...',
'stats': 'Stats',
'statistics': 'Statistics',
'model': 'Model',
'workspace': 'Workspace',
'total_tokens': 'Total Tokens',
'cache_hit_rate': 'Cache Hit Rate',
'total_cost': 'Session Cost',
'context_usage': 'Context Usage',
'connected': 'Connected',
'reconnecting': 'Reconnecting...',
'disconnected': 'Disconnected',
'placeholder': 'Message Reasonix... / for commands',
'cmd_compact': 'Compact conversation',
'cmd_new': 'New session',
'cmd_resume': 'Resume session',
'cmd_rewind': 'Rewind to checkpoint',
'cmd_tree': 'Show branch tree',
'cmd_branch': 'Create branch',
'cmd_switch': 'Switch branch',
'cmd_model': 'List/switch models',
'cmd_effort': 'Reasoning effort level',
'cmd_mcp': 'MCP servers',
'cmd_skill': 'Skills',
'cmd_hooks': 'Hooks',
'cmd_memory': 'Show memory',
'cmd_forget': 'Forget memory',
'cmd_goal': 'Set a goal for the agent to pursue autonomously',
'cmd_thinking': 'Thinking effort',
'cmd_verbose': 'Toggle reasoning',
'cmd_help': 'Help',
'cmd_group_session': 'Session',
'cmd_group_branch': 'Branches',
'cmd_group_model': 'Model',
'cmd_group_system': 'System',
'cmd_group_memory': 'Memory',
'cmd_group_agent': 'Agent',
'cmd_group_help': 'Help',
'command_palette': 'Command palette',
'command_empty': 'No commands match',
'command_nav': '↑↓ navigate · Enter insert · Esc close',
'danger': 'Danger',
'tool_running': 'Running',
'tool_done': 'Done',
'tool_failed': 'Failed',
'tool_copy': 'Copy output',
'tool_copied': 'Copied',
'tool_no_output': 'No output',
'tool_args': 'Args',
'tool_output': 'Output',
'tool_lines': 'lines',
'submit': 'Submit',
'approval_title': 'Approval Required',
'allow': 'Allow',
'session': 'Allow for session',
'session_command': 'Command for session',
'session_prefix': 'Command prefix for session',
'persist_command': 'Always allow command (save)',
'persist_prefix': 'Always allow command prefix (save)',
'persist_edit': 'Always allow edits (save)',
'persist_tool': 'Always allow (save)',
'session_edit': 'Edits for session',
'session_tool': 'Tool for session',
'deny': 'Deny',
'auto_mode': 'Auto mode (auto-approve fallback approvals)',
'plan_mode': 'Plan mode (read-only)',
'yolo_mode': 'YOLO mode (auto-approve tool approvals; ask and plan still wait)',
'goal_mode_desc': 'Goal mode — type a task for the agent to pursue autonomously',
'goal_mode': 'Goal mode',
'goal_placeholder': 'Describe your goal...',
'goal_active': 'Active goal',
'goal_exit': 'Exit goal mode',
'auto': 'Auto',
'plan': 'Plan',
'goal_btn': 'Goal',
'yolo': 'YOLO',
'send': 'Send (Enter)',
'cancel': 'Cancel (Esc)',
'cancel_plain': 'Cancel',
'delete': 'Delete',
'delete_session': 'Delete Session',
'search_sessions': 'Search sessions',
'active': 'Active',
'use_model': 'Use',
'no_sessions': 'No sessions',
'error_loading': 'Error loading',
'no_checkpoints': 'No checkpoints available.',
'no_conversation': 'Start a conversation before using this action.',
'no_branches': 'No conversation branches yet.',
'switch': 'Switch',
'compacted': 'Compacted',
'messages': 'messages',
'compacting': 'Compacting...',
'scope_both': 'Code + conversation',
'scope_conversation': 'Conversation only',
'scope_code': 'Code only',
'scope_fork': 'Fork (new branch)',
'scope_sumfrom': 'Summarize from here',
'scope_sumupto': 'Summarize up to here',
'delete_confirm': 'Delete this session?',
'welcome_tag': 'AI coding agent',
'hint_commands': '/ commands',
'hint_mode': 'Plan',
'hint_yolo': 'YOLO',
'hint_rewind': 'Esc×2 rewind',
'example_explain': 'Explain the project structure',
'example_fix': 'Find and fix any bugs',
'example_test': 'Write tests for the main module',
'total': 'Total',
'in': 'In',
'out': 'Out',
'nav_jk': 'j/k or ↑↓ to navigate',
'nav_enter_esc': 'Enter to select · Esc to close',
'nav_keys': 'b/c/d/f/s/u quick keys',
'nav_apply_esc': 'Enter to apply · Esc to go back',
'rewind_title': 'Rewind — Select Turn',
'action_title': 'Turn #{turn} — Select Action',
'files': 'files',
},
zh: {
'new_session': '新会话',
'compact': '压缩',
'rewind': '回退',
'branches': '分支',
'models': '模型',
'sessions': '会话',
'status': '状态',
'loading': '加载中...',
'cache': '缓存',
'cost': '费用',
'balance': '余额',
'ready': '就绪',
'thinking': '思考中...',
'retrying_status': '正在重试 ({attempt}/{max})...',
'stats': '统计',
'statistics': '统计',
'model': '模型',
'workspace': '工作区',
'total_tokens': '总 Token',
'cache_hit_rate': '缓存命中率',
'total_cost': '会话费用',
'context_usage': '上下文用量',
'connected': '已连接',
'reconnecting': '重新连接...',
'disconnected': '已断开',
'placeholder': '给 Reasonix 发消息... / 查看命令',
'cmd_compact': '压缩对话',
'cmd_new': '新建会话',
'cmd_resume': '恢复会话',
'cmd_rewind': '回退到检查点',
'cmd_tree': '显示分支树',
'cmd_branch': '创建分支',
'cmd_switch': '切换分支',
'cmd_model': '列出/切换模型',
'cmd_effort': '推理努力级别',
'cmd_mcp': 'MCP 服务器',
'cmd_skill': '技能',
'cmd_hooks': '钩子',
'cmd_memory': '显示记忆',
'cmd_forget': '忘记记忆',
'cmd_goal': '设置目标让代理自主执行',
'cmd_thinking': '思考努力',
'cmd_verbose': '切换推理显示',
'cmd_help': '帮助',
'cmd_group_session': '会话',
'cmd_group_branch': '分支',
'cmd_group_model': '模型',
'cmd_group_system': '系统',
'cmd_group_memory': '记忆',
'cmd_group_agent': '代理',
'cmd_group_help': '帮助',
'command_palette': '命令面板',
'command_empty': '没有匹配的命令',
'command_nav': '↑↓ 选择 · Enter 插入 · Esc 关闭',
'danger': '危险',
'tool_running': '运行中',
'tool_done': '完成',
'tool_failed': '失败',
'tool_copy': '复制输出',
'tool_copied': '已复制',
'tool_no_output': '无输出',
'tool_args': '参数',
'tool_output': '输出',
'tool_lines': '行',
'submit': '提交',
'approval_title': '需要批准',
'allow': '允许',
'session': '本会话允许',
'session_command': '本会话允许此命令',
'session_prefix': '本会话允许命令前缀',
'persist_command': '总是允许此命令(保存)',
'persist_prefix': '总是允许命令前缀(保存)',
'persist_edit': '总是允许编辑(保存)',
'persist_tool': '总是允许(保存)',
'session_edit': '本会话允许编辑',
'session_tool': '本会话允许此工具',
'deny': '拒绝',
'auto_mode': '自动模式(自动批准兜底审批)',
'plan_mode': '计划模式(只读)',
'yolo_mode': 'YOLO 模式(自动批准工具权限;ask 和计划仍会等待)',
'goal_mode_desc': '目标模式 — 输入任务让代理自主执行',
'goal_mode': '目标模式',
'goal_placeholder': '描述你的目标...',
'goal_active': '活跃目标',
'goal_exit': '退出目标模式',
'auto': '自动',
'plan': '计划',
'goal_btn': '目标',
'yolo': 'YOLO',
'send': '发送 (Enter)',
'cancel': '取消 (Esc)',
'cancel_plain': '取消',
'delete': '删除',
'delete_session': '删除会话',
'search_sessions': '搜索会话',
'active': '当前',
'use_model': '使用',
'no_sessions': '暂无会话',
'error_loading': '加载失败',
'no_checkpoints': '暂无可用检查点',
'no_conversation': '先开始一段对话,再使用此操作。',
'no_branches': '暂无会话分支',
'switch': '切换',
'compacted': '已压缩',
'messages': '条消息',
'compacting': '压缩中...',
'scope_both': '代码 + 对话',
'scope_conversation': '仅对话',
'scope_code': '仅代码',
'scope_fork': '分叉(新分支)',
'scope_sumfrom': '从此处总结',
'scope_sumupto': '总结到此处',
'delete_confirm': '删除此会话?',
'welcome_tag': 'AI 编码助手',
'hint_commands': '/ 命令',
'hint_mode': '计划',
'hint_yolo': 'YOLO',
'hint_rewind': 'Esc×2 回退',
'example_explain': '解释项目结构',
'example_fix': '查找并修复错误',
'example_test': '为主模块编写测试',
'total': '总计',
'in': '输入',
'out': '输出',
'nav_jk': 'j/k 或 ↑↓ 导航',
'nav_enter_esc': 'Enter 选择 · Esc 关闭',
'nav_keys': 'b/c/d/f/s/u 快捷键',
'nav_apply_esc': 'Enter 应用 · Esc 返回',
'rewind_title': '回退 — 选择轮次',
'action_title': '第 #{turn} 轮 — 选择操作',
'files': '个文件',
}
};
const __ = (key, ...args) => {
let s = __T[__LANG]?.[key] ?? __T.en[key] ?? key;
if (args.length) args.forEach((v, i) => { s = s.replace('#{' + ['turn','n'][i] + '}', v); });
return s;
};
function translateTokens(text) {
return String(text).replace(/__\('([^']+)'\)/g, (_, key) => __(key));
}
function applyStaticI18n() {
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
acceptNode(node) {
const parent = node.parentElement;
if (!parent || parent.closest('script,style')) return NodeFilter.FILTER_REJECT;
return node.nodeValue.includes("__('") ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
}
});
const nodes = [];
for (let node = walker.nextNode(); node; node = walker.nextNode()) nodes.push(node);
nodes.forEach(node => { node.nodeValue = translateTokens(node.nodeValue); });
document.querySelectorAll('[title],[placeholder]').forEach(node => {
if (node.hasAttribute('title')) node.setAttribute('title', translateTokens(node.getAttribute('title')));
if (node.hasAttribute('placeholder')) node.setAttribute('placeholder', translateTokens(node.getAttribute('placeholder')));
});
}
applyStaticI18n();
// ── populate welcome examples ──
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.welcome__ex').forEach((btn, i) => {
const keys = ['example_explain', 'example_fix', 'example_test'];
if (i < keys.length) btn.dataset.prompt = __(keys[i]);
});
});
const $ = s => document.querySelector(s);
const $$ = s => document.querySelectorAll(s);
const log = $('#log'), input = $('#in'), btnSend = $('#btn-send'), btnStop = $('#btn-stop');
const statusDot = $('#status-dot-footer'), statusText = $('#status-text');
const turnInfo = $('#turn-info'), balanceInfo = $('#balance-info');
const ctxFill = $('#ctx-fill'), ctxUsed = $('#ctx-used'), ctxWindow = $('#ctx-window');
const statusDotSidebar = $('#status-dot'), statusModel = $('#status-model');
const welcome = $('#welcome');
const welcomeModel = $('#welcome-model'), welcomeCwd = $('#welcome-cwd');
const slashAnchor = $('#slash-menu-anchor');
// ── state ──
let running = false, planMode = false, bypassMode = false, toolApprovalMode = 'ask', yoloRestoreMode = 'ask';
let goalMode = false, goalActive = false, goalText = '';
let currentMsg = null, currentText = null, currentReasoning = null;
let turnStartAt = 0, turnTokens = 0, tickTimer = null, retryStatus = null;
let toolCards = {};
let escTimer = null;
let hasVisibleHistory = false, checkpointCount = 0;
let sessionsCache = [], sessionFilter = '', pendingDeleteSession = null;
// ── slash commands registry ──
const SLASH_CMDS = [
{cmd:'new',sig:'/new',desc:__('cmd_new'),group:'session'},
{cmd:'resume',sig:'/resume [n]',desc:__('cmd_resume'),group:'session'},
{cmd:'compact',sig:'/compact',desc:__('cmd_compact'),group:'session'},
{cmd:'rewind',sig:'/rewind',desc:__('cmd_rewind'),group:'session'},
{cmd:'tree',sig:'/tree',desc:__('cmd_tree'),group:'branch'},
{cmd:'branch',sig:'/branch <name>',desc:__('cmd_branch'),group:'branch'},
{cmd:'switch',sig:'/switch <id>',desc:__('cmd_switch'),group:'branch'},
{cmd:'model',sig:'/model [provider/model]',desc:__('cmd_model'),group:'model'},
{cmd:'effort',sig:'/effort <level>',desc:__('cmd_effort'),group:'model'},
{cmd:'goal',sig:'/goal <task>',desc:__('cmd_goal'),group:'agent'},
{cmd:'thinking',sig:'/thinking <level>',desc:__('cmd_thinking'),group:'agent'},
{cmd:'verbose',sig:'/verbose',desc:__('cmd_verbose'),group:'agent'},
{cmd:'mcp',sig:'/mcp',desc:__('cmd_mcp'),group:'system'},
{cmd:'skill',sig:'/skill',desc:__('cmd_skill'),group:'system'},
{cmd:'hooks',sig:'/hooks',desc:__('cmd_hooks'),group:'system'},
{cmd:'memory',sig:'/memory',desc:__('cmd_memory'),group:'memory'},
{cmd:'forget',sig:'/forget <item>',desc:__('cmd_forget'),group:'memory',danger:true},
{cmd:'help',sig:'/help',desc:__('cmd_help'),group:'help'},
];
const SLASH_GROUPS = ['session','branch','model','agent','system','memory','help'];
function slashGroupLabel(group){return __('cmd_group_'+group)||group;}
// ── helpers ──
function post(path, body) {
return fetch(path, {method:'POST',headers:{'content-type':'application/json'},body:body?JSON.stringify(body):undefined});
}
function scrollDown() { requestAnimationFrame(()=>{log.scrollTop=log.scrollHeight;});}
function hideWelcome() { if(welcome) welcome.style.display='none';}
function showWelcome() { if(welcome) welcome.style.display='';}
function fmtTok(n) { return n>=1000?(n/1000).toFixed(1).replace(/\.0$/,'')+'k':String(n);}
function currencySymbol(c){const v=String(c||'¥').trim();if(/^(cny|rmb|yuan)$/i.test(v))return '¥';if(/^(usd|dollar)$/i.test(v))return '$';return v||'¥';}
function fmtMoney(n,c){if(typeof n!=='number'||!isFinite(n))return '—';const s=currencySymbol(c);return s+(n<1?n.toFixed(4):n.toFixed(2));}
function fmtElapsed(ms) { const s=Math.floor(ms/1000); return s<60?s+'s':Math.floor(s/60)+'m '+s%60+'s';}
function escHtml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
function escAttr(s) { return escHtml(s).replace(/"/g,'&quot;').replace(/'/g,'&#39;');}
function el(tag,cls,text) { const e=document.createElement(tag); if(cls)e.className=cls; if(text)e.textContent=text; return e;}
function compactText(s, max) {
const text=String(s||'').replace(/\s+/g,' ').trim();
return text.length>max?text.slice(0,max-1)+'…':text;
}
function lineCount(s) {
const text=String(s||'');
return text?text.split(/\r\n|\r|\n/).length:0;
}
function actionDisabled(id){return $('#'+id)?.classList.contains('sidebar__item--disabled');}
function setActionDisabled(id, disabled, title) {
const node = $('#'+id);
if(!node)return;
node.classList.toggle('sidebar__item--disabled', !!disabled);
node.setAttribute('aria-disabled', disabled ? 'true' : 'false');
if(title)node.setAttribute('title', title);
else node.removeAttribute('title');
}
function showNotice(text, tone) {
hideWelcome();
const n=el('div','notice'+(tone==='warn'?' notice--warn':''),text);
log.appendChild(n);
scrollDown();
}
function updateActionAvailability() {
setActionDisabled('btn-compact', running || !hasVisibleHistory, running ? __('thinking') : __('no_conversation'));
setActionDisabled('btn-rewind', running || checkpointCount === 0, running ? __('thinking') : __('no_checkpoints'));
}
function setRunning(on) {
running=on;
retryStatus=null;
btnSend.style.display=on?'none':'';
btnStop.style.display=on?'':'none';
statusDot.className=on?'status__dot status__dot--busy':'status__dot';
statusDotSidebar.className=statusDot.className;
statusText.textContent=on?(goalActive?__('goal_active')+' · ':'')+__('thinking'):__('ready');
updateActionAvailability();
if(on){turnStartAt=Date.now();turnTokens=0;tickTimer=setInterval(()=>{
const ms=Date.now()-turnStartAt; const tok=turnTokens>0?' · ↓ '+fmtTok(turnTokens)+' tok':'';
turnInfo.textContent=fmtElapsed(ms)+tok;
},1000);} else {clearInterval(tickTimer);turnInfo.textContent='';}
}
function setRetrying(attempt,max) {
retryStatus={attempt:attempt||0,max:max||0};
statusDot.className='status__dot status__dot--busy';
statusDotSidebar.className=statusDot.className;
statusText.textContent=__('retrying_status').replace('{attempt}',retryStatus.attempt).replace('{max}',retryStatus.max);
}
function clearRetrying() {
if(!retryStatus)return;
retryStatus=null;
if(running)statusText.textContent=__('thinking');
}
function setConnState(state) {
// state: 'connected' | 'reconnecting' | 'disconnected'
const colors={connected:'var(--success)',reconnecting:'var(--warning)',disconnected:'var(--danger)'};
const labels={connected:__('connected'),reconnecting:__('reconnecting'),disconnected:__('disconnected')};
if(!running){
statusDot.style.background=colors[state]||'';
statusDot.className='status__dot'+(state==='reconnecting'?' status__dot--busy':'');
statusText.textContent=labels[state]||state;
}
statusDotSidebar.style.background=colors[state]||'';
}
// ── messages ──
function addUserMsg(text) { hasVisibleHistory=true; updateActionAvailability(); hideWelcome(); const d=el('div','msg msg--user'); d.appendChild(el('span','msg__caret','')); d.appendChild(el('div','msg__text',text)); log.appendChild(d); scrollDown(); currentMsg=null;currentText=null;currentReasoning=null; }
function ensureMsg() { if(!currentMsg){hasVisibleHistory=true; updateActionAvailability(); hideWelcome();currentMsg=el('div','msg msg--assistant');log.appendChild(currentMsg);} return currentMsg; }
function appendText(t) { const m=ensureMsg(); if(!currentText){currentText=document.createElement('span');currentText.className='msg__text'; const old=m.querySelector('.cursor');if(old)old.remove(); m.appendChild(currentText); m.appendChild(el('span','cursor'));} currentText.textContent+=t; scrollDown(); }
function appendReasoning(t) { const m=ensureMsg(); if(!currentReasoning){const w=el('div','reasoning'); const b=el('button','reasoning__toggle'); b.innerHTML='<span class="reasoning__chevron">▶</span> '+__('thinking'); const body=el('div','reasoning__body'); body.style.display='none'; b.onclick=()=>{body.style.display=body.style.display==='none'?'':'none';b.querySelector('.reasoning__chevron').className='reasoning__chevron'+(body.style.display!=='none'?' reasoning__chevron--open':'');}; w.appendChild(b);w.appendChild(body); if(currentText) m.insertBefore(w,currentText); else m.appendChild(w); currentReasoning=body;} currentReasoning.textContent+=t; scrollDown(); }
function finalizeMsg() { if(currentMsg){const c=currentMsg.querySelector('.cursor');if(c)c.remove();} currentMsg=null;currentText=null;currentReasoning=null; }
// ── tool cards (v1 pattern) ──
function toolIcon(kind) {
if(kind==='success')return '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>';
if(kind==='danger')return '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
return '<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10" stroke-dasharray="60" stroke-dashoffset="20"/></svg>';
}
function toolArgsSummary(args) {
const raw=String(args||'').trim();
if(!raw)return '';
try{
const parsed=JSON.parse(raw);
if(parsed&&typeof parsed==='object'&&!Array.isArray(parsed)){
const key=['cmd','command','path','file','query','q','url','prompt','input'].find(k=>parsed[k]);
if(key)return compactText(parsed[key],90);
}
}catch{}
return compactText(raw,90);
}
function updateToolMeta(card, text) {
const meta=card.querySelector('.card-meta');
if(meta)meta.textContent=text;
}
function setToolStatus(card, tone, label) {
card.dataset.tone=tone;
const badge=card.querySelector('.card-badge');
if(badge)badge.textContent=label;
const ico=card.querySelector('.ico');
if(ico){ico.className='ico'+(tone==='accent'?' spin':'');ico.innerHTML=toolIcon(tone);}
}
function copyText(text) {
if(!text)return Promise.resolve(false);
if(navigator.clipboard?.writeText)return navigator.clipboard.writeText(text).then(()=>true).catch(()=>false);
return Promise.resolve(false);
}
function renderToolDispatch(tool) {
hideWelcome(); const card=el('div','card'); card.id='tool-'+tool.id; card.dataset.open='false'; card.dataset.tone='accent';
card.dataset.startedAt=String(Date.now());
const head=el('div','card-head');
const summary=toolArgsSummary(tool.args);
head.innerHTML='<span class="ico spin">'+toolIcon('accent')+'</span><div class="card-main"><div class="card-title"><span class="name">'+escHtml(tool.name)+'</span>'+(summary?'<span class="subject">'+escHtml(summary)+'</span>':'')+'</div><div class="card-meta">'+escHtml(__('tool_args'))+' '+fmtTok(String(tool.args||'').length)+'</div></div><span class="card-badge">'+escHtml(__('tool_running'))+'</span><div class="card-actions"><button type="button" class="card-action card-copy" title="'+escAttr(__('tool_copy'))+'" aria-label="'+escAttr(__('tool_copy'))+'"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button><span class="chev"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg></span></div>';
const body=el('div','card-body'); body.style.display='none';
head.onclick=e=>{if(e.target.closest('button'))return;const open=card.dataset.open==='true';card.dataset.open=open?'false':'true';body.style.display=open?'none':'';};
const copy=head.querySelector('.card-copy');
if(copy)copy.onclick=async e=>{e.stopPropagation();const ok=await copyText(body.textContent||tool.args||'');copy.title=ok?__('tool_copied'):__('tool_copy');setTimeout(()=>{copy.title=__('tool_copy');},1200);};
card.appendChild(head);card.appendChild(body);log.appendChild(card);toolCards[tool.id]=card;scrollDown();
}
function renderToolResult(tool) {
const card=toolCards[tool.id]; if(!card)return;
const elapsed=Math.max(0,Date.now()-Number(card.dataset.startedAt||Date.now()));
setToolStatus(card,tool.err?'danger':'success',tool.err?__('tool_failed'):__('tool_done'));
if(tool.err){card.appendChild(el('div','err-body',tool.err));updateToolMeta(card,fmtElapsed(elapsed));}
else {
const body=card.querySelector('.card-body');
const output=String(tool.output||'');
if(body)body.textContent=output?output.slice(0,2000)+(tool.truncated?'\n...[truncated]':''):__('tool_no_output');
updateToolMeta(card,fmtElapsed(elapsed)+' · '+lineCount(output)+' '+__('tool_lines'));
}
scrollDown();
}
function renderToolProgress(tool) {
const card=toolCards[tool.id]; if(!card)return;
const body=card.querySelector('.card-body'); if(!body)return;
body.style.display=''; card.dataset.open='true';
body.textContent+=tool.output||'';
if(body.textContent.length>4000) body.textContent=body.textContent.slice(-3000);
updateToolMeta(card,fmtElapsed(Date.now()-Number(card.dataset.startedAt||Date.now()))+' · '+lineCount(body.textContent)+' '+__('tool_lines'));
scrollDown();
}
// ── approval ──
function approvalSessionLabel(a) {
return __('session')+' '+approvalSessionRule(a);
}
function approvalPersistentLabel(a) {
return __('persist_tool')+' '+approvalPersistentRule(a);
}
function approvalSessionRule(a) {
if (a.tool === 'bash' && a.subject) return 'Bash('+String(a.subject).trim()+')';
if (['write_file','edit_file','multi_edit','move_file','notebook_edit','delete_range','delete_symbol'].includes(a.tool)) return 'Edit';
return a.tool;
}
function approvalPersistentRule(a) {
if (a.tool === 'bash' && a.subject) return 'Bash('+String(a.subject).trim()+')';
if (['write_file','edit_file','multi_edit','move_file','notebook_edit','delete_range','delete_symbol'].includes(a.tool)) return a.subject ? 'Edit('+String(a.subject).trim()+')' : 'Edit';
return a.tool;
}
function bashCommandPrefix(subject) {
const command = String(subject || '').trim();
if (!command || command.includes('`') || command.includes('$(') || /[;|&<>\n]/.test(command)) return '';
const fields = command.split(/\s+/).filter(Boolean);
if (fields.length < 2) return '';
if (dangerousBashCommand(command)) return '';
const base = fields[0].toLowerCase();
if (['npm','pnpm','yarn','bun'].includes(base) && fields[1] && fields[1].toLowerCase() === 'run') return fields.length >= 3 ? fields[0]+' '+fields[1]+' '+fields[2]+':*' : '';
return fields[0]+' '+fields[1]+':*';
}
function dangerousBashCommand(command) {
return /^rm\s+-[^\s]*[rf][^\s]*\b/.test(command)
|| /^git\s+push\b.*\s--force\b/.test(command)
|| /^git\s+push\b.*\s-f\b/.test(command)
|| /^git\s+reset\s+--hard\b/.test(command)
|| /^git\s+clean\s+-f\b/.test(command)
|| /^chmod\s+(?:-R\s+)?777\b/.test(command)
|| /^chown\b/.test(command)
|| /^sudo\b/.test(command)
|| /^mkfs\b/.test(command)
|| /^dd\s+if=/.test(command)
|| /^fdisk\b/.test(command);
}
function showApproval(a) {
const d=el('div','approval');
const prefix = a.tool === 'bash' ? bashCommandPrefix(a.subject) : '';
const hasPrefix = prefix !== '';
const prefixRule = hasPrefix ? 'Bash('+prefix+')' : '';
const actions = [
'<button class="approval__btn approval__btn--allow" data-allow="true" data-session="false"><span class="approval__key">Y</span> '+__('allow')+'</button>',
];
if (hasPrefix) {
actions.push('<button class="approval__btn approval__btn--allow" data-allow="true" data-session="true" data-scope="prefix"><span class="approval__key">A</span> '+escHtml(__('session')+' '+prefixRule)+'</button>');
actions.push('<button class="approval__btn approval__btn--allow" data-allow="true" data-session="true" data-persist="true" data-scope="prefix"><span class="approval__key">P</span> '+escHtml(__('persist_tool')+' '+prefixRule)+'</button>');
} else {
actions.push('<button class="approval__btn approval__btn--allow" data-allow="true" data-session="true"><span class="approval__key">A</span> '+escHtml(approvalSessionLabel(a))+'</button>');
actions.push('<button class="approval__btn approval__btn--allow" data-allow="true" data-session="true" data-persist="true"><span class="approval__key">P</span> '+escHtml(approvalPersistentLabel(a))+'</button>');
}
actions.push('<button class="approval__btn approval__btn--deny" data-allow="false"><span class="approval__key">N</span> '+__('deny')+'</button>');
d.innerHTML='<div class="approval__header"><svg class="approval__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg><span class="approval__title">'+__('approval_title')+'</span></div><div class="approval__subject">'+escHtml(a.tool)+(a.subject?' — '+escHtml(a.subject):'')+'</div><div class="approval__actions">'+actions.join('')+'</div>';
log.appendChild(d);scrollDown();
const resolve=payload=>{post('/approve',Object.assign({id:a.id},payload));d.remove();document.removeEventListener('keydown',onkey);};
d.querySelectorAll('.approval__btn').forEach(btn=>{btn.onclick=()=>resolve({allow:btn.dataset.allow==='true',session:btn.dataset.session==='true',persist:btn.dataset.persist==='true',scope:btn.dataset.scope||''});});
const onkey=e=>{const k=e.key.toLowerCase();if(k==='y'||k==='1'){resolve({allow:true,session:false});}else if(k==='a'||k==='2'){resolve({allow:true,session:true});}else if(hasPrefix&&k==='3'){resolve({allow:true,session:true,scope:'prefix'});}else if(k==='p'||(!hasPrefix&&k==='3')||(hasPrefix&&k==='4')){resolve({allow:true,session:true,persist:true,scope:hasPrefix?'prefix':''});}else if(k==='n'||k==='escape'||(!hasPrefix&&k==='4')||(hasPrefix&&k==='5')){resolve({allow:false,session:false});}};
document.addEventListener('keydown',onkey);
}
// ── ask ──
function showAsk(ask) {
const d=el('div','ask');
ask.questions.forEach(q=>{
const qDiv=el('div'); qDiv.appendChild(el('div','ask__prompt',q.prompt));
const opts=el('div','ask__options'); const selected=new Set();
q.options.forEach((o,i)=>{
const opt=el('button','ask__opt'); opt.innerHTML='<div><div class="ask__opt-label">'+escHtml(o.label)+'</div>'+(o.description?'<div class="ask__opt-desc">'+escHtml(o.description)+'</div>':'')+'</div>';
opt.onclick=()=>{if(q.multi){selected.has(i)?selected.delete(i):selected.add(i);opt.classList.toggle('ask__opt--selected',selected.has(i));}else{selected.clear();selected.add(i);opts.querySelectorAll('.ask__opt').forEach((o,j)=>o.classList.toggle('ask__opt--selected',j===i));}};
opts.appendChild(opt);
});
qDiv.appendChild(opts);
const submit=el('button','ask__submit',__('submit')); submit.disabled=true;
opts.addEventListener('click',()=>{submit.disabled=selected.size===0;});
submit.onclick=()=>{const answers=ask.questions.map(qq=>({questionId:qq.id,selected:Array.from(selected).map(i=>qq.options[i].label)}));post('/answer',{id:ask.id,answers});d.remove();};
qDiv.appendChild(submit); d.appendChild(qDiv);
});
log.appendChild(d);scrollDown();
}
// ── compaction ──
function showCompaction(c) {
const d=el('div','compaction');
if(c.summary){const head=el('div','compaction__head');head.innerHTML='<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" y1="10" x2="21" y2="3"/><line x1="3" y1="21" x2="10" y2="14"/></svg>';head.appendChild(el('span','compaction__title',__('compacted')));head.appendChild(el('span','',c.messages+' '+__('messages')));const body=el('div','compaction__body',c.summary);head.onclick=()=>body.classList.toggle('compaction__body--open');d.appendChild(head);d.appendChild(body);}
else d.textContent=__('compacting');
log.appendChild(d);scrollDown();
}
// ── rewind picker ──
let rewindCheckpoints = [];
let rewindStage = 0, rewindSelected = 0, rewindScope = 0;
const SCOPES = [
{key:'b',label:__('scope_both'),scope:'both'},
{key:'c',label:__('scope_conversation'),scope:'conversation'},
{key:'d',label:__('scope_code'),scope:'code'},
{key:'f',label:__('scope_fork'),scope:'fork'},
{key:'s',label:__('scope_sumfrom'),scope:'sumfrom'},
{key:'u',label:__('scope_sumupto'),scope:'sumupto'},
];
function openRewindPicker() {
fetch('/checkpoints').then(r=>r.json()).then(cps=>{
checkpointCount=Array.isArray(cps)?cps.length:0; updateActionAvailability();
if(!cps||cps.length===0){showNotice(__('no_checkpoints'),'warn');return;}
rewindCheckpoints=cps; rewindStage=0; rewindSelected=0; rewindScope=0;
renderRewindPicker();
}).catch(()=>{});
}
function refreshCheckpointAvailability(){
fetch('/checkpoints').then(r=>r.json()).then(cps=>{
checkpointCount=Array.isArray(cps)?cps.length:0;
updateActionAvailability();
}).catch(()=>{});
}
function renderRewindPicker() {
let overlay=$('#rewind-overlay'); if(overlay)overlay.remove();
overlay=el('div','rewind-overlay'); overlay.id='rewind-overlay';
const picker=el('div','rewind-picker');
if(rewindStage===0){
picker.innerHTML='<div class="rewind-picker__head"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg> '+__('rewind_title')+'</div>';
const list=el('div','rewind-picker__list');
rewindCheckpoints.forEach((cp,i)=>{
const item=el('div','rewind-picker__item'+(i===rewindSelected?' rewind-picker__item--active':''));
item.innerHTML='<span class="rewind-picker__turn">#'+cp.turn+'</span><span class="rewind-picker__prompt">'+escHtml((cp.prompt||'').slice(0,80))+'</span><span class="rewind-picker__files">'+cp.files+' '+__('files')+'</span>';
item.onclick=()=>{rewindSelected=i;renderRewindPicker();};
list.appendChild(item);
});
picker.appendChild(list);
picker.appendChild(el('div','rewind-picker__foot','<span>'+__('nav_jk')+'</span><span>'+__('nav_enter_esc')+'</span>'));
} else {
const cp=rewindCheckpoints[rewindSelected];
picker.innerHTML='<div class="rewind-picker__head"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg> '+__('action_title').replace('#{turn}',cp.turn)+'</div>';
const scopes=el('div','rewind-picker__scopes');
SCOPES.forEach((s,i)=>{
const item=el('div','rewind-picker__scope'+(i===rewindScope?' rewind-picker__scope--active':''));
item.innerHTML='<span class="rewind-picker__scope-key">'+s.key+'</span>'+s.label;
item.onclick=()=>{rewindScope=i;applyRewind();};
scopes.appendChild(item);
});
picker.appendChild(scopes);
picker.appendChild(el('div','rewind-picker__foot','<span>'+__('nav_keys')+'</span><span>'+__('nav_apply_esc')+'</span>'));
}
overlay.appendChild(picker);
overlay.onclick=e=>{if(e.target===overlay)overlay.remove();};
document.body.appendChild(overlay);
}
function applyRewind() {
const cp=rewindCheckpoints[rewindSelected]; const sc=SCOPES[rewindScope];
document.getElementById('rewind-overlay')?.remove();
if(sc.scope==='fork'){post('/fork',{turn:cp.turn,name:''});}
else if(sc.scope==='sumfrom'){post('/summarize',{turn:cp.turn,mode:'from'});}
else if(sc.scope==='sumupto'){post('/summarize',{turn:cp.turn,mode:'upto'});}
else{post('/rewind',{turn:cp.turn,scope:sc.scope});}
}
document.addEventListener('keydown',e=>{
const overlay=$('#rewind-overlay'); if(!overlay)return;
if(e.key==='Escape'){e.preventDefault();if(rewindStage===0)overlay.remove();else{rewindStage=0;renderRewindPicker();}return;}
if(rewindStage===0){
if(e.key==='j'||e.key==='ArrowDown'){e.preventDefault();rewindSelected=Math.min(rewindSelected+1,rewindCheckpoints.length-1);renderRewindPicker();}
if(e.key==='k'||e.key==='ArrowUp'){e.preventDefault();rewindSelected=Math.max(rewindSelected-1,0);renderRewindPicker();}
if(e.key==='Enter'){e.preventDefault();rewindStage=1;rewindScope=0;renderRewindPicker();}
} else {
if(e.key==='j'||e.key==='ArrowDown'){e.preventDefault();rewindScope=Math.min(rewindScope+1,SCOPES.length-1);renderRewindPicker();}
if(e.key==='k'||e.key==='ArrowUp'){e.preventDefault();rewindScope=Math.max(rewindScope-1,0);renderRewindPicker();}
if(e.key==='Enter'){e.preventDefault();applyRewind();}
const idx=SCOPES.findIndex(s=>s.key===e.key); if(idx>=0){e.preventDefault();rewindScope=idx;applyRewind();}
}
});
// ── slash menu ──
let slashOpen=false, slashIndex=0, slashFiltered=[];
function updateSlashMenu() {
const v=input.value;
if(!v.startsWith('/')||v.includes(' ')){closeSlashMenu();return;}
const q=v.slice(1).toLowerCase();
slashFiltered=SLASH_CMDS.filter(c=>{
const hay=[c.cmd,c.sig,c.desc,slashGroupLabel(c.group)].join(' ').toLowerCase();
return hay.includes(q);
}).sort((a,b)=>{
const ap=a.cmd.startsWith(q)?0:1, bp=b.cmd.startsWith(q)?0:1;
return ap-bp || SLASH_CMDS.indexOf(a)-SLASH_CMDS.indexOf(b);
});
if(slashFiltered.length===0){closeSlashMenu();return;}
slashOpen=true; slashIndex=0;
renderSlashMenu();
}
function renderSlashMenu() {
let menu=$('#slash-menu'); if(!menu){menu=el('div','slash-menu');menu.id='slash-menu';slashAnchor.appendChild(menu);}
const query=input.value.slice(1);
menu.innerHTML='<div class="slash-menu__head"><span>'+escHtml(__('command_palette'))+'</span><span class="slash-menu__query">/'+escHtml(query)+'</span></div><div class="slash-menu__list" id="slash-menu-list" role="listbox"></div><div class="slash-menu__foot"><span>'+escHtml(__('command_nav'))+'</span><span>'+slashFiltered.length+'</span></div>';
const list=$('#slash-menu-list');
let lastGroup='';
slashFiltered.forEach((c,i)=>{
if(c.group!==lastGroup){
lastGroup=c.group;
const group=el('div','slash-menu__group',slashGroupLabel(c.group));
list.appendChild(group);
}
const item=el('button','slash-menu__item'+(i===slashIndex?' slash-menu__item--active':''));
item.type='button';
item.setAttribute('role','option');
item.setAttribute('aria-selected',i===slashIndex?'true':'false');
item.innerHTML='<span class="slash-menu__name">/'+escHtml(c.cmd)+'</span><span class="slash-menu__desc">'+escHtml(c.desc)+'</span><span class="slash-menu__pill'+(c.danger?' slash-menu__pill--danger':'')+'">'+escHtml(c.danger?__('danger'):slashGroupLabel(c.group))+'</span><span class="slash-menu__sig">'+escHtml(c.sig)+'</span>';
item.onmouseenter=()=>{slashIndex=i;renderSlashMenu();};
item.onclick=()=>{input.value='/'+c.cmd+' ';closeSlashMenu();input.focus();};
list.appendChild(item);
});
const active=menu.querySelector('.slash-menu__item--active');
if(active)active.scrollIntoView({block:'nearest'});
}
function closeSlashMenu(){slashOpen=false;const m=$('#slash-menu');if(m)m.remove();}
function acceptSlash(){if(!slashOpen)return;const c=slashFiltered[slashIndex];if(c){input.value='/'+c.cmd+' ';}closeSlashMenu();input.focus();}
// ── SSE ──
const es=new EventSource('/events');
es.onopen=()=>{setConnState('connected');fetchStatus();fetchTodos();};
es.onmessage=ev=>{setConnState('connected');
const e=JSON.parse(ev.data);
if(e.kind!=='retrying')clearRetrying();
switch(e.kind){
case 'turn_started': setRunning(true); finalizeMsg(); toolCards={}; todosDismissed=false; break;
case 'reasoning': appendReasoning(e.reasoning||e.text||''); break;
case 'text': currentReasoning=null; appendText(e.text||''); break;
case 'message': finalizeMsg(); break;
case 'tool_dispatch': if(e.tool)renderToolDispatch(e.tool); break;
case 'tool_result': if(e.tool){renderToolResult(e.tool);if(e.tool.name==='todo_write'&&!e.tool.parentId&&!e.tool.err){try{const ts=parseTodos(e.tool.args);if(ts.length){todosState=ts;renderTodoPanel();}}catch{}}} break;
case 'tool_progress': if(e.tool)renderToolProgress(e.tool); break;
case 'usage':
if(e.usage){turnTokens=e.usage.completionTokens||0;
cumulativeTokens+=e.usage.totalTokens||0;
const usageCost=e.usage.cost??e.usage.costUsd;
if(typeof usageCost==='number')cumulativeCost+=usageCost;
cumulativeCacheHit+=e.usage.cacheHitTokens||0;
cumulativeCacheMiss+=e.usage.cacheMissTokens||0;
const strip=el('div','metric-strip');
[{l:__('total'),v:fmtTok(e.usage.totalTokens),c:''},{l:__('in'),v:fmtTok(e.usage.promptTokens),c:'acc'},{l:__('out'),v:fmtTok(e.usage.completionTokens),c:'ok'}].forEach(it=>{
const sp=el('span','item');sp.innerHTML=it.l+' <span class="v '+it.c+'">'+it.v+'</span>';strip.appendChild(sp);
});
if(e.usage.cacheHitTokens){const sp=el('span','item');sp.innerHTML=__('cache')+' <span class="v acc">'+Math.round(e.usage.cacheHitTokens/Math.max(1,e.usage.cacheHitTokens+e.usage.cacheMissTokens)*100)+'%</span>';strip.appendChild(sp);}
if(typeof usageCost==='number'&&usageCost>0){const sp=el('span','item');sp.innerHTML=__('cost')+' <span class="v">'+fmtMoney(usageCost,e.usage.currency)+'</span>';strip.appendChild(sp);}
log.appendChild(strip);scrollDown();
} break;
case 'notice': log.appendChild(el('div','notice'+(e.level==='warn'?' notice--warn':''),(e.level==='warn'?'! ':'')+(e.text||'')));scrollDown();break;
case 'phase': finalizeMsg(); log.appendChild(el('div','phase',e.text||''));scrollDown();break;
case 'approval_request': if(e.approval)showApproval(e.approval); break;
case 'ask_request': if(e.ask)showAsk(e.ask); break;
case 'compaction_started': showCompaction({trigger:e.compaction?.trigger}); break;
case 'compaction_done': showCompaction(e.compaction||{}); break;
case 'retrying': setRetrying(e.retryAttempt,e.retryMax); break;
case 'turn_done': finalizeMsg(); setRunning(false); if(e.err){log.appendChild(el('div','msg--error','✗ '+e.err));scrollDown();} fetchStatus(); fetchTodos(); refreshCheckpointAvailability(); break;
}
};
es.onerror=()=>{
if(es.readyState===EventSource.CONNECTING){setConnState('reconnecting');}
else{setConnState('disconnected');}
};
// ── status polling ──
function fetchStatus(){
fetch('/status').then(r=>r.json()).then(s=>{
if(s.label)statusModel.textContent=s.label;
if(welcomeModel)welcomeModel.textContent=s.label||'-';
if(welcomeCwd)welcomeCwd.textContent=(s.cwd||'-').replace(/^.*\//,'');
// sync mode UI without triggering POST (server is source of truth)
planMode=!!s.plan;
toolApprovalMode=s.toolApprovalMode || ((s.autoApproveTools??s.bypass)?'yolo':'ask');
bypassMode=toolApprovalMode==='yolo';
if(!bypassMode&&toolApprovalMode==='auto')yoloRestoreMode='auto';
updateModeButtons();
if(s.window){const pct=Math.min(100,Math.round(s.used/s.window*100));ctxFill.style.width=pct+'%';ctxFill.style.background=pct>85?'var(--warning)':pct>95?'var(--danger)':'var(--accent)';ctxUsed.textContent=fmtTok(s.used)+' tok';ctxWindow.textContent=fmtTok(s.window)+' tok';}
// goal state
goalText=(s.goal||'').trim();
goalActive=goalText!==''&&(s.goalStatus||'')==='running';
updateGoalUI();
// sidebar metrics
const cacheTotal=(s.cacheHit||0)+(s.cacheMiss||0);
$('#sm-cache').textContent=cacheTotal>0?Math.round((s.cacheHit||0)/cacheTotal*100)+'%':'—';
const lastCost=s.lastUsage?.cost??s.lastUsage?.costUsd??s.lastUsage?.totalCost;
if(typeof lastCost==='number')$('#sm-cost').textContent=fmtMoney(lastCost,s.lastUsage?.currency);
if(s.balance){$('#sm-balance').textContent=s.balance.display||'--';}
if(s.balance)balanceInfo.textContent='💰 '+(s.balance.display||'--');
}).catch(()=>{});
}
setInterval(fetchStatus,30000);
// ── history ──
function renderHistoryMessages(ms){
if(!ms||ms.length===0){hasVisibleHistory=false;showWelcome();updateActionAvailability();return false;}
const visible = ms.some(m => {
if(m.role==='user')return !!m.content;
if(m.role==='assistant')return !!(m.content||m.reasoning||(m.toolCalls&&m.toolCalls.length));
if(m.role==='tool')return !!(m.content||m.toolCallId||m.toolName);
return false;
});
hasVisibleHistory=visible;
if(!visible){showWelcome();updateActionAvailability();return false;}
hideWelcome(); toolCards={};
const resultById=new Map();
ms.forEach(m=>{if(m.role==='tool'&&m.toolCallId&&!resultById.has(m.toolCallId))resultById.set(m.toolCallId,m);});
const consumed=new Set();
let seq=0;
ms.forEach(m=>{
if(m.role==='system')return;
if(m.role==='user'){if(m.content)addUserMsg(m.content);return;}
if(m.role==='assistant'){
if(m.reasoning)appendReasoning(m.reasoning);
if(m.content)appendText(m.content);
if(m.content||m.reasoning)finalizeMsg();
(m.toolCalls||[]).forEach(tc=>{
const id=tc.id||'hist-tool-'+(seq++);
renderToolDispatch({id,name:tc.name,args:tc.arguments||'',readOnly:false});
const result=resultById.get(tc.id);
if(tc.id)consumed.add(tc.id);
if(result)renderToolResult({id,name:tc.name,output:result.content||''});
});
return;
}
if(m.role==='tool'){
if(m.toolCallId&&consumed.has(m.toolCallId))return;
const id=m.toolCallId||'hist-tool-'+(seq++);
renderToolDispatch({id,name:m.toolName||'tool',args:'',readOnly:false});
renderToolResult({id,name:m.toolName||'tool',output:m.content||''});
}
});
currentMsg=null;currentText=null;currentReasoning=null; scrollDown(); updateActionAvailability(); return true;
}
fetch('/history').then(r=>r.json()).then(msgs=>{renderHistoryMessages(msgs);fetchTodos();}).catch(()=>{});
// ── session list ──
let sessionCount=0;
function sessionTitle(s){
const name=String(s.name||'').replace(/^.*\//,'').replace(/\.jsonl$/,'');
return s.title||name.replace(/^\w+-/,'').replace(/T/,' ').replace(/[-_]/g,' ').slice(0,30);
}
function renderSessions(){
const list=$('#session-list'); if(!list)return;
const count=$('#session-count');
const q=sessionFilter.trim().toLowerCase();
const filtered=sessionsCache.filter(s=>{
if(!q)return true;
return [sessionTitle(s),s.name,s.path,String(s.turns||'')].some(v=>String(v||'').toLowerCase().includes(q));
});
sessionCount=sessionsCache.length;
if(count)count.textContent=sessionCount?String(sessionCount):'';
if(sessionsCache.length===0){list.innerHTML='<div style="padding:10px;color:var(--muted-2);font-size:12px">'+__('no_sessions')+'</div>';return;}
if(filtered.length===0){list.innerHTML='<div style="padding:10px;color:var(--muted-2);font-size:12px">'+__('no_sessions')+'</div>';return;}
list.innerHTML='';
filtered.forEach(s=>{
const item=el('div','session-item'+(s.current?' session-item--active':''));
const title=sessionTitle(s);
const meta=s.turns?s.turns+' turns':'';
item.innerHTML='<svg class="session-item__icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg><div class="session-item__body"><div class="session-item__title">'+escHtml(title)+'</div><div class="session-item__meta">'+escHtml(meta)+'</div></div><button type="button" class="session-del" data-name="'+escAttr(s.name)+'" title="'+escAttr(__('delete_session'))+'">&times;</button>';
item.onclick=ev=>{if(ev.target.closest('.session-del')||running||s.current)return;post('/resume',{path:s.path}).then(()=>{log.innerHTML='';log.appendChild(welcome);showWelcome();hasVisibleHistory=false;checkpointCount=0;todosDismissed=false;resetCumulativeStats();loadSessions();updateActionAvailability();setTimeout(()=>fetch('/history').then(r=>r.json()).then(msgs=>{renderHistoryMessages(msgs);fetchTodos();refreshCheckpointAvailability();}),300);});};
list.appendChild(item);
});
}
function loadSessions(){
fetch('/sessions').then(r=>r.json()).then(ss=>{
sessionsCache=Array.isArray(ss)?ss:[];
renderSessions();
}).catch(()=>{const list=$('#session-list');if(list)list.innerHTML='<div style="padding:10px;color:var(--muted-2);font-size:12px">'+__('error_loading')+'</div>';});
}
loadSessions();
// ── branches panel ──
function branchValue(b, ...keys) {
for(const k of keys){ if(b && b[k] != null && b[k] !== '') return b[k]; }
return '';
}
function branchTitle(b) {
return branchValue(b,'custom_title','CustomTitle','name','Name','topic_title','TopicTitle','id','ID') || 'branch';
}
function renderBranches(data) {
const tree=$('#branches-tree'), list=$('#branches-list');
if(!tree||!list)return;
tree.textContent=data?.tree||'branches: none';
const branches=Array.isArray(data?.branches)?data.branches:[];
if(branches.length===0){
list.innerHTML='<div class="empty-note">'+escHtml(__('no_branches'))+'</div>';
return;
}
list.innerHTML='';
branches.forEach(b=>{
const id=String(branchValue(b,'id','ID'));
const preview=String(branchValue(b,'preview','Preview'));
const turns=branchValue(b,'turns','Turns');
const model=String(branchValue(b,'model','Model'));
const item=el('div','branch-item');
const meta=[turns?turns+' turns':'',model,preview].filter(Boolean).join(' · ');
item.innerHTML='<div><div class="branch-item__title">'+escHtml(branchTitle(b))+'</div><div class="branch-item__meta">'+escHtml(meta||id)+'</div></div><button class="branch-item__btn" data-branch-id="'+escAttr(id)+'">'+escHtml(__('switch'))+'</button>';
list.appendChild(item);
});
}
function openBranches() {
const modal=$('#branches-modal');
if(!modal)return;
$('#branches-tree').textContent=__('loading');
$('#branches-list').innerHTML='';
modal.style.display='flex';
fetch('/branches').then(r=>r.json()).then(renderBranches).catch(()=>{
$('#branches-tree').textContent=__('error_loading');
});
}
function closeBranches(){const m=$('#branches-modal');if(m)m.style.display='none';}
// ── models panel ──
function renderModelsPanel(data) {
const list=$('#models-list');
if(!list)return;
const models=Array.isArray(data?.models)?data.models:[];
if(models.length===0){list.innerHTML='<div class="empty-note">'+escHtml(__('error_loading'))+'</div>';return;}
list.innerHTML='';
models.forEach(m=>{
const active=!!m.active;
const item=el('div','model-item'+(active?' model-item--active':''));
item.innerHTML='<div><div class="model-item__title">'+escHtml(m.ref||'')+'</div><div class="model-item__meta">'+escHtml([m.kind,m.default?'default':''].filter(Boolean).join(' · '))+'</div></div>';
if(active){
item.innerHTML+='<span class="model-item__status model-item__status--active">'+escHtml(__('active'))+'</span>';
}else{
item.innerHTML+='<button class="branch-item__btn" data-model-ref="'+escAttr(m.ref||'')+'">'+escHtml(__('use_model'))+'</button>';
}
list.appendChild(item);
});
}
function openModels() {
const modal=$('#models-modal');
if(!modal)return;
$('#models-list').innerHTML='<div class="empty-note">'+escHtml(__('loading'))+'</div>';
modal.style.display='flex';
fetch('/models').then(r=>r.json()).then(renderModelsPanel).catch(()=>{
$('#models-list').innerHTML='<div class="empty-note">'+escHtml(__('error_loading'))+'</div>';
});
}
function closeModels(){const m=$('#models-modal');if(m)m.style.display='none';}
// ── delete confirmation ──
function openDeleteSession(name) {
if(!name)return;
pendingDeleteSession=name;
const title=$('#delete-session-title');
if(title)title.textContent=name;
const modal=$('#delete-modal');
if(modal)modal.style.display='flex';
}
function closeDeleteSession() {
pendingDeleteSession=null;
const modal=$('#delete-modal');
if(modal)modal.style.display='none';
}
// ── input handling ──
async function syncModeBeforeSubmit(){
await post('/plan',{on:planMode});
await post('/tool-approval-mode',{mode:bypassMode?'yolo':toolApprovalMode});
// If goalMode is on, we'll send as /goal command — no separate POST needed
}
async function send(){
const v=input.value.trim(); if(!v)return;
await syncModeBeforeSubmit();
let submitInput=v;
if(goalMode && !v.startsWith('/goal')){
// Send as /goal command for goal-draft mode
submitInput='/goal '+v;
goalMode=false;
updateGoalUI();
} else if(goalMode){
// User typed /goal manually, exit goal-draft mode
goalMode=false;
updateGoalUI();
}
addUserMsg(v);
post('/submit',{input:submitInput}).then(r=>{if(r.ok&&r.status===204){fetchStatus();loadSessions();}});
input.value='';input.style.height='';closeSlashMenu();
}
input.addEventListener('input',()=>{input.style.height='auto';input.style.height=Math.min(input.scrollHeight,140)+'px';updateSlashMenu();});
input.addEventListener('keydown',e=>{
// slash menu nav
if(slashOpen){
if(e.key==='ArrowDown'){e.preventDefault();slashIndex=Math.min(slashIndex+1,slashFiltered.length-1);renderSlashMenu();return;}
if(e.key==='ArrowUp'){e.preventDefault();slashIndex=Math.max(slashIndex-1,0);renderSlashMenu();return;}
if(e.key==='Tab'||e.key==='Enter'){e.preventDefault();acceptSlash();return;}
if(e.key==='Escape'){e.preventDefault();closeSlashMenu();return;}
}
// main keys
if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send();return;}
if(e.key==='Escape'){
if(goalMode&&!running){goalMode=false;updateGoalUI();input.value='';closeSlashMenu();return;}
if(running){post('/cancel');return;}
// double-Esc for rewind
if(input.value===''){if(escTimer){clearTimeout(escTimer);escTimer=null;openRewindPicker();}else{escTimer=setInterval(()=>escTimer=null,600);}return;}
}
});
// shift+tab mode cycle
document.addEventListener('keydown',e=>{
if(e.target===input&&e.key==='Tab'&&e.shiftKey){e.preventDefault();cycleMode();return;}
if(e.target===input&&(e.key==='y'||e.key==='Y')&&(e.ctrlKey||e.metaKey)&&!e.altKey&&!e.shiftKey){e.preventDefault();toggleYolo();return;}
if(e.key==='/'&&e.target!==input){e.preventDefault();input.focus();return;}
});
// mode helpers — Shift+Tab toggles Plan; Ctrl+Y toggles YOLO separately.
function updateModeButtons(){
const auto=!bypassMode&&toolApprovalMode==='auto';
$('#btn-auto').classList.toggle('toolbar__btn--ok',auto);
$('#btn-plan').classList.toggle('toolbar__btn--active',planMode);
$('#btn-bypass').classList.toggle('toolbar__btn--danger',bypassMode);
$('#btn-goal').classList.toggle('toolbar__btn--goal',goalMode&&!goalActive);
$('#btn-goal').classList.toggle('toolbar__btn--goal-active',goalActive);
}
function updateGoalUI(){
updateModeButtons();
const bar=$('#goal-active-bar');
if(goalActive&&goalText){
bar.style.display='';
$('#goal-chip-text').textContent=goalText;
} else {
bar.style.display='none';
}
input.placeholder=goalMode?__('goal_placeholder'):__('placeholder');
}
async function setToolApprovalMode(mode){toolApprovalMode=mode;bypassMode=mode==='yolo';updateModeButtons();await post('/tool-approval-mode',{mode});}
async function setPlan(on){planMode=on;updateModeButtons();await post('/plan',{on});}
async function cycleMode(){if(goalMode){goalMode=false;updateGoalUI();return;}await setPlan(!planMode);setTimeout(fetchStatus,200);}
async function toggleYolo(){if(bypassMode){const restore=yoloRestoreMode==='auto'?'auto':'ask';await setToolApprovalMode(restore);}else{yoloRestoreMode=toolApprovalMode==='auto'?'auto':'ask';await setToolApprovalMode('yolo');}setTimeout(fetchStatus,200);}
async function setMode(m){if(m==='auto')await setToolApprovalMode('auto');else if(m==='plan')await setPlan(!planMode);else if(m==='yolo')await toggleYolo();else if(m==='goal')toggleGoalMode();}
function toggleGoalMode(){
if(goalActive){
// Clear active goal
post('/goal',{goal:''}).then(()=>{goalActive=false;goalText='';updateGoalUI();fetchStatus();});
return;
}
if(goalMode){
goalMode=false;
updateGoalUI();
} else {
goalMode=true;
updateGoalUI();
input.focus();
}
}
// ── todo panel ──
let todosState=[],todosDismissed=false;
function parseTodos(args){
try{const a=JSON.parse(args);return Array.isArray(a.todos)?a.todos:[];}
catch{return[];}
}
function todoStatusLabel(s){
switch(String(s||'').trim()){
case'completed':return '✓';
case'in_progress':return '▶';
default:return '○';
}
}
function hasIncompleteTodos(ts){return ts.some(t=>String(t.status||'').trim()!=='completed');}
function renderTodoPanel(){
const panel=$('#todo-panel'),list=$('#todos-list'),badge=$('#todos-badge');
const summary=$('#todos-summary'),dismiss=$('#todos-dismiss'),title=$('#todos-title');
if(!panel||todosDismissed){panel&&(panel.classList.remove('todos--visible'));return;}
if(!todosState.length){panel.classList.remove('todos--visible');return;}
const done=todosState.filter(t=>String(t.status||'').trim()==='completed').length;
const total=todosState.length;
const current=todosState.find(t=>String(t.status||'').trim()==='in_progress');
const allDone=done===total;
badge.textContent=done+'/'+total;
summary.textContent=(current?.activeForm||current?.content||todosState[todosState.length-1]?.content||'').slice(0,60);
dismiss.style.display=allDone?'':'none';
if(allDone)panel.classList.add('todos--collapsed');
list.innerHTML='';
todosState.forEach((t,i)=>{
const st=String(t.status||'').trim();
const li=el('li','todos__item todos__item--'+st+(t.level?' todos__item--sub':''));
li.innerHTML='<span class="todos__status todos__status--'+st+'">'+todoStatusLabel(st)+'</span><span class="todos__text">'+escHtml((st==='in_progress'&&t.activeForm)?t.activeForm:t.content)+'</span>';
list.appendChild(li);
});
panel.classList.add('todos--visible');
}
function fetchTodos(){
fetch('/todos').then(r=>r.json()).then(ts=>{
if(Array.isArray(ts)){todosState=ts;renderTodoPanel();}
}).catch(()=>{});
}
$('#todos-head').onclick=function(e){
if(e.target.closest('.todos__dismiss')){todosDismissed=true;renderTodoPanel();return;}
const panel=$('#todo-panel');panel.classList.toggle('todos--collapsed');
};
// toolbar buttons
btnSend.onclick=()=>void send();
btnStop.onclick=()=>post('/cancel');
$('#btn-auto').onclick=()=>void setMode('auto');
$('#btn-plan').onclick=()=>void setPlan(!planMode);
$('#btn-bypass').onclick=()=>void toggleYolo();
$('#btn-goal').onclick=()=>toggleGoalMode();
$('#goal-chip').onclick=()=>toggleGoalMode();
$('#btn-new').onclick=()=>{if(running)return;post('/new').then(()=>{log.innerHTML='';log.appendChild(welcome);showWelcome();hasVisibleHistory=false;checkpointCount=0;todosState=[];todosDismissed=false;renderTodoPanel();resetCumulativeStats();loadSessions();updateActionAvailability();});};
$('#btn-compact').onclick=()=>{if(actionDisabled('btn-compact')){showNotice(__('no_conversation'),'warn');return;}post('/compact');};
$('#btn-rewind').onclick=()=>{if(actionDisabled('btn-rewind')){showNotice(__('no_checkpoints'),'warn');return;}openRewindPicker();};
$('#btn-tree').onclick=()=>openBranches();
$('#btn-models').onclick=()=>openModels();
$('#btn-stats').onclick=()=>openStats();
$('#branches-modal-close').onclick=()=>closeBranches();
$('#branches-modal').onclick=e=>{if(e.target===e.currentTarget)closeBranches();};
$('#branches-list').onclick=e=>{
const btn=e.target.closest('[data-branch-id]');
if(!btn||running)return;
const id=btn.dataset.branchId;
if(!id)return;
closeBranches();
post('/submit',{input:'/switch '+id}).then(()=>{setTimeout(()=>{log.innerHTML='';log.appendChild(welcome);showWelcome();loadSessions();fetch('/history').then(r=>r.json()).then(msgs=>{renderHistoryMessages(msgs);fetchTodos();refreshCheckpointAvailability();});},300);});
};
$('#models-modal-close').onclick=()=>closeModels();
$('#models-modal').onclick=e=>{if(e.target===e.currentTarget)closeModels();};
$('#models-list').onclick=e=>{
const btn=e.target.closest('[data-model-ref]');
if(!btn||running)return;
const ref=btn.dataset.modelRef;
if(!ref)return;
closeModels();
post('/submit',{input:'/model '+ref}).then(()=>setTimeout(fetchStatus,300));
};
$('#session-search').addEventListener('input',e=>{sessionFilter=e.target.value;renderSessions();});
// ── mobile sidebar toggle ──
const sidebar=$('.sidebar'), overlay=$('#sidebar-overlay'), menuBtn=$('#menu-btn');
function openSidebar(){sidebar.classList.add('sidebar--open');overlay.classList.add('sidebar-overlay--visible');menuBtn.style.opacity='0';}
function closeSidebar(){sidebar.classList.remove('sidebar--open');overlay.classList.remove('sidebar-overlay--visible');menuBtn.style.opacity='';}
menuBtn.onclick=()=>openSidebar();
overlay.onclick=()=>closeSidebar();
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&sidebar.classList.contains('sidebar--open'))closeSidebar();});
// ── cumulative stats ──
// Session-scoped counters accumulated while this page is open. They reset on
// session rotation (/new, /resume) so the stats card labeled "session cost"
// never quietly turns into an all-sessions running total.
let cumulativeTokens=0, cumulativeCost=0, cumulativeCacheHit=0, cumulativeCacheMiss=0;
function resetCumulativeStats(){cumulativeTokens=0;cumulativeCost=0;cumulativeCacheHit=0;cumulativeCacheMiss=0;}
function openStats(){
const modal=$('#stats-modal');
if(!modal)return;
// populate from latest status
fetch('/status').then(r=>r.json()).then(s=>{
$('#stats-model').textContent=s.label||'-';
$('#stats-sessions').textContent=sessionCount||'0';
const total=cumulativeTokens||s.used||0;
$('#stats-total-tokens').textContent=total>=1000?fmtTok(total):String(total);
const hit=cumulativeCacheHit||s.cacheHit||0, miss=cumulativeCacheMiss||s.cacheMiss||0;
const rate=hit+miss>0?Math.round(hit/(hit+miss)*100)+'%':'0%';
$('#stats-cache-rate').textContent=rate;
// No single-request fallback here: rendering lastUsage.cost (one turn)
// under the session-cost label displayed one turn's spend as the session's.
const cost=cumulativeCost;
$('#stats-total-cost').textContent=cost>0?fmtMoney(cost,s.lastUsage?.currency):'-';
$('#stats-balance').textContent=s.balance?.display||'-';
if(s.window){const pct=Math.min(100,Math.round(s.used/s.window*100));$('#stats-ctx-fill').style.width=pct+'%';$('#stats-ctx-used').textContent=fmtTok(s.used)+' tok';$('#stats-ctx-window').textContent=fmtTok(s.window)+' tok';}
}).catch(()=>{});
modal.style.display='flex';
}
$('#stats-modal-close').onclick=()=>{const m=$('#stats-modal');if(m)m.style.display='none';};
$('#stats-modal').onclick=e=>{if(e.target===e.currentTarget){const m=$('#stats-modal');if(m)m.style.display='none';}};
// ── session delete ──
document.addEventListener('click',e=>{
const del=e.target.closest('.session-del');
if(!del)return;
e.stopPropagation();
openDeleteSession(del.dataset.name);
});
$('#delete-modal-close').onclick=()=>closeDeleteSession();
$('#delete-cancel').onclick=()=>closeDeleteSession();
$('#delete-modal').onclick=e=>{if(e.target===e.currentTarget)closeDeleteSession();};
$('#delete-confirm').onclick=()=>{
const name=pendingDeleteSession;
if(!name)return;
closeDeleteSession();
post('/delete-session',{name}).then(()=>loadSessions());
};
// welcome examples
$$('.welcome__ex').forEach(btn=>{btn.onclick=()=>{input.value=btn.dataset.prompt;send();};});
// initial fetch
fetchStatus();
refreshCheckpointAvailability();
updateActionAvailability();
</script>
</body>
</html>