@wendongfly/myhi 1.3.191 → 1.3.192

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/index.html CHANGED
@@ -13,8 +13,9 @@
13
13
  <script src="/lib/machine.js"></script>
14
14
  <script src="/lib/prefs.js"></script>
15
15
  <script src="/sio-client/socket.io.min.js"></script>
16
+ <script src="/theme.js"></script>
16
17
  <script>
17
- (function(){try{var t=localStorage.getItem('myhi_theme')||'dark',f=localStorage.getItem('myhi_font')||'medium',r=document.documentElement;if(t&&t!=='dark')r.setAttribute('data-theme',t);r.setAttribute('data-font',f);}catch(e){}})();
18
+ (function(){try{var t=localStorage.getItem('myhi_theme')||window.MYHI_DEFAULT_THEME||'dark',f=localStorage.getItem('myhi_font')||'medium',r=document.documentElement;if(t&&t!=='dark')r.setAttribute('data-theme',t);r.setAttribute('data-font',f);}catch(e){}})();
18
19
  </script>
19
20
  <style>
20
21
  :root {
@@ -36,8 +37,15 @@
36
37
  html[data-font="small"]{font-size:15px;} html[data-font="medium"]{font-size:17px;} html[data-font="large"]{font-size:19.5px;}
37
38
  * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
38
39
 
40
+ /* 整页滚动(原来只有中间 640px 的列表在滚:大屏上鼠标在两侧空白处滚不动,系统白色粗滚动条还贴着卡片)。
41
+ 顶栏 + 搜索区吸顶,滚动条回到浏览器最右,改成细的深色样式 */
42
+ html { scrollbar-width: thin; scrollbar-color: var(--border2) transparent; }
43
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
44
+ ::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
45
+ ::-webkit-scrollbar-thumb:hover { background: var(--muted); }
46
+ ::-webkit-scrollbar-track { background: transparent; }
39
47
  html, body {
40
- height: 100%;
48
+ min-height: 100%;
41
49
  background: var(--bg);
42
50
  color: var(--text);
43
51
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
@@ -48,10 +56,11 @@
48
56
  #app {
49
57
  display: flex;
50
58
  flex-direction: column;
51
- height: 100%;
59
+ min-height: 100vh;
52
60
  max-width: 640px;
53
61
  margin: 0 auto;
54
62
  }
63
+ #view-controls { position: sticky; top: var(--hd-h, 57px); z-index: 9; background: var(--bg); padding-bottom: 0.4rem !important; }
55
64
 
56
65
  /* ── Header ── */
57
66
  #header {
@@ -169,6 +178,77 @@
169
178
  .ai-banner button:disabled { opacity: 0.6; cursor: default; }
170
179
  .ai-banner.ok { border-color: rgba(63,185,80,0.45); background: rgba(63,185,80,0.1); }
171
180
  .ai-banner.err { border-color: rgba(248,81,73,0.45); background: rgba(248,81,73,0.08); }
181
+ /* ── 电脑端工作台(≥1024px,body.ws):左侧任务栏 + 右侧嵌入当前任务会话(仿 DeepSeek)。手机/窄屏不变 ── */
182
+ #ws-main, #ws-foot, #ws-collapse, #ws-rail { display: none; }
183
+ body.ws { overflow: hidden; }
184
+ body.ws #app { position: fixed; left: 0; top: 0; bottom: 0; width: 290px; max-width: none; min-height: 0; margin: 0;
185
+ border-right: 1px solid var(--border); background: var(--surface); overflow: hidden; display: flex; flex-direction: column; }
186
+ body.ws #header { background: var(--surface); padding-top: 0.8rem; padding-bottom: 0.6rem; border-bottom: none; }
187
+ body.ws .hd-actions > :not(#conn-dot) { display: none !important; }
188
+ body.ws #ws-collapse { display: block; }
189
+ body.ws #view-controls { position: static; background: var(--surface); padding-top: 0 !important; }
190
+ body.ws #view-controls > div:first-child { flex-direction: column-reverse; align-items: stretch; }
191
+ body.ws #new-btn { display: block !important; width: 100%; padding: 0.55rem; }
192
+ body.ws #search-count { display: none; }
193
+ body.ws #sess-search { flex: none; width: 100%; }
194
+ body.ws #header { justify-content: flex-start; }
195
+ body.ws #ws-collapse { margin-left: auto; }
196
+ body.ws #list { flex: 1; overflow-y: auto; padding: 0.3rem 0.5rem 1rem; scrollbar-width: thin; }
197
+ body.ws .card { padding: 0.45rem 0.55rem; gap: 0.55rem; margin-bottom: 2px; border: 1px solid transparent; background: transparent; border-radius: 8px; }
198
+ body.ws .card:hover { background: var(--bg); }
199
+ body.ws .card.active { background: var(--bg); border-color: var(--border); }
200
+ body.ws .card.active .card-title { color: var(--accent); }
201
+ body.ws .avatar { width: 26px; height: 26px; border-radius: 7px; font-size: 0.78rem; }
202
+ body.ws .card-title { font-size: 0.84rem; font-weight: 500; }
203
+ body.ws .card-sub { font-size: 0.68rem; margin-top: 0.05rem; }
204
+ body.ws .card-gist, body.ws .card-tags { display: none !important; }
205
+ body.ws .group-header { margin: 0.6rem 0.3rem 0.2rem !important; }
206
+ body.ws #fab, body.ws #fab-note, body.ws #fab-tip { display: none !important; }
207
+ body.ws #ws-foot { display: flex; align-items: center; gap: 0.4rem; padding: 0.55rem 0.6rem 0.6rem 0.9rem; }
208
+ #ws-user { flex: 1; min-width: 0; font-size: 0.8rem; color: var(--text2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
209
+ #ws-set { position: relative; background: none; border: none; color: var(--text2); font-size: 1.05rem; width: 34px; height: 34px; border-radius: 8px; cursor: pointer; }
210
+ #ws-set:hover, #ws-set.on { background: var(--bg); color: var(--text); }
211
+ #ws-set.has-dot::after, #ws-menu-dot.on { content: ''; position: absolute; top: 6px; right: 6px; width: 7px; height: 7px; border-radius: 50%; background: var(--red); }
212
+ #ws-menu { display: none; position: fixed; z-index: 45; min-width: 12rem; background: var(--surface); border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 10px 30px rgba(0,0,0,0.4); padding: 0.3rem; }
213
+ #ws-menu.open { display: block; }
214
+ #ws-menu button { position: relative; display: flex; align-items: baseline; gap: 0.5rem; width: 100%; text-align: left; background: none; border: none; color: var(--text); font-size: 0.84rem; padding: 0.5rem 0.6rem; border-radius: 7px; cursor: pointer; }
215
+ #ws-menu button:hover { background: var(--bg); }
216
+ #ws-menu button small { margin-left: auto; font-size: 0.68rem; color: var(--muted); }
217
+ #ws-menu button.danger { color: var(--red); }
218
+ #ws-menu hr { border: none; border-top: 1px solid var(--border); margin: 0.3rem 0.2rem; }
219
+ #ws-menu-dot.on { position: static; display: inline-block; margin-left: 0.3rem; }
220
+ #ws-collapse { background: none; border: none; color: var(--muted); font-size: 1rem; cursor: pointer; padding: 0.2rem 0.4rem; border-radius: 6px; }
221
+ #ws-collapse:hover { background: var(--bg); color: var(--text); }
222
+ body.ws #ws-main { display: block; position: fixed; left: 290px; top: 0; right: 0; bottom: 0; background: var(--bg); }
223
+ #ws-main iframe { width: 100%; height: 100%; border: none; display: block; }
224
+ #ws-welcome { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1rem; padding: 2rem; color: var(--muted); text-align: center; }
225
+ #ws-welcome .logo { font-size: 2.2rem; }
226
+ #ws-welcome .ai-banner { margin: 0; max-width: 520px; text-align: left; }
227
+ #ws-welcome button.ws-new { background: var(--accent); color: #fff; border: none; border-radius: 10px; padding: 0.7rem 1.6rem; font-size: 0.95rem; font-weight: 600; cursor: pointer; }
228
+ /* 收起任务栏 → 留一条 56px 窄栏(仿 VS Code / DeepSeek):展开、新建、最近任务头像(悬停看名字、点了直接切)、设置。
229
+ 原来收起后只剩左下角一个小「» 任务」,还压在快捷栏上,普通用户根本发现不了 */
230
+ body.ws.ws-collapsed #app { display: none; }
231
+ body.ws.ws-collapsed #ws-main { left: 56px; }
232
+ body.ws.ws-collapsed #ws-rail { display: flex; position: fixed; left: 0; top: 0; bottom: 0; width: 56px; flex-direction: column; align-items: center;
233
+ gap: 0.35rem; padding: 0.7rem 0 0.6rem; background: var(--surface); border-right: 1px solid var(--border); z-index: 20; }
234
+ #ws-rail .rb { position: relative; width: 38px; height: 38px; border-radius: 10px; border: none; background: none; color: var(--text2); font-size: 1.05rem;
235
+ cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
236
+ #ws-rail .rb:hover { background: var(--bg); color: var(--text); }
237
+ #ws-rail .rb.new { background: var(--accent); color: #fff; font-size: 1.3rem; }
238
+ #ws-rail .rb.has-dot::after { content: ''; position: absolute; top: 5px; right: 5px; width: 7px; height: 7px; border-radius: 50%; background: var(--red); }
239
+ #ws-rail .rail-sep { width: 28px; border-top: 1px solid var(--border); margin: 0.2rem 0; flex-shrink: 0; }
240
+ #ws-rail .rail-list { flex: 1; min-height: 0; overflow-y: auto; scrollbar-width: none; display: flex; flex-direction: column; align-items: center; gap: 0.35rem; padding: 0.1rem 0; }
241
+ #ws-rail .rail-list::-webkit-scrollbar { display: none; }
242
+ #ws-rail .ra { position: relative; width: 34px; height: 34px; border-radius: 9px; color: #fff; font-weight: 700; font-size: 0.85rem; border: 2px solid transparent;
243
+ display: flex; align-items: center; justify-content: center; cursor: pointer; flex-shrink: 0; font-family: 'SF Mono', Consolas, monospace; }
244
+ #ws-rail .ra:hover { filter: brightness(1.15); }
245
+ #ws-rail .ra.active { border-color: var(--text); box-shadow: 0 0 0 2px var(--bg); }
246
+ #ws-rail .ra.busy::after { content: ''; position: absolute; top: -3px; right: -3px; width: 9px; height: 9px; border-radius: 50%; background: var(--accent);
247
+ border: 2px solid var(--surface); animation: stPulse 1.2s ease-in-out infinite; }
248
+ #ws-rail .rail-more { font-size: 0.68rem; color: var(--muted); cursor: pointer; padding: 0.2rem; }
249
+ /* 窄栏悬停提示(原生 title 有延迟,这里即时显示) */
250
+ #ws-tip { position: fixed; z-index: 60; left: 62px; background: var(--text); color: var(--bg); font-size: 0.75rem; padding: 0.3rem 0.55rem; border-radius: 6px;
251
+ white-space: nowrap; pointer-events: none; display: none; max-width: 320px; overflow: hidden; text-overflow: ellipsis; }
172
252
  /* 当前使用模式(单用户 / 多用户·管理员 / 多用户·成员),让成员知道为什么有些项是只读 */
