@wendongfly/myhi 1.3.191 → 1.3.193

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/chat.html CHANGED
@@ -12,14 +12,19 @@
12
12
  <script src="/lib/machine.js" data-session></script>
13
13
  <script src="/lib/prefs.js"></script>
14
14
  <script src="/sio-client/socket.io.min.js"></script>
15
+ <script src="/theme.js"></script>
15
16
  <script>
16
17
  (function(){try{
17
- var t=localStorage.getItem('myhi_theme')||'dark';
18
+ var t=localStorage.getItem('myhi_theme')||window.MYHI_DEFAULT_THEME||'dark';
18
19
  var f=localStorage.getItem('myhi_font')||'medium';
19
20
  var r=document.documentElement;
20
21
  if(t&&t!=='dark') r.setAttribute('data-theme',t);
21
22
  r.setAttribute('data-font',f);
22
- if(new URLSearchParams(location.search).get('embed')==='1') r.classList.add('embed');
23
+ var em=new URLSearchParams(location.search).get('embed');
24
+ if(em==='1') r.classList.add('embed');
25
+ // 首页电脑端工作台右侧嵌入:保留本页顶栏,左侧已有任务列表 → 藏掉分屏/切换/返回;
26
+ // 页面里的链接(如「首页 ⚙ 设置」)在整个窗口打开,别在右侧小框里再套一个首页
27
+ if(em==='ws'){ r.classList.add('embed-ws'); var bs=document.createElement('base'); bs.target='_top'; document.head.appendChild(bs); }
23
28
  }catch(e){}})();
24
29
  </script>
25
30
  <style>
@@ -55,6 +60,7 @@
55
60
  html[data-font="large"] { font-size: 19.5px; }
56
61
  /* 分屏内嵌模式(?embed=1):隐藏本页顶栏,让每个窗格更紧凑(外层 grid.html 提供窗格头) */
57
62
  html.embed #top-bar { display: none; }
63
+ html.embed-ws #grid-btn, html.embed-ws [onclick="openSwitchSheet()"], html.embed-ws [onclick="goBack()"] { display: none !important; }
58
64
  /* 分屏是 PC 大屏功能,手机端(触屏)隐藏入口 */
