1801 lines
106 KiB
HTML
1801 lines
106 KiB
HTML
<!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×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">×</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">×</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">›</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">×</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">×</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">×</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">×</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,'&').replace(/</g,'<').replace(/>/g,'>');}
|
||
function escAttr(s) { return escHtml(s).replace(/"/g,'"').replace(/'/g,''');}
|
||
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'))+'">×</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>
|