173
253
  .set-mode { font-size: 0.72rem; color: var(--muted); margin: 0.35rem 0.25rem 0; }
174
254
  .set-mode a { color: var(--accent); text-decoration: none; }
@@ -184,11 +264,10 @@
184
264
  /* ── Session list ── */
185
265
  #list {
186
266
  flex: 1;
187
- overflow-y: auto;
188
- -webkit-overflow-scrolling: touch;
189
- padding: 0.75rem 0.75rem 6rem;
267
+ padding: 0.5rem 0.75rem 6rem;
190
268
  }
191
269
 
270
+
192
271
  .empty-state {
193
272
  display: flex;
194
273
  flex-direction: column;
@@ -213,7 +292,7 @@
213
292
  display: flex;
214
293
  align-items: center;
215
294
  gap: 0.85rem;
216
- padding: 0.85rem 0.9rem;
295
+ padding: 0.7rem 0.85rem;
217
296
  background: var(--surface);
218
297
  border: 1px solid var(--border);
219
298
  border-radius: 12px;
@@ -226,10 +305,10 @@
226
305
  .card:active { background: #1c2230; }
227
306
 
228
307
  .avatar {
229
- width: 44px; height: 44px;
308
+ width: 38px; height: 38px;
230
309
  border-radius: 10px;
231
310
  display: flex; align-items: center; justify-content: center;
232
- font-size: 1.3rem;
311
+ font-size: 1.1rem;
233
312
  font-weight: 700;
234
313
  flex-shrink: 0;
235
314
  font-family: 'SF Mono', Consolas, monospace;
@@ -240,7 +319,10 @@
240
319
  flex: 1;
241
320
  min-width: 0;
242
321
  }
322
+ .card-title-row { display: flex; align-items: center; gap: 0.4rem; min-width: 0; }
323
+ .card-title-row .card-tags { display: flex; gap: 0.25rem; flex: 0 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; }
243
324
  .card-title {
325
+ flex: 0 1 auto; min-width: 0;
244
326
  font-size: 0.95rem;
245
327
  font-weight: 600;
246
328
  white-space: nowrap;
@@ -279,6 +361,12 @@
279
361
  }
280
362
  .status-dot.alive { background: var(--green); box-shadow: 0 0 5px var(--green); }
281
363
  .status-dot.dead { background: var(--red); }
364
+ /* 任务状态:执行中(AI 正在跑,跳动)/ 已停止;空闲不显示,免得一排绿点看不出谁在干活 */
365
+ .st { font-size: 0.68rem; white-space: nowrap; display: flex; align-items: center; gap: 0.3rem; }
366
+ .st-busy { color: var(--accent); font-weight: 600; }
367
+ .st-busy::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: var(--accent); animation: stPulse 1.2s ease-in-out infinite; }
368
+ @keyframes stPulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.35; transform: scale(0.7); } }
369
+ .st-dead { color: var(--muted); }
282
370
  .viewers {
283
371
  font-size: 0.7rem;
284
372
  color: var(--muted);
@@ -612,6 +700,7 @@
612
700
  <div id="app">
613
701
  <div id="header">
614
702
  <div class="logo">my<span>hi</span></div>
703
+ <button id="ws-collapse" onclick="wsSetCollapsed(true)" title="收起任务栏(收起后左侧保留窄栏)">«</button>
615
704
  <div class="hd-actions">
616
705
  <button id="grid-btn" class="hd-btn" onclick="location.href='/grid'" title="分屏工作台(多任务并排)">▦<span class="hd-txt"> 分屏</span></button>
617
706
  <button id="doctor-btn" class="hd-btn" onclick="openDoctor()" title="会话体检">🩺<span class="hd-txt"> 体检</span></button>
@@ -655,6 +744,42 @@
655
744
  <small>以后随时点右下角的 + 新建</small>
656
745
  </div>
657
746
  </div>
747
+ <!-- 工作台任务栏底部:平时只留一行「用户 + ⚙」,其余功能收进 ⚙ 菜单(功能再多也不占地方) -->
748
+ <div id="ws-foot">
749
+ <span id="ws-user">👤 本机</span>
750
+ <button id="ws-set" onclick="toggleWsMenu(event)" title="设置与更多">⚙</button>
751
+ </div>
752
+ </div>
753
+
754
+ <!-- 电脑端工作台:任务栏底部功能区 + 右侧会话区(body.ws 时显示) -->
755
+ <div id="ws-main"><div id="ws-welcome">
756
+ <div class="logo">my<span>hi</span></div>
757
+ <div>在左边选一个任务继续,或者新建一个</div>
758
+ <button class="ws-new" onclick="openSheet()">+ 新建任务</button>
759
+ </div></div>
760
+ <!-- 收起后的窄栏 -->
761
+ <div id="ws-rail">
762
+ <button class="rb" onclick="wsSetCollapsed(false)" data-tip="展开任务栏">☰</button>
763
+ <button class="rb new" onclick="openSheet()" data-tip="新建任务">+</button>
764
+ <div class="rail-sep"></div>
765
+ <div class="rail-list" id="rail-list"></div>
766
+ <div class="rail-sep"></div>
767
+ <button class="rb" id="rail-set" onclick="toggleWsMenu(event)" data-tip="设置与更多">⚙</button>
768
+ </div>
769
+ <div id="ws-tip"></div>
770
+ <!-- ⚙ 菜单(工作台底部 / 窄栏的 ⚙ 打开) -->
771
+ <div id="ws-menu">
772
+ <button onclick="wsMenuGo(openSettings)">⚙ 设置<span id="ws-menu-dot"></span></button>
773
+ <button onclick="wsMenuGo(() => location.href = '/grid')">▦ 分屏<small>多任务并排</small></button>
774
+ <button onclick="wsMenuGo(openNoteSheet)">📥 随手记<small>先记进草稿箱</small></button>
775
+ <button onclick="wsMenuGo(openDoctor)">🩺 会话体检</button>
776
+ <button onclick="wsMenuGo(showUsage)">📊 用量</button>
777
+ <button onclick="wsMenuGo(openHelpSheet)">❓ 帮助</button>
778
+ <div id="ws-menu-acct">
779
+ <hr>
780
+ <button id="ws-menu-pw" onclick="wsMenuGo(openPasswdSheet)" style="display:none">🔑 修改密码</button>
781
+ <button class="danger" onclick="wsMenuGo(doLogout)">↩ 退出登录</button>
782
+ </div>
658
783
  </div>
659
784
 
660
785
  <!-- FAB -->
@@ -906,6 +1031,7 @@
906
1031
  document.getElementById('user-name').textContent = data.name;
907
1032
  document.getElementById('um-name').textContent = data.name;
908
1033
  document.getElementById('user-btn').style.display = '';
1034
+ document.getElementById('ws-user').textContent = '👤 ' + data.name;
909
1035
  fitHeader();
910
1036
  }