59
65
  @media (pointer: coarse) { #grid-btn { display: none !important; } }
60
66
 
@@ -223,7 +229,11 @@
223
229
  .msg-synthetic .syn-body { white-space: pre-wrap; font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace; font-size: 0.75rem; line-height: 1.5; color: var(--muted); background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 0.5rem 0.75rem; max-height: 320px; overflow-y: auto; word-break: break-word; }
224
230
 
225
231
  /* ── 输入栏 ─────────────────────────────── */
226
- #input-area { background: var(--surface); border-top: 1px solid var(--border); flex-shrink: 0; padding: 0.5rem 0.75rem 0.35rem; }
232
+ /* 输入区与对话同底色、不画分隔线:输入框自带圆角边框,浮在对话下方即可(原来一条贯穿的横线 + 浅色底带显得割裂) */
233
+ #input-area { background: var(--bg); flex-shrink: 0; padding: 0.5rem 0.75rem 0.35rem; }
234
+ #pending-bar { margin: 0 0 0.45rem; padding: 0.45rem 0.7rem; border-radius: 9px; background: rgba(210,153,34,0.14); border: 1px solid rgba(210,153,34,0.45); color: var(--text); font-size: 0.8rem; cursor: pointer; }
235
+ .msg-plan { border: 1px solid var(--purple); border-radius: 10px; padding: 0.55rem 0.8rem !important; background: rgba(124,58,237,0.06); }
236
+ .msg-plan .plan-head { font-size: 0.78rem; font-weight: 600; color: var(--purple); margin-bottom: 0.25rem; }
227
237
  #input-area.no-shortcuts { padding-bottom: max(0.5rem, env(safe-area-inset-bottom)); }
228
238
  #input-box { background: var(--bg); border: 1px solid var(--border); border-radius: 12px; transition: border-color 0.2s; overflow: hidden; position: relative; }
229
239
  #img-preview { display: none; align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--surface2); background: var(--surface); }
@@ -302,7 +312,7 @@
302
312
  #readonly-overlay { display: none; text-align: center; padding: 0.6rem; font-size: 0.8rem; color: var(--muted); background: var(--surface); }
303
313
 
304
314
  /* ── 快捷键栏 ───────────────────────────── */
305
- #shortcut-bar { background: var(--surface); border-top: 1px solid var(--surface2); padding: 0.35rem 0.5rem; padding-bottom: max(0.35rem, env(safe-area-inset-bottom)); flex-shrink: 0; overflow: hidden; }
315
+ #shortcut-bar { background: var(--bg); padding: 0.35rem 0.5rem; padding-bottom: max(0.35rem, env(safe-area-inset-bottom)); flex-shrink: 0; overflow: hidden; }
306
316
  #shortcut-bar > div { display: flex; align-items: center; gap: 0.35rem; overflow-x: auto; scrollbar-width: none; }
307
317
  #shortcut-bar > div::-webkit-scrollbar { display: none; }
308
318
  .sk { background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 0.3rem 0.55rem; font-size: 0.75rem; font-family: 'SF Mono', 'Consolas', monospace; cursor: pointer; white-space: nowrap; flex-shrink: 0; user-select: none; -webkit-user-select: none; touch-action: manipulation; }
@@ -504,6 +514,7 @@
504
514
  <span class="sb-item" id="work-status"><span class="sb-dot idle"></span> 空闲</span>
505
515
  <span id="control-badge" class="readonly" style="display:none">只读</span>
506
516
  <span id="viewer-count" class="sb-item"></span>
517
+ <span id="guide-chip" class="sb-item" style="display:none;cursor:pointer" onclick="showMemory('project')"></span>
507
518
  <span id="update-badge" style="display:none;color:var(--yellow);cursor:pointer" onclick="doUpdate()">⬆ 有更新</span>
508
519
  <span id="claude-update-badge" style="display:none;color:var(--blue);cursor:pointer" onclick="doClaudeUpdate()">⬆ claude</span>
509
520
  </div>
@@ -511,6 +522,7 @@
511
522
  <div id="chat-area"></div>
512
523
 
513
524
  <div id="input-area">
525
+ <div id="pending-bar" style="display:none" onclick="reopenPending()"></div>
514
526
  <div id="readonly-overlay" style="display:none"></div>
515
527
  <div id="input-box">
516
528
  <!-- 待发草稿提示:本轮干完(idle)且收件箱里还有本任务的草稿时浮出来 -->
@@ -519,6 +531,14 @@
519
531
  <span id="draft-tip-text" style="flex:1;font-size:0.74rem;color:var(--yellow);overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></span>
520
532
  <button class="img-remove" onclick="event.stopPropagation();hideDraftTip()" title="暂不处理">&times;</button>
521
533
  </div>
534
+ <!-- 建议生成项目说明:看出是长期项目(同目录多任务 / 聊了很多轮 / 说了「以后都…」)且还没有 CLAUDE.md 时浮出 -->
535
+ <div id="guide-tip" style="display:none;align-items:center;gap:0.5rem;padding:0.4rem 0.75rem;border-bottom:1px solid var(--surface2);background:rgba(46,160,67,0.10)">
536
+ <span style="font-size:0.95rem;flex-shrink:0">📘</span>
537
+ <span id="guide-tip-text" style="flex:1;min-width:0;font-size:0.74rem;color:var(--text2);line-height:1.45"></span>
538
+ <button onclick="guideTipGo()" style="flex-shrink:0;background:var(--green);color:#fff;border:none;border-radius:6px;padding:0.25rem 0.6rem;font-size:0.72rem;cursor:pointer">帮我生成</button>
539
+ <button onclick="guideTipOff()" style="flex-shrink:0;background:none;border:none;color:var(--muted);font-size:0.7rem;cursor:pointer;padding:0.2rem">不再提示</button>
540
+ <button class="img-remove" onclick="guideTipClose()" title="先不管">&times;</button>
541
+ </div>
522
542
  <div id="img-preview">
523
543
  <img id="img-preview-thumb" src="" alt="">
524
544
  <span class="img-info">已添加图片,输入文字后发送</span>
@@ -931,7 +951,7 @@
931
951
  <div id="memory-sheet" class="action-sheet">
932
952
  <div class="action-sheet-backdrop" onclick="closeMemorySheet()"></div>
933
953
  <div class="action-sheet-box" style="height:90vh;max-height:90vh;display:flex;flex-direction:column">
934
- <div class="action-sheet-title">Claude 记忆</div>
954
+ <div class="action-sheet-title">项目说明与记忆</div>
935
955
  <div id="memory-content" style="flex:1;overflow-y:auto;padding:0.3rem 0;font-size:0.78rem;color:var(--text2);scrollbar-width:thin">
936
956
  <div style="text-align:center;color:var(--muted);padding:2rem 0">加载中...</div>
937
957
  </div>
@@ -1005,7 +1025,47 @@
1005
1025
  </div>
1006
1026
  <button id="ask-sheet-submit-multi" class="action-sheet-cancel ask-submit-btn" style="display:none;background:var(--blue3);color:#fff;font-weight:600;margin-bottom:0.4rem" onclick="submitAskMulti()">提交回答</button>
1007
1027
  <button id="ask-sheet-submit-single" class="action-sheet-cancel" style="background:var(--purple);color:#fff;font-weight:600;margin-bottom:0.4rem" onclick="submitAskReply()">发送自定义回答</button>
1008
- <button class="action-sheet-cancel" onclick="closeAskSheet()">取消</button>
1028
+ <button id="ask-sheet-skip" class="action-sheet-cancel" style="display:none" onclick="skipAsk()">⏭ 跳过,让 AI 自己决定</button>
1029
+ <button id="ask-sheet-cancel" class="action-sheet-cancel" onclick="closeAskSheet()">取消</button>
1030
+ </div>
1031
+ </div>
1032
+ <!-- 方案审批:计划模式下 AI 用 ExitPlanMode 提交方案 -->
1033
+ <div id="plan-sheet" class="action-sheet">
1034
+ <div class="action-sheet-backdrop" onclick="closePlanSheet()"></div>
1035
+ <div class="action-sheet-box" style="max-height:88vh;display:flex;flex-direction:column">
1036
+ <div class="action-sheet-title">📋 AI 提交了方案,请审批</div>
1037
+ <div class="msg-assistant" style="flex:1;min-height:0;overflow-y:auto;padding:0.2rem 0.4rem;scrollbar-width:thin"><div class="content" id="plan-sheet-body"></div></div>
1038
+ <textarea id="plan-sheet-fb" class="slash-inp" rows="2" placeholder="需要改的话写在这里,再点「按意见修改」(可选)" style="margin:0.5rem 0 0.4rem;resize:vertical"></textarea>
1039
+ <button class="action-sheet-cancel" style="background:var(--green);color:#fff;font-weight:600;margin-bottom:0.4rem" onclick="approvePlan()">✅ 批准并执行</button>
1040
+ <button class="action-sheet-cancel" style="margin-bottom:0.4rem" onclick="revisePlan()">✏️ 按意见修改方案</button>
1041
+ <button class="action-sheet-cancel" onclick="closePlanSheet()">稍后再看</button>
1042
+ </div>
1043
+ </div>
1044
+ <!-- 退出计划:按方案执行 / 仅退出 -->
1045
+ <div id="planexit-sheet" class="action-sheet">
1046
+ <div class="action-sheet-backdrop" onclick="closePlanExit()"></div>
1047
+ <div class="action-sheet-box">
1048
+ <div class="action-sheet-title">退出计划模式</div>
1049
+ <button class="action-sheet-cancel" style="background:var(--green);color:#fff;font-weight:600;margin-bottom:0.4rem" onclick="exitPlan(true)">✅ 退出并按方案执行</button>
1050
+ <button class="action-sheet-cancel" style="margin-bottom:0.4rem" onclick="exitPlan(false)">仅退出计划模式</button>
1051
+ <button class="action-sheet-cancel" onclick="closePlanExit()">取消</button>
1052
+ </div>
1053
+ </div>
1054
+
1055
+ <!-- 清除:先说清楚会发生什么,免得误以为出了故障 -->
1056
+ <div id="clear-sheet" class="action-sheet">
1057
+ <div class="action-sheet-backdrop" onclick="closeClearSheet()"></div>
1058
+ <div class="action-sheet-box">
1059
+ <div class="action-sheet-title">清除对话,从头开始?</div>
1060
+ <div style="font-size:0.82rem;color:var(--muted);line-height:1.65;padding:0 0.4rem 0.7rem">
1061
+ · AI 会<b>忘掉本任务之前聊过的内容</b>,下次提问是全新对话<br>
1062
+ · 界面上的聊天记录一并清空<br>
1063
+ · <b>项目文件、已做的修改都不受影响</b><br>
1064
+ 适合换新话题、或 AI 被旧内容带偏时用。只是嫌对话太长,用「压缩」更合适——会保留要点。
1065
+ </div>
1066
+ <button class="action-sheet-cancel" style="background:var(--red);color:#fff;font-weight:600;margin-bottom:0.4rem" onclick="doClear(true)">清除并开始新对话</button>
1067
+ <button class="action-sheet-cancel" style="margin-bottom:0.4rem" onclick="closeClearSheet();doSlashCmd('/compact')">改用压缩(保留要点)</button>
1068
+ <button class="action-sheet-cancel" onclick="closeClearSheet()">取消</button>
1009
1069
  </div>
1010
1070
  </div>
1011
1071
 
@@ -1234,7 +1294,7 @@
1234
1294
  syncAppearanceUI();
1235
1295
  }
