rdsh 0.7.0

Rust port/shim for dsh: fast launcher, token saver, zero behavior breakage via passthrough
<!DOCTYPE html>
<html lang="__RDSH_LOCALE__" data-rdsh-locale="__RDSH_LOCALE__">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title id="ui-page-title">rdsh dashboard</title>
<link rel="icon" type="image/png" href="/icon.png">
<link rel="apple-touch-icon" href="/icon.png">
<style>
:root { color-scheme: dark; --bg: #0d1117; --card: #161b22; --line: #30363d; --fg: #e6edf3; --mut: #8b949e; --acc: #58a6ff; --ok: #3fb950; --warn: #d29922; }
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--fg); font: 14px/1.6 system-ui, "Hiragino Sans", sans-serif; }
header { padding: 20px 24px; border-bottom: 1px solid var(--line); display: flex; gap: 12px; align-items: center; }
header .titles { display: flex; flex-direction: column; gap: 2px; }
header h1 { margin: 0; font-size: 20px; }
header span { color: var(--mut); font-size: 12px; margin-left: auto; white-space: nowrap; }
header .subtitle { margin: 0; color: var(--mut); font-size: 12px; }
#overview { margin: 16px 24px 0; }
#overview h2 { margin: 0 0 8px; font-size: 15px; }
#overview p { color: var(--mut); font-size: 12px; margin: 0 0 8px; }
#connect-state { color: var(--warn); }
main { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; padding: 20px 24px; max-width: 1100px; }
.card { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 16px; }
.card h2 { margin: 0 0 8px; font-size: 15px; }
.card p { color: var(--mut); font-size: 12px; margin: 0 0 10px; }
textarea, input[type=number] { width: 100%; background: #0d1117; color: var(--fg); border: 1px solid var(--line); border-radius: 6px; padding: 8px; font: 13px monospace; }
input[type=password] { width: min(100%, 520px); background: #0d1117; color: var(--fg); border: 1px solid var(--line); border-radius: 6px; padding: 8px; font: 13px monospace; }
textarea { min-height: 110px; resize: vertical; }
button { background: #238636; color: #fff; border: 0; border-radius: 6px; padding: 7px 14px; font-size: 13px; cursor: pointer; margin-top: 8px; }
button:hover { background: #2ea043; }
button.ghost { background: transparent; border: 1px solid var(--line); color: var(--fg); }
pre { background: #0d1117; border: 1px solid var(--line); border-radius: 6px; padding: 8px; overflow: auto; max-height: 220px; font-size: 12px; white-space: pre-wrap; }
.badge { display: inline-block; padding: 1px 8px; border-radius: 20px; font-size: 12px; }
.ok { background: rgba(63,185,80,.15); color: var(--ok); }
.row { display: flex; gap: 8px; align-items: center; }
.row label { color: var(--mut); font-size: 12px; white-space: nowrap; }
.row input { width: 90px; }
footer { padding: 0 24px 24px; color: var(--mut); font-size: 12px; }
</style>
</head>
<body>
<header><img src="/icon.png" width="40" height="40" alt="rdsh"><div class="titles"><h1 id="ui-title">Audit &amp; Performance Monitor for DeepSeek Harness</h1><p id="ui-subtitle" class="subtitle">Local-only monitoring dashboard served by rdsh serve — not the upstream dsh web chat interface.</p></div><span id="ver">rdsh __RDSH_VERSION__</span></header>
<section id="overview" class="card">
<h2 id="ui-overview-heading">What this dashboard is for</h2><p id="ui-overview-help">Audits and measures doctor diagnostics, recent sessions, token estimates, prune, skills and benchmarks locally. Cards load automatically once you connect.</p>
<p id="connect-state">Not connected: enter the token printed in the terminal.</p></section>
<section id="auth" class="card" style="margin:16px 24px 0" hidden>
<h2 id="ui-connect-heading">ダッシュボードに接続</h2><p id="ui-connect-help">端末に表示された保護済みトークンファイルの内容を入力してください。トークンはアドレスバーに保存されません。</p>
<input id="dashboard-token" type="password" autocomplete="off" spellcheck="false" aria-label="Dashboard token">
<button id="ui-connect" onclick="connectDashboard()">接続</button><p id="auth-error" role="alert"></p></section>
<main>
<section class="card"><h2 id="ui-doctor-heading">状態 (doctor)</h2><p id="ui-doctor-help">本家dsh・DSH_HOME・slim設定を確認</p>
<button class="ghost" id="ui-refresh-doctor" onclick="doctor()">更新</button><pre id="doctor">Loads automatically after you connect</pre></section>
<section class="card"><h2 id="ui-tokens-heading">トークン推定</h2><p id="ui-tokens-help">初期入力トークンの見積もり(約4文字=1トークン)</p>
<textarea id="ttext" placeholder="ここにテキストを貼り付け"></textarea>
<button id="ui-estimate" onclick="tokens()">推定する</button><pre id="tout">-</pre></section>
<section class="card"><h2 id="ui-prune-heading">prune(切り詰め)</h2><p id="ui-prune-help">head+tailを残して予算内に収める</p>
<textarea id="ptext" placeholder="長いツール出力を貼り付け"></textarea>
<div class="row"><label id="ui-budget">予算</label><input id="pmax" type="number" value="4000" min="100"></div>
<button id="ui-prune" onclick="prune()">prune実行</button> <button class="ghost" id="pcopy" onclick="copyPrune()">コピー</button><pre id="pout">-</pre></section>
<section class="card"><h2 id="ui-sessions-heading">セッション</h2><p id="ui-sessions-help">最新のセッション一覧(Nodeなし)</p>
<button class="ghost" id="ui-refresh-sessions" onclick="sessions()">更新</button><pre id="sess">Loads automatically after you connect</pre></section>
<section class="card"><h2 id="ui-skills-heading">スキル</h2><p id="ui-skills-help">導入済みスキル一覧</p>
<button class="ghost" id="ui-refresh-skills" onclick="skills()">更新</button><pre id="sk">Loads automatically after you connect</pre></section>
<section class="card"><h2 id="ui-bench-heading">ベンチ</h2><p id="ui-bench-help">rdsh vs dsh の起動速度を比較</p>
<button class="ghost" id="ui-bench" onclick="bench()">計測 (n=3)</button><pre id="bench">未計測</pre></section>
</main>
<footer id="ui-footer">rdsh は本家dshを壊さない安全なRustランチャーです。serveは127.0.0.1のみで待ち受けます。</footer>
<script>
const RDSH_UI_LOCALE = ["en", "zh-CN", "ja"].includes(document.documentElement.dataset.rdshLocale) ? document.documentElement.dataset.rdshLocale : "en";
const RDSH_UI_CATALOG = {};
for (const [key, ja, en, zh] of [
  ["tokens.result", "トークン: {tokens} / 文字数: {chars}", "tokens: {tokens} / chars: {chars}", "令牌:{tokens} / 字符:{chars}"],
  ["prune.result", "[処理前 {before} → 処理後 {after}]({chars}文字)", "[before {before} -> after {after}] ({chars} chars)", "[处理前 {before} → 处理后 {after}]({chars} 个字符)"],
  ["sessions.projects", "({count} プロジェクト)", "({count} projects)", "({count} 个项目)"],
  ["page.title", "rdsh ダッシュボード", "rdsh dashboard", "rdsh 仪表板"],
  ["token.label", "ダッシュボードのトークン", "Dashboard token", "仪表板令牌"],
  ["response.invalid", "応答の解析に失敗: サーバーから不正な応答が返されました。再試行してください。", "Could not parse the response: the server returned an invalid response. Retry.", "无法解析响应:服务器返回了无效响应。请重试。"],
  ["connection.failed", "接続できません: rdsh serve が起動しているか確認し、再試行してください。", "Could not connect: check that rdsh serve is running and retry.", "无法连接:请确认 rdsh serve 正在运行,然后重试。"],

  ["app.role", "DeepSeek Harness の監査・パフォーマンスモニター", "Audit & Performance Monitor for DeepSeek Harness", "DeepSeek Harness 审计与性能监控"],
  ["app.subtitle", "rdsh serve が提供するローカル専用の監視ダッシュボードです。本家 dsh web のチャット画面ではありません。", "Local-only monitoring dashboard served by rdsh serve — not the upstream dsh web chat interface.", "由 rdsh serve 提供的仅限本地的监控仪表板,并非上游 dsh web 的聊天界面。"],
  ["overview.heading", "このダッシュボードの役割", "What this dashboard is for", "此仪表板的用途"],
  ["overview.help", "システム診断 (doctor)・直近セッション・トークン推定・prune・スキル・ベンチマークをローカルで監査・計測します。接続すると各カードへ自動で読み込まれます。", "Audits and measures doctor diagnostics, recent sessions, token estimates, prune, skills and benchmarks locally. Cards load automatically once you connect.", "在本地审计和测量 doctor 诊断、近期会话、令牌估算、prune、技能与基准测试。连接后各卡片会自动加载。"],
  ["awaiting", "接続後に自動表示します", "Loads automatically after you connect", "连接后将自动显示"],
  ["connection.waiting", "未接続: 端末に表示されたトークンを入力してください。", "Not connected: enter the token printed in the terminal.", "未连接:请输入终端显示的令牌。"],
  ["connection.connected", "接続済み: {time} に更新", "Connected: updated {time}", "已连接:更新于 {time}"],

  ["connect.heading", "ダッシュボードに接続", "Connect to dashboard", "连接到仪表板"], ["connect.help", "端末に表示された保護済みトークンファイルの内容を入力してください。トークンはアドレスバーに保存されません。", "Enter the protected token handoff file contents printed in the terminal. The token is not stored in the address bar.", "请输入终端显示的受保护令牌交接文件内容。令牌不会保存在地址栏中。"], ["connect.button", "接続", "Connect", "连接"],
  ["doctor.heading", "状態 (doctor)", "Status (doctor)", "状态 (doctor)"], ["doctor.help", "本家dsh・DSH_HOME・slim設定を確認", "Check upstream dsh, DSH_HOME, and slim settings", "检查上游 dsh、DSH_HOME 和 slim 设置"], ["refresh", "更新", "Refresh", "刷新"],
  ["tokens.heading", "トークン推定", "Token estimate", "令牌估算"], ["tokens.help", "初期入力トークンの見積もり(約4文字=1トークン)", "Estimate initial input tokens (about 4 characters = 1 token)", "估算初始输入令牌(约 4 个字符 = 1 个令牌)"], ["tokens.placeholder", "ここにテキストを貼り付け", "Paste text here", "在此粘贴文本"], ["tokens.estimate", "推定する", "Estimate", "估算"],
  ["prune.heading", "prune(切り詰め)", "Prune", "裁剪"], ["prune.help", "head+tailを残して予算内に収める", "Keep head and tail within the budget", "保留开头和结尾并控制在预算内"], ["prune.placeholder", "長いツール出力を貼り付け", "Paste long tool output", "粘贴较长的工具输出"], ["budget", "予算", "Budget", "预算"], ["prune.run", "prune実行", "Run prune", "执行裁剪"], ["copy", "コピー", "Copy", "复制"],
  ["sessions.heading", "セッション", "Sessions", "会话"], ["sessions.help", "最新のセッション一覧(Nodeなし)", "Latest sessions (without Node)", "最新会话列表(无 Node)"], ["skills.heading", "スキル", "Skills", "技能"], ["skills.help", "導入済みスキル一覧", "Installed skills", "已安装的技能"], ["bench.heading", "ベンチ", "Benchmark", "基准测试"], ["bench.help", "rdsh vs dsh の起動速度を比較", "Compare rdsh and dsh startup speed", "比较 rdsh 与 dsh 的启动速度"], ["bench.measure", "計測 (n=3)", "Measure (n=3)", "测量 (n=3)"], ["bench.pending", "計測中…", "Measuring…", "正在测量…"], ["bench.none", "未計測", "Not measured", "未测量"], ["footer", "rdsh は本家dshを壊さない安全なRustランチャーです。serveは127.0.0.1のみで待ち受けます。", "rdsh is a safe Rust launcher that leaves upstream dsh intact. serve listens only on 127.0.0.1.", "rdsh 是不会破坏上游 dsh 的安全 Rust 启动器。serve 仅监听 127.0.0.1。"],
  ["auth.rejected", "トークンを確認できません。ファイルの内容を確認してください。", "Token rejected. Check the handoff file contents.", "令牌被拒绝。请检查交接文件内容。"], ["error.prefix", "エラー: {message}", "Error: {message}", "错误:{message}"], ["retry", "再試行", "Retry", "重试"], ["copy.done", "コピー済み", "Copied", "已复制"], ["copy.none", "コピー対象がありません。先にprune実行してください。", "There is nothing to copy. Run prune first.", "没有可复制的内容。请先执行裁剪。"],
  ["http.400", "不正なリクエスト (400): {path} への送信内容を確認してください。", "Bad request (400): check the data sent to {path}.", "请求无效 (400):请检查发送到 {path} 的数据。"], ["http.401", "認証エラー (401): ダッシュボードのトークンが必要です。", "Authentication error (401): dashboard token required.", "认证错误 (401):需要仪表板令牌。"], ["http.403", "アクセス拒否 (403): 信頼できないホスト/オリジンです。127.0.0.1 のダッシュボードURLから直接開いてください。", "Access denied (403): untrusted host/origin. Open directly from the 127.0.0.1 dashboard URL.", "访问被拒绝 (403):主机或来源不受信任。请直接从 127.0.0.1 仪表板 URL 打开。"], ["http.404", "見つかりません (404): {path} は存在しません。", "Not found (404): {path} does not exist.", "未找到 (404):{path} 不存在。"], ["http.413", "送信データが大きすぎます (413): テキストを短くして再試行してください。", "Request data is too large (413): shorten the text and retry.", "发送数据过大 (413):请缩短文本后重试。"], ["http.429", "混雑しています (429): しばらく待って再試行してください。", "Busy (429): wait and retry.", "繁忙 (429):请稍候重试。"], ["http.500", "サーバー内部エラー (500): 再試行してください。直らない場合は serve を再起動してください。", "Internal server error (500): retry. If it continues, restart serve.", "服务器内部错误 (500):请重试。若持续发生,请重启 serve。"], ["http.503", "サーバーが混み合っています (503): しばらく待って再試行してください。", "Server is busy (503): wait and retry.", "服务器繁忙 (503):请稍候重试。"], ["http.default", "リクエスト失敗 ({status}): {path}。再試行してください。", "Request failed ({status}): {path}. Retry.", "请求失败 ({status}):{path}。请重试。"],
]) { (RDSH_UI_CATALOG[key] ||= {}) .ja = ja; RDSH_UI_CATALOG[key].en = en; RDSH_UI_CATALOG[key]["zh-CN"] = zh; }
function t(key, values = {}) { return (RDSH_UI_CATALOG[key]?.[RDSH_UI_LOCALE] || key).replace(/\{(\w+)\}/g, (_, name) => String(values[name] ?? `{${name}}`)); }
function setConnectState(key, values) { document.getElementById("connect-state").textContent = t(key, values); }
function formatNumber(value) { return new Intl.NumberFormat(RDSH_UI_LOCALE).format(value); }
function formatSessionTime(value) {
  if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/.test(value)) return value;
  // The sessions API supplies UTC minutes; keep its timezone and precision.
  const date = new Date(value.replace(" ", "T") + ":00Z");
  return Number.isNaN(date.getTime()) ? value : new Intl.DateTimeFormat(RDSH_UI_LOCALE, {timeZone:"UTC",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",timeZoneName:"short"}).format(date);
}
for (const [id, key] of Object.entries({ "ui-title":"app.role", "ui-page-title":"page.title", "ui-subtitle":"app.subtitle", "ui-overview-heading":"overview.heading", "ui-overview-help":"overview.help", "connect-state":"connection.waiting", "doctor":"awaiting", "sess":"awaiting", "sk":"awaiting", "bench":"bench.none", "ui-connect-heading": "connect.heading", "ui-connect-help": "connect.help", "ui-connect": "connect.button", "ui-doctor-heading": "doctor.heading", "ui-doctor-help": "doctor.help", "ui-refresh-doctor": "refresh", "ui-tokens-heading": "tokens.heading", "ui-tokens-help": "tokens.help", "ui-estimate": "tokens.estimate", "ui-prune-heading": "prune.heading", "ui-prune-help": "prune.help", "ui-budget": "budget", "ui-prune": "prune.run", pcopy: "copy", "ui-sessions-heading": "sessions.heading", "ui-sessions-help": "sessions.help", "ui-refresh-sessions": "refresh", "ui-skills-heading": "skills.heading", "ui-skills-help": "skills.help", "ui-refresh-skills": "refresh", "ui-bench-heading": "bench.heading", "ui-bench-help": "bench.help", "ui-bench": "bench.measure", "ui-footer": "footer" })) document.getElementById(id).textContent = t(key);
document.getElementById("dashboard-token").setAttribute("aria-label", t("token.label"));
document.getElementById("ttext").placeholder = t("tokens.placeholder");
document.getElementById("ptext").placeholder = t("prune.placeholder");
let dashboardToken = new URLSearchParams(location.hash.slice(1)).get("key") || sessionStorage.getItem("rdsh_dashboard_token") || "";
if (dashboardToken) sessionStorage.setItem("rdsh_dashboard_token", dashboardToken);
history.replaceState(null, "", location.pathname);
function showAuth(message){
  dashboardToken = "";
  sessionStorage.removeItem("rdsh_dashboard_token");
  document.getElementById("auth").hidden = false;
  document.getElementById("auth-error").textContent = message || "";
  setConnectState("connection.waiting");
}
async function connectDashboard(candidateInput){
  const candidate = typeof candidateInput === "string" ? candidateInput : document.getElementById("dashboard-token").value.trim();
  const error = document.getElementById("auth-error");
  try {
    const response = await fetch("/api/doctor", {headers:{"X-RDSH-Token":candidate}});
    if (!response.ok) throw new Error(t("auth.rejected"));
    dashboardToken = candidate;
    sessionStorage.setItem("rdsh_dashboard_token", dashboardToken);
    document.getElementById("dashboard-token").value = "";
    document.getElementById("auth").hidden = true;
    // Render the role header and connect state first, then fill every card in
    // parallel so the first connected paint already shows diagnostics and the
    // latest sessions instead of placeholders.
    setConnectState("connection.connected", { time: new Date().toLocaleTimeString(RDSH_UI_LOCALE) });
    const [version] = await Promise.all([jget("/api/version"), doctor(), sessions(), skills()]);
    document.getElementById("ver").textContent = version.name + " " + version.version;
  } catch(e) { showAuth(e.message); error.textContent = e.message; }
}
function statusMessage(status, path){
  switch (status){
    case 400: return t("http.400", { path });
    case 401: showAuth(t("auth.rejected")); return t("http.401");
    case 403: return t("http.403");
    case 404: return t("http.404", { path });
    case 413: return t("http.413");
    case 429: return t("http.429");
    case 500: return t("http.500");
    case 503: return t("http.503");
    default: return t("http.default", { status, path });
  }
}
async function checkRes(r, path){
  if (r.ok){
    try { return await r.json(); }
    catch(e){ throw new Error(t("response.invalid")); }
  }
  let detail = "";
  try { const b = await r.json(); if (b && b.error) detail = "(" + b.error + ")"; } catch(e){}
  throw new Error(statusMessage(r.status, path) + detail);
}
async function jget(p){
  let r;
  try { r = await fetch(p, {headers:{"X-RDSH-Token":dashboardToken}}); }
  catch(e){ throw new Error(t("connection.failed")); }
  return checkRes(r, p);
}
async function jpost(p, b){
  let r;
  try { r = await fetch(p, {method:"POST", headers:{"Content-Type":"application/json","X-RDSH-Token":dashboardToken}, body: JSON.stringify(b)}); }
  catch(e){ throw new Error(t("connection.failed")); }
  return checkRes(r, p);
}
function fail(outId, message, retry){
  const out = document.getElementById(outId);
  out.textContent = t("error.prefix", { message });
  let btn = document.getElementById(outId + "-retry");
  if (!btn){
    btn = document.createElement("button");
    btn.id = outId + "-retry";
    btn.className = "ghost";
    btn.textContent = t("retry");
    out.after(btn);
  }
  btn.onclick = retry;
  btn.hidden = false;
}
function clearRetry(outId){
  const btn = document.getElementById(outId + "-retry");
  if (btn) btn.hidden = true;
}
if (dashboardToken) connectDashboard(dashboardToken); else showAuth("");
async function doctor(){ try { clearRetry("doctor"); const d = await jget("/api/doctor"); document.getElementById("doctor").textContent = JSON.stringify(d, null, 2); } catch(e){ fail("doctor", e.message, doctor); } }
async function tokens(){ try { clearRetry("tout"); const text = document.getElementById("ttext").value; const r = await jpost("/api/tokens", {text}); document.getElementById("tout").textContent = t("tokens.result", { tokens: formatNumber(r.tokens), chars: formatNumber(r.chars) }); } catch(e){ fail("tout", e.message, tokens); } }
let lastPruned = "";
async function prune(){
  try {
    clearRetry("pout");
    const text = document.getElementById("ptext").value;
    const m = +document.getElementById("pmax").value || 4000;
    const r = await jpost("/api/prune", {text, max_tokens: m});
    lastPruned = r.pruned;
    document.getElementById("pout").textContent = t("prune.result", {before:formatNumber(r.before),after:formatNumber(r.after),chars:formatNumber(r.pruned.length)}) + "\n" + r.pruned;
  } catch(e){ fail("pout", e.message, prune); }
}
async function copyPrune(){
  const btn = document.getElementById("pcopy");
  try {
    if (!lastPruned) throw new Error(t("copy.none"));
    await navigator.clipboard.writeText(lastPruned);
    const old = btn.textContent;
    btn.textContent = t("copy.done");
    setTimeout(() => { btn.textContent = old; }, 1500);
  } catch(e){ fail("pout", e.message, prune); }
}
async function bench(){ try { clearRetry("bench"); document.getElementById("bench").textContent = t("bench.pending"); const r = await jget("/api/bench?n=3"); document.getElementById("bench").textContent = JSON.stringify(r, null, 2); } catch(e){ fail("bench", e.message, bench); } }
async function sessions(){ try { clearRetry("sess"); const r = await jget("/api/sessions?limit=10"); document.getElementById("sess").textContent = r.sessions.map(s => formatSessionTime(s.mtime) + " " + s.project + "/" + s.id).join("\n") + "\n" + t("sessions.projects", {count:formatNumber(r.projects)}); } catch(e){ fail("sess", e.message, sessions); } }
async function skills(){ try { clearRetry("sk"); const r = await jget("/api/skills"); document.getElementById("sk").textContent = r.names.join("\n"); } catch(e){ fail("sk", e.message, skills); } }
</script>
</body>
</html>