911
1037
  // 成员用户(users.json 登录)才显示自助改密入口;管理员改密码在 /admin
@@ -917,7 +1043,9 @@
917
1043
  if (!_isAdmin) document.querySelectorAll('#set-svc .admin-only').forEach(el => el.remove());
918
1044
  checkAiEngine();
919
1045
  checkHomeVersion();
920
- if (data.hasUsers && data.role === 'operator') document.getElementById('passwd-btn').style.display = '';
1046
+ if (data.hasUsers && data.role === 'operator') { document.getElementById('passwd-btn').style.display = ''; document.getElementById('ws-menu-pw').style.display = ''; }
1047
+ // 单用户模式没有「登录账号」可退:⚙ 菜单不显示账户段
1048
+ if (!(data.exclusive || data.hasUsers)) document.getElementById('ws-menu-acct').style.display = 'none';
921
1049
  if (data.dir) {
922
1050
  _userDir = data.dir;
923
1051
  // 默认填入绑定目录,隐藏盘符栏,保留最近目录
@@ -1026,9 +1154,14 @@
1026
1154
  e.stopPropagation();
1027
1155
  const m = document.getElementById('user-menu');
1028
1156
  if (m.classList.contains('open')) return closeUserMenu();
1029
- const r = document.getElementById('user-btn').getBoundingClientRect();
1030
- m.style.top = (r.bottom + 6) + 'px';
1031
- m.style.right = Math.max(8, window.innerWidth - r.right) + 'px';
1157
+ const r = (e.currentTarget || document.getElementById('user-btn')).getBoundingClientRect();
1158
+ if (r.bottom > innerHeight - 220) { // 工作台任务栏底部的用户按钮:往上弹
1159
+ m.style.top = ''; m.style.bottom = (innerHeight - r.top + 6) + 'px';
1160
+ m.style.left = r.left + 'px'; m.style.right = 'auto';
1161
+ } else {
1162
+ m.style.bottom = ''; m.style.top = (r.bottom + 6) + 'px';
1163
+ m.style.left = 'auto'; m.style.right = Math.max(8, window.innerWidth - r.right) + 'px';
1164
+ }
1032
1165
  m.classList.add('open');
1033
1166
  };