1236
1296
  function syncAppearanceUI() {
1237
- const t = localStorage.getItem('myhi_theme') || 'dark';
1297
+ const t = localStorage.getItem('myhi_theme') || window.MYHI_DEFAULT_THEME || 'dark';
1238
1298
  const f = localStorage.getItem('myhi_font') || 'medium';
1239
1299
  document.querySelectorAll('#seg-theme .seg').forEach(b => b.classList.toggle('active', b.dataset.theme === t));
1240
1300
  document.querySelectorAll('#seg-font .seg').forEach(b => b.classList.toggle('active', b.dataset.font === f));
@@ -2146,6 +2206,8 @@
2146
2206
  if (block.type === 'thinking' && block.thinking) thinkings.push(block.thinking);
2147
2207
  else if (block.type === 'text' && block.text) texts.push(block.text);
2148
2208
  else if (block.type === 'tool_use') {
2209
+ const planTxt = planTextOf(block.name, block.input);
2210
+ if (planTxt !== null) { addPlanCard(planTxt); continue; }
2149
2211
  // Task 在翻页(fragment)模式下退化为普通工具卡——面板逻辑挂在聊天区,
2150
2212
  // 塞进 fragment 会错位;整本回放仍用面板
2151
2213
  if (block.name === 'Task' && !_replayParent) {
@@ -2492,6 +2554,7 @@
2492
2554
  if (draftBtn) draftBtn.style.display = busy ? '' : 'none';
2493
2555
  // 用 try 包住:这两个绑定声明在文件后段,初始化早期调用会撞 TDZ
2494
2556
  try { if (!busy) _draftTipDismissed = false; renderDraftTip(); } catch {}
2557
+ try { if (!busy) checkGuide(); else renderGuideTip(); } catch {}
2495
2558
  }
2496
2559
  _postStatus(state);
2497
2560
  }
@@ -2651,6 +2714,7 @@
2651
2714
  currentSession = session;
2652
2715
  overlay.classList.add('hidden');
2653
2716
  document.title = session.title + ' — myhi';
2717
+ setTimeout(() => { try { checkGuide(); } catch {} }, 0);
2654
2718
  document.getElementById('session-title').textContent = session.title;
2655
2719
  if (session.cwd) document.getElementById('session-cwd').textContent = session.cwd;
2656
2720
  updateViewers(session.viewers);
@@ -2775,9 +2839,9 @@
2775
2839
  _textRenderedThisTurn = true;
2776
2840
  }
2777
2841
  } else if (block.type === 'tool_use') {
2778
- if (block.name === 'AskUserQuestion' && block.input) {
2779
- openAskSheet(block.input);
2780
- }
2842
+ if (block.name === 'AskUserQuestion' && block.input) legacyAsk(block.id, block.input);
2843
+ const planTxt = planTextOf(block.name, block.input);
2844
+ if (planTxt !== null) { addPlanCard(planTxt); continue; }
2781
2845
  if (block.name === 'Task') {
2782
2846
  subaCreatePanel(block.id, subagentToolInfo(block.input || {}));
2783
2847
  } else {
@@ -2851,9 +2915,8 @@
2851
2915
  break;
2852
2916
  case 'tool_use':
2853
2917
  // 独立工具调用消息
2854
- if (msg.name === 'AskUserQuestion' && msg.input) {
2855
- openAskSheet(msg.input);
2856
- }
2918
+ if (msg.name === 'AskUserQuestion' && msg.input) legacyAsk(msg.id, msg.input);
2919
+ if (planTextOf(msg.name, msg.input) !== null) { addPlanCard(planTextOf(msg.name, msg.input)); break; }
2857
2920
  if (msg.name === 'Task') {
2858
2921
  subaCreatePanel(msg.id, subagentToolInfo(msg.input || {}));
2859
2922
  } else {
@@ -3224,6 +3287,7 @@
3224
3287
 
3225
3288
  function sendCommand() {
3226
3289
  const text = cmdInput.value.trim();
3290
+ if (GUIDE_REMEMBER_RE.test(text)) _guideRemember = true; // 「以后都… / 记住…」:这一轮干完建议写进项目说明
3227
3291
  if (!text && !_pendingImage && !_pendingFile) return;
3228
3292
 
3229
3293
  // AI 还在跑这一轮(可能正开着子代理,整轮 result 未到,服务端 isBusy 会拒收):
@@ -3558,22 +3622,56 @@
3558
3622
 
3559
3623
  // ── 设置面板 ──────────────────────────────────
3560
3624
  // ── 会话内使用说明(精简版;完整版在首页 ❓ 帮助)──────────────
3625
+ // 快捷栏按钮说明:帮助面板和鼠标悬停提示共用这一份,加按钮时在这里补一句
3626
+ const SK_HELP = {
3627
+ draft: ['📥 草稿', 'AI 忙时先把想法存起来,干完再发;勾「自动接力」会在这一轮结束后自动发出。'],
3628
+ folder: ['📂 文件夹', '在这台电脑上用资源管理器打开本任务目录(后台/服务模式下不可用)。'],
3629
+ model: ['模型', '切换 AI 模型。复杂任务用强模型,简单活换快模型更省;被安全检测误拦时也可换个模型再试。'],
3630
+ plan: ['计划', '先让 AI 只调研、出方案、不改文件;你审批后再执行。再点一次「退出计划」可按方案执行或仅退出。'],
3631
+ compact: ['压缩', '对话太长时把前面内容压成摘要,AI 保留要点、继续干活,上下文水位下降。不会丢文件。'],
3632
+ resume: ['恢复', '翻看本任务以前的对话,选一段接着聊。'],
3633
+ clear: ['清除', '让 AI 忘掉之前聊过的内容、从头开始新对话,聊天记录也清空;项目文件不受影响。只嫌太长用「压缩」。'],
3634
+ secrets: ['🔑 密钥', '保存 token / 密码等,命令里用 $名字 引用,值不会进聊天和模型。新增后清除或重开会话生效。'],
3635
+ guard: ['🛡 护栏', '发给 AI 前把姓名、证件号、手机号等换成代号,回来再还原;映射只留在本机。'],
3636
+ followup: ['🔄 跟进', '长任务交出去后,AI 每说完一轮,隔一段时间自动替你问进度、让它接着干;判断全部完成就停(最多 5 次,每次都算一轮模型调用)。'],
3637
+ git: ['提交', '查看改动并 git 提交 / 推送。'],
3638
+ memory: ['记忆', '查看、编辑「项目说明」和 AI 的长期记忆,每次新对话都会带上;长期项目可一键让 AI 生成项目说明。'],
3639
+ skills: ['技能', '管理常用指令模板(/命令),一点就发;可自建。'],
3640
+ cost: ['费用', '查看本次会话的 token 用量与花费。'],
3641
+ };
3642
+ document.querySelectorAll('.sk[data-sk]').forEach(b => { const h = SK_HELP[b.dataset.sk]; if (h && !b.title) b.title = h[1]; });
3643
+
3561
3644
  window.openChatHelp = function() {
3562
3645
  overlay.innerHTML = '';
3563
3646
  overlay.classList.remove('hidden');
3564
3647
  const wrap = document.createElement('div');
3565
3648
  wrap.style.cssText = 'position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:1rem;background:rgba(0,0,0,0.55)';
3566
3649
  const box = document.createElement('div');
3567
- box.style.cssText = 'max-width:520px;background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:1.1rem;font-size:0.82rem;line-height:1.75;color:var(--text);max-height:80vh;overflow-y:auto';
3650
+ box.style.cssText = 'max-width:560px;width:100%;background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:1.1rem;font-size:0.82rem;line-height:1.75;color:var(--text);max-height:85vh;overflow-y:auto';
3651
+ const sec = (t) => `<div style="font-size:0.86rem;font-weight:700;margin:0.8rem 0 0.3rem">${t}</div>`;
3652
+ const row = (k, v) => `<div style="display:flex;gap:0.6rem;padding:0.28rem 0;border-top:1px solid var(--border)"><span style="flex:0 0 5.2em;font-weight:600">${k}</span><span style="flex:1;color:var(--text2,var(--text))">${v}</span></div>`;
3653
+ const shown = new Set([...document.querySelectorAll('#sk-claude-agent .sk[data-sk]')].map(b => b.dataset.sk));
3654
+ const skRows = Object.entries(SK_HELP).filter(([k]) => currentSession?.mode !== 'agent' || shown.has(k)).map(([, h]) => row(h[0], h[1])).join('');
3568
3655
  box.innerHTML = `
3569
- <div style="font-size:1rem;font-weight:700;margin-bottom:0.5rem">💬 会话怎么用</div>
3656
+ <div style="font-size:1rem;font-weight:700;margin-bottom:0.3rem">💬 会话怎么用</div>
3570
3657
  <b>发消息</b>:像聊天一样把需求说清楚即可;触屏回车=换行,用发送键发送。<br>
3571
3658
  <b>让它读文件</b>:打 <b>@</b> 从本项目逐层浏览、打字搜索;项目外打 <b>@D:</b>、<b>@/</b>、<b>@~/</b> 按路径逐层浏览;选中插路径;或点「文件」面板的 <b>@</b> 引用。<br>
3572
3659
  <b>工具/思考</b>:AI 干活的过程默认折叠,点「🔧 N 个工具调用」展开看命令与输出。<br>
3573
3660
  <b>AI 忙时</b>:输入框变「存草稿」,先记着干完取回;勾自动接力会自动发出。<br>
3574
- <b>定位旧消息</b>:电脑上鼠标移到聊天区,右下角 ☰ 可列出你发过的消息点击跳转;看完点底部「↓ 回到底部」一键回来。<br>
3575
- <b>常用按钮</b>:模型/计划/压缩/恢复/清除/密钥/护栏/提交——普通用户可在设置→快捷栏精简。<br>
3576
- <span style="color:var(--muted)">更多说明见首页顶栏 ❓ 帮助。</span>`;
3661
+ <b>AI 提问 / 交方案</b>:AI 需要你拿主意时会弹出选项,选一个即可,也可「跳过,让 AI 自己决定」;收起后输入框上方「❓ AI 在等你回答」可再打开。<br>
3662
+ <b>定位旧消息</b>:电脑上鼠标移到聊天区,右下角 ☰ 可列出你发过的消息点击跳转;看完点底部「↓ 回到底部」一键回来。
3663
+ ${sec('顶栏')}
3664
+ ${row('▦ 分屏', '电脑上多个任务并排看、并排干。')}
3665
+ ${row('?', '就是这份说明。')}
3666
+ ${row('⚙ 设置', '聊天显示(思考 / 工具输出是否展开)、快捷栏显示哪些按钮、本任务的选项。')}
3667
+ ${row('◈ 水位', 'AI 的「记忆容量」用了多少。快满时 AI 容易忘事,点「压缩」腾空间。')}
3668
+ ${row('文件', '浏览、编辑、上传、下载本任务目录里的文件。')}
3669
+ ${row('切换', '跳到其他任务。')}
3670
+ ${row('›', '返回首页。')}
3671
+ ${row('📘', '状态栏出现「已带项目说明」表示 AI 每次会先读本项目的项目说明;变黄「偏长」时建议精简。点它查看 / 编辑。')}
3672
+ ${sec('快捷栏(输入框下方)')}
3673
+ ${skRows}
3674
+ <div style="color:var(--muted);margin-top:0.5rem">按钮太多?到 ⚙ 设置 → 快捷栏 关掉用不到的。鼠标停在按钮上也能看到说明。更多见首页 ❓ 帮助。</div>`;
3577
3675
  const closeBtn = document.createElement('button');
3578
3676
  closeBtn.textContent = '知道了';
3579
3677
  closeBtn.style.cssText = 'display:block;margin:0.8rem auto 0;background:var(--blue2);color:#fff;border:none;border-radius:8px;padding:0.5rem 1.4rem;font-size:0.85rem;cursor:pointer';
@@ -4436,10 +4534,7 @@
4436
4534
  askMultiState.answers[qIdx] = { question: qText, label };
4437
4535
  refreshAskMultiSubmit();
4438
4536
  } else {
4439
- _clearAsk();
4440
- addInputMessage(label);
4441
- socket.emit('agent:query', { prompt: label });
4442
- showThinking();
4537
+ answerAsk({ [qText]: label }, label);
4443
4538
  }
4444
4539
  };
4445
4540
  block.appendChild(item);
@@ -4449,6 +4544,8 @@
4449
4544
  });
4450
4545
 
4451
4546
  if (isMulti) refreshAskMultiSubmit();
4547
+ document.getElementById('ask-sheet-skip').style.display = _askCtlId ? '' : 'none';
4548
+ document.getElementById('ask-sheet-cancel').textContent = _askCtlId ? '稍后回答' : '取消';
4452
4549
  sheet.classList.add('open');
4453
4550
  if (!isMulti) inp.focus();
4454
4551
  }
@@ -4476,21 +4573,125 @@
4476
4573
  const answers = askMultiState.answers.slice(0, n);
4477
4574
  if (answers.some((a) => !a)) return;
4478
4575
  const reply = answers.map((a, i) => 'Q' + (i + 1) + ': ' + a.question + '\nA: ' + a.label).join('\n\n');
4576
+ const map = {}; answers.forEach((a) => { map[a.question] = a.label; });
4577
+ answerAsk(map, reply);
4578
+ };
4579
+ // 交互通道下「取消」只是先收起,问题还挂着(输入框上方的提示条可再打开);旧模式照旧清掉
4580
+ window.closeAskSheet = function() {
4581
+ if (_askCtlId) { document.getElementById('ask-sheet').classList.remove('open'); renderPendingBar(); return; }
4479
4582
  _clearAsk();
4480
- addInputMessage(reply);
4481
- socket.emit('agent:query', { prompt: reply });
4482
- showThinking();
4483
4583
  };
4484
- window.closeAskSheet = function() { _clearAsk(); };
4485
4584
  window.submitAskReply = function() {
4486
4585
  const inp = document.getElementById('ask-sheet-input');
4487
4586
  const text = inp.value.trim();
4488
4587
  if (!text) return;
4588
+ answerAsk({ [_askQuestions[0]?.question || '']: text }, text);
4589
+ };
4590
+
4591
+ // ── 交互通道(1.3.193):AI 的提问 / 方案审批由 claude 发起、等 myhi 回答 ──
4592
+ // 新 claude:服务端推 agent:interaction(带 id),答案用 agent:respond 交回 claude 正在等待的那个调用,
4593
+ // 模型拿到的是「对自己问题的回答」,上下文连贯;刷新 / 换设备时服务端会补发,先答先算。
4594
+ // 老 claude(没有交互通道):沿用旧做法——看到 tool_use 就弹窗,答案当新消息发出去。
4595
+ let _askCtlId = null; // 当前问答弹窗对应的交互 id(null = 旧模式)
4596
+ let _planCtl = null; // 待审批的方案 { id, plan }
4597
+ const _ctlToolUseIds = new Set(); // 已由交互通道接管的 tool_use,旧路径别再弹一次
4598
+ function ensureControl() { if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID }); }
4599
+ function answerAsk(map, display) {
4600
+ if (_askCtlId) {
4601
+ ensureControl();
4602
+ socket.emit('agent:respond', { id: _askCtlId, decision: { answers: map } });
4603
+ _askCtlId = null; _clearAsk(); renderPendingBar(); showThinking();
4604
+ return;
4605
+ }
4489
4606
  _clearAsk();
4490
- addInputMessage(text);
4491
- socket.emit('agent:query', { prompt: text });
4607
+ addInputMessage(display);
4608
+ socket.emit('agent:query', { prompt: display });
4492
4609
  showThinking();
4610
+ }
4611
+ window.skipAsk = function() {
4612
+ if (!_askCtlId) return _clearAsk();
4613
+ ensureControl();
4614
+ socket.emit('agent:respond', { id: _askCtlId, decision: { skip: true } });
4615
+ _askCtlId = null; _clearAsk(); renderPendingBar();
4616
+ };
4617
+ // 旧路径:tool_use 一出现就弹。交互通道开着时先等一下,服务端的正式请求到了就以它为准
4618
+ function legacyAsk(toolUseId, input) {
4619
+ if (!currentSession?.interactive) return openAskSheet(input);
4620
+ setTimeout(() => { if (!_ctlToolUseIds.has(toolUseId) && !_askCtlId) openAskSheet(input); }, 1500);
4621
+ }
4622
+ socket.on('agent:interaction', (v) => {
4623
+ if (!v?.id) return;
4624
+ if (v.toolUseId) _ctlToolUseIds.add(v.toolUseId);
4625
+ if (v.kind === 'ask') {
4626
+ if (_askCtlId === v.id) return; // 重连补发的同一题
4627
+ _clearAsk(); _askCtlId = v.id;
4628
+ openAskSheet({ questions: v.questions || [] });
4629
+ } else if (v.kind === 'plan') {
4630
+ if (_planCtl?.id === v.id) return;
4631
+ _planCtl = { id: v.id, plan: v.plan || '' };
4632
+ openPlanSheet();
4633
+ _postStatus('waiting');
4634
+ }
4635
+ renderPendingBar();
4636
+ });
4637
+ socket.on('agent:interaction-closed', ({ id } = {}) => {
4638
+ if (_askCtlId === id) { _askCtlId = null; _clearAsk(); }
4639
+ if (_planCtl?.id === id) { _planCtl = null; closePlanSheet(); }
4640
+ renderPendingBar();
4641
+ });
4642
+ // 输入框上方的「AI 在等你」提示条:弹窗被收起后从这里再打开
4643
+ function renderPendingBar() {
4644
+ const bar = document.getElementById('pending-bar');
4645
+ if (!bar) return;
4646
+ const txt = _planCtl ? '📋 AI 提交了方案,等你审批' : _askCtlId ? '❓ AI 在等你回答一个问题' : '';
4647
+ bar.textContent = txt ? txt + ' · 点这里查看' : '';
4648
+ bar.style.display = txt ? '' : 'none';
4649
+ }
4650
+ window.reopenPending = function() {
4651
+ if (_planCtl) return openPlanSheet();
4652
+ if (_askCtlId) document.getElementById('ask-sheet').classList.add('open');
4653
+ };
4654
+ // ── 方案审批 ──
4655
+ window.openPlanSheet = function() {
4656
+ if (!_planCtl) return;
4657
+ document.getElementById('plan-sheet-body').innerHTML = renderMarkdown(_planCtl.plan || '(方案为空)');
4658
+ document.getElementById('plan-sheet').classList.add('open');
4493
4659
  };
4660
+ window.closePlanSheet = function() { document.getElementById('plan-sheet').classList.remove('open'); renderPendingBar(); };
4661
+ window.approvePlan = function() {
4662
+ if (!_planCtl) return;
4663
+ ensureControl();
4664
+ socket.emit('agent:respond', { id: _planCtl.id, decision: { approve: true } });
4665
+ _planCtl = null; closePlanSheet(); showThinking();
4666
+ };
4667
+ window.revisePlan = function() {
4668
+ if (!_planCtl) return;
4669
+ const fb = document.getElementById('plan-sheet-fb').value.trim();
4670
+ if (!fb) { document.getElementById('plan-sheet-fb').focus(); document.getElementById('plan-sheet-fb').placeholder = '先写下要怎么改,再点这个按钮'; return; }
4671
+ ensureControl();
4672
+ socket.emit('agent:respond', { id: _planCtl.id, decision: { approve: false, feedback: fb } });
4673
+ document.getElementById('plan-sheet-fb').value = '';
4674
+ _planCtl = null; closePlanSheet(); showThinking();
4675
+ };
4676
+ // ── 聊天里的方案卡片:ExitPlanMode 提交的方案、写进 ~/.claude/plans/*.md 的方案文件(原来只是个折叠的「写文件」,看不到内容)──
4677
+ function planTextOf(name, input) {
4678
+ if (!input) return null;
4679
+ if (name === 'ExitPlanMode') return String(input.plan || '');
4680
+ if (name === 'Write' && /[\\/]\.claude[\\/]plans[\\/][^\\/]+\.md$/.test(input.file_path || '')) return String(input.content || '');
4681
+ return null;
4682
+ }
4683
+ let _lastPlanText = '';
4684
+ function addPlanCard(text) {
4685
+ const t = String(text || '').trim();
4686
+ if (!t || t === _lastPlanText) return; // 先写方案文件、再 ExitPlanMode 提交同一份:只显示一次
4687
+ _lastPlanText = t;
4688
+ const el = document.createElement('div');
4689
+ el.className = 'msg msg-assistant msg-plan';
4690
+ el.innerHTML = '<div class="plan-head">📋 方案</div><div class="content"></div>';
4691
+ el.querySelector('.content').innerHTML = renderMarkdown(t);
4692
+ (_replayParent || chatArea).appendChild(el);
4693
+ if (!_replayParent) { trimMessages(); scrollToBottom(); }
4694
+ }
4494
4695
  document.getElementById('ask-sheet-input').addEventListener('keydown', (e) => {
4495
4696
  if (e.key === 'Enter') { e.preventDefault(); submitAskReply(); }
4496
4697
  });
@@ -4602,15 +4803,22 @@
4602
4803
  };
4603
4804
 
4604
4805
  // ── 清除会话 ──────────────────────────────────
4605
- window.doClear = function() {
4806
+ window.closeClearSheet = function() { document.getElementById('clear-sheet').classList.remove('open'); };
4807
+ window.doClear = function(confirmed) {
4808
+ // Agent 模式先弹说明确认:清除不可撤销,且用户常不清楚它会让 AI 忘掉上下文
4809
+ if (currentSession?.mode === 'agent' && !confirmed) {
4810
+ document.getElementById('clear-sheet').classList.add('open');
4811
+ return;
4812
+ }
4813
+ closeClearSheet();
4814
+ if (workState !== 'idle' && !confirm('AI 还在执行,清除会中断这一轮。确定清除?')) return;
4606
4815
  // 清除聊天区 UI
4607
4816
  chatArea.innerHTML = '';
4608
4817
  endStream();
4609
4818
  // Agent 模式:发送 /clear 清除 Claude 上下文
4610
4819
  if (currentSession?.mode === 'agent') {
4611
4820
  if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
4612
- socket.emit('agent:query', { prompt: '/clear' });
4613
- addStatusMessage('已清除对话和 Claude 上下文');
4821
+ socket.emit('agent:query', { prompt: '/clear' }); // 服务端回一条「已开始新对话」,所有在看的人都能看到
4614
4822
  _ctxNudgeLevel = 0;
4615
4823
  _lastCtxTokens = 0;
4616
4824
  ctxUpdate(0); // 上下文已清空,水位指示复位
@@ -4786,6 +4994,55 @@
4786
4994
  `本任务有 ${n} 条待发草稿 —— 点这里取回`;
4787
4995
  tip.style.display = 'flex';
4788
4996
  }
4997
+ // ── 项目说明(CLAUDE.md):状态标记 + 适时建议生成 ─────────────
4998
+ // 新用户不知道 CLAUDE.md,等看出是长期项目时再提:同目录还有别的任务 / 本任务聊了很多轮 / 说了「以后都…」。
4999
+ // 每个目录可「不再提示」(存本机);× 只关本页。
5000
+ const GUIDE_REMEMBER_RE = /以后(都|每次|一律)|每次都(要|得|先)|记住|总是要|别再忘|下次(也|还)要/;
5001
+ const GUIDE_TURNS = 15;
5002
+ let _guide = null, _guideRemember = false, _guideTipClosed = false;
5003
+ const guideOffKey = 'myhi_guide_tip_off';
5004
+ const guideOffList = () => { try { return JSON.parse(localStorage.getItem(guideOffKey) || '[]'); } catch { return []; } };
5005
+ async function checkGuide() {
5006
+ if (currentSession?.mode !== 'agent') return;
5007
+ try { const r = await fetch('/api/session/' + SESSION_ID + '/guide'); if (r.ok) _guide = await r.json(); } catch {}
5008
+ renderGuideChip(); renderGuideTip();
5009
+ }
5010
+ function renderGuideChip() {
5011
+ const c = document.getElementById('guide-chip');
5012
+ if (!c) return;
5013
+ if (!_guide?.exists) { c.style.display = 'none'; return; }
5014
+ c.style.display = '';
5015
+ c.style.color = _guide.long ? 'var(--yellow)' : 'var(--muted)';
5016
+ c.textContent = _guide.long ? '📘 项目说明偏长' : '📘 已带项目说明';
5017
+ c.title = _guide.long
5018
+ ? `项目说明(CLAUDE.md)有 ${_guide.lines} 行,每轮都会占用 AI 的记忆容量。建议精简到要点(可以让 AI 帮你精简)。点击查看`
5019
+ : `AI 每次开始工作前会先读本项目的项目说明(CLAUDE.md,${_guide.lines} 行)。点击查看 / 编辑`;
5020
+ }
5021
+ function guideTipReason() {
5022
+ if (!_guide || _guide.exists) return '';
5023
+ if (_guideRemember) return '想让 AI 以后每次都记得?可以写进「项目说明」,每次新对话 AI 都会先读,不用再重复交代。';
5024
+ if (_guide.siblings > 0) return `这个项目目录还有 ${_guide.siblings} 个其他任务在用。让 AI 把项目情况和你的要求写成「项目说明」,以后每个任务、每次对话都会先读。`;
5025
+ if (_guide.turns >= GUIDE_TURNS) return '这个项目你常来。让 AI 把项目情况和你的要求写成「项目说明」,以后每次新对话不用再重复交代。';
5026
+ return '';
5027
+ }
5028
+ window.renderGuideTip = function() {
5029
+ const tip = document.getElementById('guide-tip');
5030
+ if (!tip) return;
5031
+ const why = guideTipReason();
5032
+ const off = currentSession?.cwd && guideOffList().includes(currentSession.cwd);
5033
+ if (!why || off || _guideTipClosed || workState !== 'idle' || !canTakeControl()) { tip.style.display = 'none'; return; }
5034
+ document.getElementById('guide-tip-text').textContent = why;
5035
+ tip.style.display = 'flex';
5036
+ };
5037
+ window.guideTipGo = function() { _guideTipClosed = true; renderGuideTip(); genProjectGuide(); };
5038
+ window.guideTipClose = function() { _guideTipClosed = true; _guideRemember = false; renderGuideTip(); };
5039
+ window.guideTipOff = function() {
5040
+ const l = guideOffList();
5041
+ if (currentSession?.cwd && !l.includes(currentSession.cwd)) { l.push(currentSession.cwd); localStorage.setItem(guideOffKey, JSON.stringify(l.slice(-200))); }
5042
+ renderGuideTip();
5043
+ addStatusMessage('本项目不再提示生成项目说明(需要时在快捷栏「记忆」里生成)');
5044
+ };
5045
+
4789
5046
  window.hideDraftTip = function() { _draftTipDismissed = true; renderDraftTip(); };
4790
5047
 
4791
5048
  window.openInboxSheet = async function() {
@@ -5208,16 +5465,28 @@
5208
5465
  const isPlan = currentSession?.permissionMode === 'plan';
5209
5466
  if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
5210
5467
  if (isPlan) {
5211
- // 直接切回 bypassPermissions,不走 /plan 命令(因为那要等 agent 响应)
5212
- socket.emit('set-mode', { sessionId: SESSION_ID, mode: 'bypassPermissions' });
5213
- if (currentSession) currentSession.permissionMode = 'bypassPermissions';
5214
- updatePlanButton();
5215
- addStatusMessage('已退出计划模式,恢复到自动执行');
5468
+ // AI 已经提交了方案在等审批:直接打开审批(批准即在原进程里切到执行,不打断)
5469
+ if (_planCtl) return openPlanSheet();
5470
+ document.getElementById('planexit-sheet').classList.add('open');
5216
5471
  } else {
5217
5472
  // 进入计划:弹窗输入计划描述
5218
5473
  doSlashCmd('/plan');
5219
5474
  }
5220
5475
  };
5476
+ window.closePlanExit = function() { document.getElementById('planexit-sheet').classList.remove('open'); };
5477
+ // 手动退出计划:切回自动执行会重启 claude 进程——AI 还在规划时先确认,免得这一轮被悄悄打断
5478
+ window.exitPlan = function(run) {
5479
+ closePlanExit();
5480
+ if (workState !== 'idle' && !confirm('AI 还在规划,现在退出会中断这一轮。确定退出?')) return;
5481
+ socket.emit('set-mode', { sessionId: SESSION_ID, mode: 'bypassPermissions' });
5482
+ if (currentSession) currentSession.permissionMode = 'bypassPermissions';
5483
+ updatePlanButton();
5484
+ addStatusMessage('已退出计划模式,恢复到自动执行');
5485
+ if (run) setTimeout(() => {
5486
+ const t = '按刚才的方案开始执行。';
5487
+ addInputMessage(t); socket.emit('agent:query', { prompt: t }); showThinking();
5488
+ }, 600);
5489
+ };
5221
5490
  socket.on('mode-changed', ({ sessionId, mode }) => {
5222
5491
  if (sessionId !== SESSION_ID) return;
5223
5492
  if (currentSession) currentSession.permissionMode = mode;
@@ -5749,10 +6018,22 @@
5749
6018
  container.innerHTML = '';
5750
6019
  container.appendChild(makeBar());
5751
6020
 
6021
+ // 本项目还没有项目说明(CLAUDE.md):讲清它是什么、适合什么项目,给一键生成入口——新用户不必知道这个文件名
6022
+ if (_memScope === 'project' && !files.some(f => f.name === 'CLAUDE.md')) {
6023
+ const intro = document.createElement('div');
6024
+ intro.style.cssText = 'border:1px dashed var(--border);border-radius:10px;padding:0.75rem 0.85rem;margin-bottom:0.6rem;line-height:1.7';
6025
+ intro.innerHTML = '<div style="font-weight:700;color:var(--text);font-size:0.86rem">📘 本项目还没有「项目说明」</div>'
6026
+ + '<div>项目说明是 AI <b>每次开始工作前都会先读</b>的一页项目规矩:项目是做什么的、关键目录、怎么构建测试、你的固定要求(如「回复用中文」「改完先跑测试」)。有了它,每次新对话不用再重复交代。</div>'
6027
+ + '<div style="color:var(--muted);margin-top:0.2rem">适合会反复来做的长期项目;一次性的小任务不需要。</div>'
6028
+ + '<button onclick="genProjectGuide()" style="margin-top:0.55rem;width:100%;padding:0.5rem;border-radius:8px;cursor:pointer;font-size:0.8rem;border:none;background:var(--green);color:#fff;font-weight:600">📘 让 AI 生成项目说明</button>'
6029
+ + '<div style="color:var(--muted);font-size:0.7rem;margin-top:0.3rem">AI 会先看一遍项目、写出草稿给你确认,确认后才保存(文件名 CLAUDE.md,放在项目目录里)。</div>';
6030
+ container.appendChild(intro);
6031
+ }
6032
+
5752
6033
  if (files.length === 0) {
5753
6034
  const e = document.createElement('div');
5754
6035
  e.style.cssText = 'text-align:center;color:var(--muted);padding:1.5rem 0';
5755
- e.textContent = _memScope === 'project' ? '本项目暂无记忆文件' : '暂无记忆文件';
6036
+ e.textContent = _memScope === 'project' ? '还没有其他记忆(AI 在对话中记下的经验会出现在这里)' : '暂无记忆文件';
5756
6037
  container.appendChild(e);
5757
6038
  return;
5758
6039
  }
@@ -5764,7 +6045,9 @@
5764
6045
  const dm = m[1].match(/^description:\s*(.+)$/m);
5765
6046
  return { desc: dm ? dm[1].trim() : '', body: content.slice(m[0].length) };
5766
6047
  };
5767
- const cleanName = (n) => n.replace(/^.*\//, '').replace(/\.md$/i, '');
6048
+ // CLAUDE.md 用大白话称呼(文件名放进描述里给懂行的人看)
6049
+ const NICE = { 'CLAUDE.md': ['📘 项目说明', 'CLAUDE.md · AI 每次开始工作前先读的项目规矩'], '~/.claude/CLAUDE.md': ['🌐 全局说明', '~/.claude/CLAUDE.md · 对所有项目都生效'] };
6050
+ const cleanName = (n) => NICE[n] ? NICE[n][0] : n.replace(/^.*\//, '').replace(/\.md$/i, '');
5768
6051
 
5769
6052
  // 搜索框
5770
6053
  const search = document.createElement('input');
@@ -5782,7 +6065,7 @@
5782
6065
  for (const f of files) {
5783
6066
  if (kw && !(f.name.toLowerCase().includes(kw) || (f.content || '').toLowerCase().includes(kw))) continue;
5784
6067
  shown++;
5785
- const { desc, body: md } = parseFM(f.content || '');
6068
+ const fm = parseFM(f.content || ''); const md = fm.body; const desc = fm.desc || (NICE[f.name] ? NICE[f.name][1] : '');
5786
6069
  const deletable = f.name.startsWith('memory/') || f.name.startsWith('~/.claude/memory/');
5787
6070
  const editable = deletable || f.name.endsWith('CLAUDE.md') || f.name.endsWith('settings.json');
5788
6071
  const card = document.createElement('div');
@@ -5830,6 +6113,26 @@
5830
6113
  };
5831
6114
 
5832
6115
  // 让 AI 按规则过一遍记忆(只分析给建议,不改文件)
6116
+ // 一键生成项目说明(CLAUDE.md):AI 先调研、出草稿,用户确认后才写文件——项目说明会改变 AI 之后的行为,不能静默写入
6117
+ window.genProjectGuide = function() {
6118
+ closeMemorySheet();
6119
+ if (currentSession?.mode !== 'agent') return addStatusMessage('该功能仅在 agent(Claude) 会话可用');
6120
+ if (workState !== 'idle') return addStatusMessage('AI 正在干活,等这一轮结束再生成项目说明');
6121
+ const prompt = [
6122
+ '请为当前项目目录写一份项目说明(项目根目录的 CLAUDE.md,以后每次对话开始你都会先读它)。',
6123
+ '步骤:1) 先快速了解项目:目录结构、README、构建/依赖配置(如 package.json、pyproject、Makefile 等),不用逐个细读源码;',
6124
+ '2) 结合本次对话里我交代过的要求和习惯;',
6125
+ '3) 用中文写一份精简草稿(建议 30~60 行,只写要点):项目是做什么的、关键目录/文件、怎么运行/构建/测试、约定与注意事项、我的固定要求;',
6126
+ '不要写任何密码、token、密钥等敏感值。',
6127
+ '4) 先把完整草稿贴出来让我确认,问我是否保存(可以直接保存 / 我要修改);我确认后再写入 CLAUDE.md。在我确认前不要创建或修改任何文件。',
6128
+ '如果项目根目录已经有 CLAUDE.md,就在它基础上补充,并说明改了哪些。',
6129
+ ].join('');
6130
+ if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
6131
+ addInputMessage('📘 让 AI 生成项目说明');
6132
+ socket.emit('agent:query', { prompt });
6133
+ showThinking();
6134
+ };
6135
+
5833
6136
  window.analyzeMemory = function() {
5834
6137
  const scopeTxt = _memScope === 'all' ? '所有项目 + 全局' : '本项目';
5835
6138
  const prompt = [
package/dist/grid.html CHANGED
@@ -7,9 +7,10 @@
7
7
  <title>myhi</title>
8
8
  <script src="/lib/machine.js"></script>
9
9
  <script src="/sio-client/socket.io.min.js"></script>
10
+ <script src="/theme.js"></script>
10
11
  <script>
11
12
  (function(){try{
12
- var t=localStorage.getItem('myhi_theme')||'dark';
13
+ var t=localStorage.getItem('myhi_theme')||window.MYHI_DEFAULT_THEME||'dark';
13
14
  var f=localStorage.getItem('myhi_font')||'medium';
14
15
  var r=document.documentElement;
15
16
  if(t&&t!=='dark') r.setAttribute('data-theme',t);