1034
1167
  window.closeUserMenu = function() { document.getElementById('user-menu').classList.remove('open'); };
@@ -1047,7 +1180,7 @@
1047
1180
  // ── 外观:主题 & 字号(与会话页共用 localStorage myhi_theme / myhi_font)──
1048
1181
  const THEME_META = { dark: '#0d1117', light: '#ffffff', black: '#000000', system: '#0d1117' };
1049
1182
  function applyAppearance() {
1050
- const t = localStorage.getItem('myhi_theme') || 'dark';
1183
+ const t = localStorage.getItem('myhi_theme') || window.MYHI_DEFAULT_THEME || 'dark';
1051
1184
  const f = localStorage.getItem('myhi_font') || 'medium';
1052
1185
  const r = document.documentElement;
1053
1186
  if (!t || t === 'dark') r.removeAttribute('data-theme'); else r.setAttribute('data-theme', t);
@@ -1059,6 +1192,7 @@
1059
1192
  // 顶栏按钮组折行了就逐级收紧:字号「大」时按钮跟着 rem 变宽,固定断点兜不住
1060
1193
  function fitHeader() {
1061
1194
  const h = document.getElementById('header');
1195
+ requestAnimationFrame(() => document.documentElement.style.setProperty('--hd-h', h.offsetHeight + 'px')); // 搜索区吸顶在顶栏下方
1062
1196
  const acts = h.querySelector('.hd-actions');
1063
1197
  const wraps = () => acts.offsetHeight > document.getElementById('settings-btn').offsetHeight * 1.5;
1064
1198
  h.classList.remove('hd-fit1', 'hd-fit2');
@@ -1072,7 +1206,7 @@
1072
1206
  const THEMES = [['system','跟随系统'],['dark','深色'],['light','浅色'],['black','纯黑']];
1073
1207
  const FONTS = [['small','小'],['medium','中'],['large','大']];
1074
1208
  function renderAppearance() {
1075
- const t = localStorage.getItem('myhi_theme') || 'dark';
1209
+ const t = localStorage.getItem('myhi_theme') || window.MYHI_DEFAULT_THEME || 'dark';
1076
1210
  const f = localStorage.getItem('myhi_font') || 'medium';
1077
1211
  const seg = (key, opts, cur) => opts.map(([v, l]) => `<button data-k="${key}" data-v="${v}" class="${v === cur ? 'on' : ''}">${l}</button>`).join('');
1078
1212
  document.getElementById('set-theme').innerHTML = seg('myhi_theme', THEMES, t);
@@ -1210,6 +1344,9 @@
1210
1344
  btn.style.display = _homeVer.updateAvailable ? '' : 'none';
1211
1345
  btn.textContent = '升级到 v' + _homeVer.latest;
1212
1346
  document.getElementById('settings-btn').classList.toggle('has-dot', !!_homeVer.updateAvailable);
1347
+ document.getElementById('ws-set').classList.toggle('has-dot', !!_homeVer.updateAvailable);
1348
+ document.getElementById('ws-menu-dot').classList.toggle('on', !!_homeVer.updateAvailable);
1349
+ document.getElementById('rail-set').classList.toggle('has-dot', !!_homeVer.updateAvailable);
1213
1350
  } catch {}
1214
1351
  }
1215
1352
  // 等升级落地:轮询 /api/version(重启/停服期间连不上是正常的)。成功 = 版本变了(且等于目标);
@@ -1340,7 +1477,12 @@
1340
1477
 
1341
1478
  // 视图状态:分组方式 + 标签筛选
1342
1479
  let _allSessions = [];
1343
- let _groupMode = localStorage.getItem('myhi_group_mode') || 'time'; // time | project | tag
1480
+ // 电脑端工作台状态(声明放这里:列表渲染会用到,必须先于任何一次渲染)
1481
+ let _wsCur = null; // 右侧当前打开的任务
1482
+ let _wsPending = new URLSearchParams(location.search).get('s'); // 刷新后回到上次的任务
1483
+ const _wsFrames = new Map(); // id → iframe,按最近使用排序(最后一个最新)
1484
+ // 电脑端工作台默认按项目分组(任务多了按项目找最快),手机默认按最近;用户切过就按用户的
1485
+ let _groupMode = localStorage.getItem('myhi_group_mode') || (matchMedia('(min-width: 1024px)').matches ? 'project' : 'time'); // time | project | tag
1344
1486
  let _tagFilter = null; // 仅 tag 模式下:只看某标签
1345
1487
  let _search = ''; // 会话搜索关键词(标题/首条需求/目录/标签)
1346
1488
  let _draftCounts = {}; // { sessionId: 待发草稿数 },给卡片打角标
@@ -1388,17 +1530,17 @@
1388
1530
  ? `<div style="font-size:0.66rem;color:#d29922;margin-top:0.2rem">📥 ${nDraft} 条待发草稿</div>`
1389
1531
  : '';
1390
1532
  return `
1391
- <div class="card" data-id="${safeId}" data-title="${safeTitle}">
1533
+ <div class="card${s.id === _wsCur ? ' active' : ''}" data-id="${safeId}" data-title="${safeTitle}">
1392
1534
  <div class="avatar" style="background:${color}">${letter}</div>
1393
1535
  <div class="card-body">
1394
- <div class="card-title">${safeTitle}</div>
1536
+ <div class="card-title-row"><div class="card-title">${safeTitle}</div>${tags ? `<div class="card-tags">${tags}</div>` : ''}</div>
1395
1537
  <div class="card-sub">${sub}</div>
1396
1538
  ${gist}
1397
1539
  ${draftChip}
1398
- ${tags ? `<div class="card-tags" style="display:flex;flex-wrap:wrap;gap:0.3rem;margin-top:0.3rem">${tags}</div>` : ''}
1399
1540
  </div>
1400
1541
  <div class="card-right">
1401
- <div class="status-dot ${s.alive ? 'alive' : 'dead'}"></div>
1542
+ ${!s.alive ? '<div class="st st-dead" title="会话进程已退出,点进去会自动恢复">已停止</div>'
1543
+ : s.busy ? '<div class="st st-busy" title="AI 正在执行">执行中</div>' : ''}
1402
1544
  ${s.viewers > 1 ? `<div class="viewers">👁 ${s.viewers}</div>` : ''}
1403
1545
  </div>
1404
1546
  </div>`;
@@ -1490,6 +1632,7 @@
1490
1632
  html = items.map(cardHTML).join('');
1491
1633
  }
1492
1634
  list.innerHTML = html;
1635
+ wsAfterRender();
1493
1636
 
1494
1637
  // 组头点击折叠/展开(项目多了列表很长)
1495
1638
  list.querySelectorAll('.group-header[data-gkey]').forEach(h => {
@@ -1576,6 +1719,7 @@
1576
1719
  if (first && !creating) showFabHint();
1577
1720
  };
1578
1721
  function showFabHint() {
1722
+ if (wsOn()) return; // 工作台里「+ 新建任务」就在左上角,不用再指右下角
1579
1723
  const fab = document.getElementById('fab'), tip = document.getElementById('fab-tip');
1580
1724
  fab.classList.add('hint'); tip.classList.add('show');
1581
1725
  const done = () => { fab.classList.remove('hint'); tip.classList.remove('show'); document.removeEventListener('pointerdown', done, true); };
@@ -1662,10 +1806,115 @@
1662
1806
  };
1663
1807
 
1664
1808
  // ── Navigation ──────────────────────────────────────────────
1809
+ // 电脑端工作台:在右侧打开;手机/窄屏:进会话页
1665
1810
  function openSession(id) {
1811
+ if (wsOn()) return wsOpen(id);
1666
1812
  window.location.href = `/terminal/${id}`;
1667
1813
  }
1668
1814
 
1815
+ // ── 电脑端工作台(≥1024px):左侧任务栏 + 右侧嵌入会话页(/terminal/:id?embed=ws)──
1816
+ // 最近打开的 5 个任务的会话页留在后台(隐藏不销毁),来回切换不用重载,滚动位置/没发的输入都还在。
1817
+ const WS_MQ = matchMedia('(min-width: 1024px)');
1818
+ const WS_KEEP = 5;
1819
+ function wsOn() { return document.body.classList.contains('ws'); }
1820
+ function wsOpen(id) {
1821
+ _wsCur = id;
1822
+ let f = _wsFrames.get(id);
1823
+ if (f) _wsFrames.delete(id);
1824
+ else {
1825
+ f = document.createElement('iframe');
1826
+ f.src = '/terminal/' + encodeURIComponent(id) + '?embed=ws';
1827
+ f.setAttribute('allow', 'microphone; camera; clipboard-read; clipboard-write');
1828
+ document.getElementById('ws-main').appendChild(f);
1829
+ }
1830
+ _wsFrames.set(id, f);
1831
+ while (_wsFrames.size > WS_KEEP) { const [oid, of] = _wsFrames.entries().next().value; _wsFrames.delete(oid); of.remove(); }
1832
+ for (const [k, fr] of _wsFrames) fr.style.display = k === id ? '' : 'none';
1833
+ document.getElementById('ws-welcome').style.display = 'none';
1834
+ history.replaceState(null, '', '/?s=' + encodeURIComponent(id));
1835
+ const s = _allSessions.find(x => x.id === id);
1836
+ document.title = (s?.title || 'myhi') + ' — myhi';
1837
+ wsMarkActive();
1838
+ setTimeout(() => f.contentWindow?.focus(), 50);
1839
+ }
1840
+ function wsShowWelcome() {
1841
+ _wsCur = null;
1842
+ for (const fr of _wsFrames.values()) fr.style.display = 'none';
1843
+ document.getElementById('ws-welcome').style.display = '';
1844
+ history.replaceState(null, '', '/');
1845
+ document.title = 'myhi';
1846
+ wsMarkActive();
1847
+ }
1848
+ function wsMarkActive() {
1849
+ document.querySelectorAll('#list .card').forEach(c => c.classList.toggle('active', c.dataset.id === _wsCur));
1850
+ document.querySelectorAll('#rail-list .ra').forEach(c => c.classList.toggle('active', c.dataset.id === _wsCur));
1851
+ }
1852
+ // 收起后的窄栏:最近的任务头像(最多 20 个,按最近活动),执行中带跳点,点了直接切
1853
+ const RAIL_MAX = 20;
1854
+ function renderRail() {
1855
+ const el = document.getElementById('rail-list');
1856
+ if (!el) return;
1857
+ const items = [..._allSessions].sort((a, b) => sessionTime(b) - sessionTime(a));
1858
+ el.innerHTML = items.slice(0, RAIL_MAX).map(s =>
1859
+ `<div class="ra${s.id === _wsCur ? ' active' : ''}${s.busy ? ' busy' : ''}" data-id="${esc(s.id)}" style="background:${avatarColor(s.title)}"` +
1860
+ ` data-tip="${esc(s.title || '')}${s.busy ? ' · 执行中' : !s.alive ? ' · 已停止' : ''}">${esc((s.title || '?')[0].toUpperCase())}</div>`).join('') +
1861
+ (items.length > RAIL_MAX ? `<div class="rail-more" data-tip="展开任务栏看全部 ${items.length} 个任务" onclick="wsSetCollapsed(false)">+${items.length - RAIL_MAX}</div>` : '');
1862
+ el.querySelectorAll('.ra').forEach(a => a.onclick = () => wsOpen(a.dataset.id));
1863
+ }
1864
+ // 窄栏即时悬停提示
1865
+ document.addEventListener('mouseover', (e) => {
1866
+ const t = e.target.closest('#ws-rail [data-tip]'), tip = document.getElementById('ws-tip');
1867
+ if (!t) { tip.style.display = 'none'; return; }
1868
+ const r = t.getBoundingClientRect();
1869
+ tip.textContent = t.dataset.tip;
1870
+ tip.style.top = (r.top + r.height / 2 - 13) + 'px';
1871
+ tip.style.display = 'block';
1872
+ });
1873
+ // 每次列表重绘后:刷新前打开的任务等列表到了再打开;当前任务被删了就回欢迎页、回收它的会话页
1874
+ function wsAfterRender() {
1875
+ if (!wsOn()) return;
1876
+ if (_wsPending && _allSessions.some(x => x.id === _wsPending)) { const id = _wsPending; _wsPending = null; wsOpen(id); return; }
1877
+ for (const [k, fr] of _wsFrames) if (!_allSessions.some(x => x.id === k)) { fr.remove(); _wsFrames.delete(k); if (k === _wsCur) wsShowWelcome(); }
1878
+ renderRail();
1879
+ wsMarkActive();
1880
+ }
1881
+ // ⚙ 菜单:从底部 / 窄栏的 ⚙ 往上弹,点外面或 Esc 关闭
1882
+ window.toggleWsMenu = function(e) {
1883
+ e.stopPropagation();
1884
+ const m = document.getElementById('ws-menu');
1885
+ if (m.classList.contains('open')) return closeWsMenu();
1886
+ const r = e.currentTarget.getBoundingClientRect();
1887
+ m.classList.add('open');
1888
+ // 右边缘对齐 ⚙、往左展开,留在任务栏范围内,不压右侧会话的输入框(窄栏的 ⚙ 在最左,就贴左边)
1889
+ m.style.left = Math.max(8, r.right - m.offsetWidth) + 'px';
1890
+ m.style.bottom = (innerHeight - r.top + 6) + 'px';
1891
+ document.getElementById('ws-set').classList.add('on');
1892
+ };
1893
+ function closeWsMenu() { document.getElementById('ws-menu').classList.remove('open'); document.getElementById('ws-set').classList.remove('on'); }
1894
+ window.wsMenuGo = function(fn) { closeWsMenu(); fn(); };
1895
+ document.addEventListener('click', (e) => { if (!e.target.closest('#ws-menu')) closeWsMenu(); });
1896
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeWsMenu(); });
1897
+
1898
+ window.wsSetCollapsed = function(on) {
1899
+ document.body.classList.toggle('ws-collapsed', on);
1900
+ localStorage.setItem('myhi_ws_collapsed', on ? '1' : '');
1901
+ document.getElementById('ws-tip').style.display = 'none';
1902
+ if (on) renderRail();
1903
+ };
1904
+ function applyWs() {
1905
+ const on = WS_MQ.matches && !new URLSearchParams(location.search).get('embed');
1906
+ document.body.classList.toggle('ws', on);
1907
+ document.body.classList.toggle('ws-collapsed', on && !!localStorage.getItem('myhi_ws_collapsed'));
1908
+ // 「AI 引擎没装」提示:工作台里放到右侧欢迎页,手机上放列表上方
1909
+ const banner = document.getElementById('ai-banner');
1910
+ const target = on ? document.getElementById('ws-welcome') : document.getElementById('view-controls');
1911
+ if (on) target.appendChild(banner); else target.parentNode.insertBefore(banner, target);
1912
+ if (!on && _wsCur) { location.href = '/terminal/' + encodeURIComponent(_wsCur); return; } // 窗口拉窄:直接进当前任务的会话页
1913
+ fitHeader();
1914
+ }
1915
+ WS_MQ.addEventListener('change', applyWs);
1916
+ applyWs();
1917
+
1669
1918
  // ── 会话体检 ────────────────────────────────────────────────
1670
1919
  window.openDoctor = function() {
1671
1920
  document.getElementById('doctor-menu').style.display = 'block';