@wendongfly/myhi 1.3.110 → 1.3.112

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.
Files changed (2) hide show
  1. package/dist/grid.html +179 -35
  2. package/package.json +1 -1
package/dist/grid.html CHANGED
@@ -45,6 +45,8 @@
45
45
  .pane { display:flex; flex-direction:column; min-height:260px; min-width:0; background:var(--surface); border:1px solid var(--border); border-radius:10px; overflow:hidden; }
46
46
  .pane-head { flex-shrink:0; display:flex; align-items:center; gap:0.4rem; padding:0.3rem 0.4rem; background:var(--surface2); border-bottom:1px solid var(--border); }
47
47
  .pane-head select { flex:1; min-width:0; background:var(--bg); color:var(--text); border:1px solid var(--border); border-radius:6px; padding:0.28rem 0.4rem; font-size:0.78rem; }
48
+ .pane-head .pane-picker { flex:1; min-width:0; display:flex; align-items:center; justify-content:space-between; gap:0.3rem; text-align:left; }
49
+ #tp-list [data-id]:hover { background:var(--surface2) !important; }
48
50
  .pane-head button { background:var(--bg); border:1px solid var(--border); color:var(--muted); border-radius:6px; padding:0.2rem 0.45rem; font-size:0.8rem; cursor:pointer; line-height:1; }
49
51
  .pane-head button:hover { color:var(--text); border-color:var(--blue2); }
50
52
  .pane-body { flex:1; min-height:0; position:relative; }
@@ -67,10 +69,48 @@
67
69
  <span class="spacer"></span>
68
70
  <button class="bar-btn" onclick="refreshSessions(true)" title="刷新任务列表">↻</button>
69
71
  <button class="bar-btn" onclick="addPane('')" title="加一个空窗格,从下拉选已有任务">+ 窗格</button>
70
- <button class="bar-btn" style="border-color:var(--green);color:var(--green)" onclick="newTask()" title="新建一个任务并放进新窗格">🆕 新建任务</button>
72
+ <button class="bar-btn" style="border-color:var(--green);color:var(--green)" onclick="openNewTask()" title="新建任务(和首页一致:预设/名称/命令/目录)">🆕 新建任务</button>
71
73
  </div>
72
74
  <div id="grid"></div>
73
- <div id="empty-hint" style="display:none">点「+ 添加任务」把多个任务并排看。<br>拉宽窗口自动多几格,或用上方按钮锁定布局。</div>
75
+ <div id="empty-hint" style="display:none">点「+ 窗格」或「🆕 新建任务」把多个任务并排看。<br>拉宽窗口自动多几格,或用上方按钮锁定布局。</div>
76
+
77
+ <!-- 任务选择弹窗(搜索 + 分组 + 标签)-->
78
+ <div id="task-picker" style="display:none;position:fixed;inset:0;z-index:100">
79
+ <div style="position:absolute;inset:0;background:rgba(0,0,0,0.5)" onclick="closeTaskPicker()"></div>
80
+ <div style="position:relative;max-width:540px;width:92%;margin:6vh auto 0;background:var(--surface);border:1px solid var(--border);border-radius:12px;max-height:82vh;display:flex;flex-direction:column;box-shadow:0 16px 48px rgba(0,0,0,0.45)">
81
+ <div style="padding:0.6rem 0.8rem;border-bottom:1px solid var(--border);display:flex;gap:0.5rem;align-items:center">
82
+ <input id="tp-search" placeholder="搜索任务(名称 / 目录 / 标签)…" autocomplete="off" style="flex:1;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:7px;padding:0.42rem 0.6rem;font-size:0.82rem">
83
+ <button onclick="closeTaskPicker()" style="background:var(--bg);border:1px solid var(--border);color:var(--muted);border-radius:7px;padding:0.38rem 0.6rem;cursor:pointer">✕</button>
84
+ </div>
85
+ <div style="padding:0.45rem 0.8rem;display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;border-bottom:1px solid var(--border)">
86
+ <div class="seg" id="tp-group">
87
+ <button data-g="time">最近</button><button data-g="project">项目</button><button data-g="tag">标签</button>
88
+ </div>
89
+ <span id="tp-tagbar" style="display:none;gap:0.3rem;flex-wrap:wrap;align-items:center"></span>
90
+ </div>
91
+ <div id="tp-list" style="flex:1;overflow-y:auto;padding:0.4rem 0.6rem"></div>
92
+ </div>
93
+ </div>
94
+
95
+ <!-- 新建任务面板(和首页「新建终端会话」一致)-->
96
+ <div id="new-task" style="display:none;position:fixed;inset:0;z-index:100">
97
+ <div style="position:absolute;inset:0;background:rgba(0,0,0,0.5)" onclick="closeNewTask()"></div>
98
+ <div style="position:relative;max-width:460px;width:92%;margin:8vh auto 0;background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:1rem;box-shadow:0 16px 48px rgba(0,0,0,0.45)">
99
+ <div style="font-weight:600;margin-bottom:0.7rem">新建任务</div>
100
+ <div id="nt-presets" style="display:flex;gap:0.4rem;margin-bottom:0.7rem;flex-wrap:wrap"></div>
101
+ <label style="font-size:0.72rem;color:var(--muted)">名称 <span style="color:var(--red)">必填</span></label>
102
+ <input id="nt-title" placeholder="请输入任务名称" autocomplete="off" style="width:100%;box-sizing:border-box;margin:0.2rem 0 0.6rem;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:7px;padding:0.45rem 0.6rem;font-size:0.85rem">
103
+ <label style="font-size:0.72rem;color:var(--muted)">启动命令(可选,终端类型用)</label>
104
+ <input id="nt-cmd" placeholder="留空仅打开 shell" autocomplete="off" style="width:100%;box-sizing:border-box;margin:0.2rem 0 0.6rem;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:7px;padding:0.45rem 0.6rem;font-size:0.82rem;font-family:monospace">
105
+ <label style="font-size:0.72rem;color:var(--muted)">工作目录(可选)</label>
106
+ <input id="nt-cwd" placeholder="留空使用默认目录" autocomplete="off" style="width:100%;box-sizing:border-box;margin:0.2rem 0 0.4rem;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:7px;padding:0.45rem 0.6rem;font-size:0.82rem">
107
+ <div id="nt-recent" style="display:flex;flex-wrap:wrap;gap:0.3rem;margin-bottom:0.7rem"></div>
108
+ <div style="display:flex;gap:0.5rem">
109
+ <button onclick="closeNewTask()" style="flex:1;background:var(--bg);border:1px solid var(--border);color:var(--muted);border-radius:8px;padding:0.5rem;cursor:pointer">取消</button>
110
+ <button onclick="submitNewTask()" style="flex:2;background:var(--green);border:none;color:#fff;border-radius:8px;padding:0.5rem;cursor:pointer;font-weight:600">创建</button>
111
+ </div>
112
+ </div>
113
+ </div>
74
114
 
75
115
  <script>
76
116
  const LS_KEY = 'myhi_grid_v1';
@@ -125,39 +165,39 @@
125
165
  }));
126
166
 
127
167
  // ── 窗格 ──
128
- function buildSelect(sid) {
129
- const sel = document.createElement('select');
130
- const opt0 = document.createElement('option'); opt0.value=''; opt0.textContent='— 选择任务 —'; sel.appendChild(opt0);
131
- for (const s of sessions) { const o = document.createElement('option'); o.value = sId(s); o.textContent = sName(s); sel.appendChild(o); }
132
- sel.value = sid || '';
133
- return sel;
168
+ function updatePickerLabel(pane) {
169
+ const s = pane._sid ? sessById(pane._sid) : null;
170
+ pane._picker.innerHTML = s
171
+ ? `<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(sName(s))}</span><span style="color:var(--dim);flex-shrink:0">▾</span>`
172
+ : `<span style="color:var(--muted)">+ 选择任务</span><span style="color:var(--dim);flex-shrink:0">▾</span>`;
134
173
  }
135
174
  function addPane(sid, skipSave) {
136
175
  const pane = document.createElement('div'); pane.className = 'pane'; pane._sid = sid || '';
137
176
  const head = document.createElement('div'); head.className = 'pane-head';
138
- const sel = buildSelect(sid);
177
+ const picker = document.createElement('button'); picker.className = 'pane-picker';
139
178
  const full = document.createElement('button'); full.textContent='↗'; full.title='在新标签页打开';
140
179
  const close = document.createElement('button'); close.textContent='✕'; close.title='关闭此格'; close.style.color='var(--red)';
141
- head.appendChild(sel); head.appendChild(full); head.appendChild(close);
180
+ head.appendChild(picker); head.appendChild(full); head.appendChild(close);
142
181
  const body = document.createElement('div'); body.className = 'pane-body';
143
- const empty = document.createElement('div'); empty.className='pane-empty'; empty.textContent='从上方下拉选一个任务'; body.appendChild(empty);
182
+ const empty = document.createElement('div'); empty.className='pane-empty'; empty.textContent='点上方「选择任务」挑一个(可搜索/分组/按标签)'; body.appendChild(empty);
144
183
  pane.appendChild(head); pane.appendChild(body);
145
- pane._sel = sel; pane._body = body; pane._empty = empty; pane._iframe = null;
184
+ pane._picker = picker; pane._body = body; pane._empty = empty; pane._iframe = null;
146
185
 
147
- sel.addEventListener('change', () => setPaneSession(pane, sel.value));
186
+ picker.addEventListener('click', () => openTaskPicker(pane));
148
187
  full.addEventListener('click', () => { if (pane._sid) window.open('/terminal/' + pane._sid, '_blank'); });
149
188
  close.addEventListener('click', () => removePane(pane));
150
189
 
151
190
  gridEl.appendChild(pane);
152
191
  paneEls.push(pane);
153
- if (sid) setPaneSession(pane, sid, true);
192
+ updatePickerLabel(pane);
193
+ if (sid) setPaneSession(pane, sid);
154
194
  if (!skipSave) { saveState(); }
155
195
  applyLayout();
156
196
  }
157
- function setPaneSession(pane, sid, keepSelect) {
197
+ function setPaneSession(pane, sid) {
158
198
  pane._sid = sid || '';
159
199
  pane._key = sid ? keyOf(sessById(sid)) : null; // 记住稳定标识,供重启后找回
160
- if (!keepSelect) pane._sel.value = sid || '';
200
+ updatePickerLabel(pane);
161
201
  // 只重建这一格的 iframe,其它格不受影响
162
202
  if (pane._iframe) { pane._iframe.remove(); pane._iframe = null; }
163
203
  if (sid) {
@@ -176,35 +216,139 @@
176
216
  }
177
217
 
178
218
  // ── 任务列表 ──
179
- async function refreshSessions(rebuildSelects) {
219
+ async function refreshSessions(rebuild) {
180
220
  try {
181
221
  const r = await fetch('/api/sessions'); if (!r.ok) return;
182
222
  const data = await r.json();
183
223
  sessions = (Array.isArray(data) ? data : (data.sessions || [])).filter(s => sId(s));
184
224
  } catch {}
185
- if (rebuildSelects) {
186
- for (const pane of paneEls) {
187
- const cur = pane._sid;
188
- const fresh = buildSelect(cur);
189
- fresh.addEventListener('change', () => setPaneSession(pane, fresh.value));
190
- pane._sel.replaceWith(fresh); pane._sel = fresh;
225
+ if (rebuild) for (const pane of paneEls) updatePickerLabel(pane);
226
+ }
227
+
228
+ // ── 任务选择弹窗(搜索 + 最近/项目/标签分组 + 标签过滤,借鉴首页)──
229
+ let _pickTarget = null;
230
+ let _tpGroup = localStorage.getItem('myhi_grid_group') || 'time';
231
+ let _tpTag = null;
232
+ function sessionTime(s){ return s.lastActivityAt || s.createdAt || 0; }
233
+ window.openTaskPicker = function(pane) {
234
+ _pickTarget = pane;
235
+ document.getElementById('task-picker').style.display = '';
236
+ document.getElementById('tp-search').value = '';
237
+ refreshSessions(false).then(renderPicker);
238
+ renderPicker();
239
+ setTimeout(() => document.getElementById('tp-search').focus(), 30);
240
+ };
241
+ window.closeTaskPicker = function(){ document.getElementById('task-picker').style.display='none'; _pickTarget=null; };
242
+ function pushMap(m,k,v){ if(!m.has(k)) m.set(k,[]); m.get(k).push(v); }
243
+ function renderPicker() {
244
+ document.querySelectorAll('#tp-group button').forEach(b => b.classList.toggle('active', b.dataset.g === _tpGroup));
245
+ const tagbar = document.getElementById('tp-tagbar');
246
+ if (_tpGroup === 'tag') {
247
+ const allTags = [...new Set(sessions.flatMap(s => s.tags || []))].sort();
248
+ const chip = (label,val,on) => `<button data-tag="${val==null?'':esc(val)}" style="border:1px solid ${on?'var(--blue2)':'var(--border)'};background:${on?'var(--blue2)':'var(--bg)'};color:${on?'#fff':'var(--muted)'};border-radius:999px;font-size:0.7rem;padding:0.12rem 0.5rem;cursor:pointer">${esc(label)}</button>`;
249
+ tagbar.innerHTML = chip('全部',null,!_tpTag) + allTags.map(t=>chip(t,t,_tpTag===t)).join('');
250
+ tagbar.style.display = 'flex';
251
+ } else { tagbar.style.display='none'; }
252
+
253
+ const kw = (document.getElementById('tp-search').value||'').trim().toLowerCase();
254
+ let items = sessions.filter(s => !kw || sName(s).toLowerCase().includes(kw) || (s.cwd||'').toLowerCase().includes(kw) || (s.tags||[]).join(' ').toLowerCase().includes(kw));
255
+ items.sort((a,b)=>sessionTime(b)-sessionTime(a));
256
+ const itemHtml = (s) => {
257
+ const cur = _pickTarget && _pickTarget._sid === sId(s);
258
+ const tags = (s.tags||[]).map(t=>`<span style="background:var(--surface2);color:var(--muted);border-radius:4px;font-size:0.62rem;padding:0.02rem 0.3rem">${esc(t)}</span>`).join(' ');
259
+ return `<div data-id="${esc(sId(s))}" style="padding:0.45rem 0.5rem;border-radius:7px;cursor:pointer;border:1px solid ${cur?'var(--blue2)':'transparent'};background:${cur?'var(--surface2)':'transparent'}">
260
+ <div style="font-size:0.82rem;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(sName(s))} ${tags}</div>
261
+ <div style="font-size:0.66rem;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(s.cwd||'(无目录)')}</div>
262
+ </div>`;
263
+ };
264
+ const header = (t) => `<div style="color:var(--muted);font-size:0.7rem;font-weight:600;margin:0.6rem 0.2rem 0.25rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(t)}</div>`;
265
+ let html = '';
266
+ if (_tpGroup === 'project') {
267
+ const groups = new Map();
268
+ for (const s of items) pushMap(groups, s.cwd || '(无目录)', s);
269
+ const ordered = [...groups.entries()].sort((a,b)=>sessionTime(b[1][0])-sessionTime(a[1][0]));
270
+ for (const [k,arr] of ordered) html += header(k) + arr.map(itemHtml).join('');
271
+ } else if (_tpGroup === 'tag') {
272
+ if (_tpTag) { html += items.filter(s => (s.tags||[]).includes(_tpTag)).map(itemHtml).join(''); }
273
+ else {
274
+ const tagged = new Map(); const untagged = [];
275
+ for (const s of items) { const ts=s.tags||[]; if(!ts.length) untagged.push(s); for(const t of ts) pushMap(tagged,t,s); }
276
+ const ordered=[...tagged.entries()].sort((a,b)=>sessionTime(b[1][0])-sessionTime(a[1][0]));
277
+ for (const [t,arr] of ordered) html += header('# '+t) + arr.map(itemHtml).join('');
278
+ if (untagged.length) html += header('无标签') + untagged.map(itemHtml).join('');
191
279
  }
192
- }
280
+ } else { html = items.map(itemHtml).join(''); }
281
+ document.getElementById('tp-list').innerHTML = html || '<div style="color:var(--muted);text-align:center;padding:1.5rem 0;font-size:0.8rem">没有匹配的任务</div>';
193
282
  }
283
+ // 事件委托:选任务 / 选标签 / 切分组 / 搜索
284
+ document.getElementById('tp-list').addEventListener('click', (e) => {
285
+ const el = e.target.closest('[data-id]'); if (!el) return;
286
+ if (_pickTarget) setPaneSession(_pickTarget, el.dataset.id);
287
+ closeTaskPicker();
288
+ });
289
+ document.getElementById('tp-tagbar').addEventListener('click', (e) => {
290
+ const el = e.target.closest('[data-tag]'); if (!el) return;
291
+ _tpTag = el.dataset.tag || null; renderPicker();
292
+ });
293
+ document.querySelectorAll('#tp-group button').forEach(b => b.addEventListener('click', () => {
294
+ _tpGroup = b.dataset.g; _tpTag = null; localStorage.setItem('myhi_grid_group', _tpGroup); renderPicker();
295
+ }));
296
+ document.getElementById('tp-search').addEventListener('input', renderPicker);
194
297
 
195
- // ── 新建任务(走 socket create-agent,和首页一致)──
298
+ // ── 新建任务(和首页「新建终端会话」一致:预设 + 名称 + 命令 + 目录 + 最近目录)──
196
299
  const socket = io();
197
- window.newTask = function() {
198
- const title = (prompt('新任务名称:') || '').trim();
199
- if (!title) return;
200
- socket.emit('create-agent', { title }, (res) => {
201
- if (res && res.ok && res.session) {
202
- refreshSessions(true).then(() => addPane(res.session.id));
203
- } else {
204
- alert('创建失败:' + ((res && res.error) || '未知错误'));
205
- }
206
- });
300
+ const NT_PRESETS = [
301
+ { icon:'🤖', label:'Claude', title:'claude', type:'agent', cmd:'' },
302
+ { icon:'✨', label:'Gemini', title:'gemini', type:'pty', cmd:'gemini' },
303
+ { icon:'🐚', label:'Shell', title:'shell', type:'pty', cmd:'' },
304
+ ];
305
+ let _ntPreset = 0;
306
+ const RECENT_DIRS_KEY = 'myhi_recent_dirs'; // 和首页共用
307
+ function ntRecentDirs(){ try { return JSON.parse(localStorage.getItem(RECENT_DIRS_KEY)) || {}; } catch { return {}; } }
308
+ function ntRecordDir(dir){ if(!dir) return; const d=ntRecentDirs(); d[dir]=(d[dir]||0)+1; const s=Object.entries(d).sort((a,b)=>b[1]-a[1]).slice(0,5); localStorage.setItem(RECENT_DIRS_KEY, JSON.stringify(Object.fromEntries(s))); }
309
+ function ntRenderPresets(){
310
+ document.getElementById('nt-presets').innerHTML = NT_PRESETS.map((p,i)=>`<button data-i="${i}" style="flex:1;min-width:5rem;display:flex;align-items:center;justify-content:center;gap:0.3rem;border:1px solid ${i===_ntPreset?'var(--blue2)':'var(--border)'};background:${i===_ntPreset?'var(--blue2)':'var(--bg)'};color:${i===_ntPreset?'#fff':'var(--text2)'};border-radius:8px;padding:0.4rem;font-size:0.8rem;cursor:pointer">${p.icon} ${esc(p.label)}</button>`).join('');
311
+ }
312
+ function ntRenderRecent(){
313
+ const dirs = Object.entries(ntRecentDirs()).sort((a,b)=>b[1]-a[1]);
314
+ document.getElementById('nt-recent').innerHTML = dirs.map(([p])=>`<button data-dir="${esc(p)}" style="background:var(--surface2);border:1px solid var(--border);color:var(--muted);border-radius:6px;font-size:0.68rem;padding:0.12rem 0.4rem;cursor:pointer;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(p)}</button>`).join('');
315
+ }
316
+ window.openNewTask = function(){
317
+ _ntPreset = 0;
318
+ document.getElementById('new-task').style.display = '';
319
+ document.getElementById('nt-title').value = '';
320
+ document.getElementById('nt-cmd').value = '';
321
+ document.getElementById('nt-cwd').value = '';
322
+ ntRenderPresets(); ntRenderRecent();
323
+ setTimeout(()=>document.getElementById('nt-title').focus(), 30);
324
+ };
325
+ window.closeNewTask = function(){ document.getElementById('new-task').style.display='none'; };
326
+ window.submitNewTask = function(){
327
+ const title = document.getElementById('nt-title').value.trim();
328
+ if (!title) { document.getElementById('nt-title').focus(); return; }
329
+ const cmd = document.getElementById('nt-cmd').value.trim() || undefined;
330
+ const cwd = document.getElementById('nt-cwd').value.trim() || undefined;
331
+ if (cwd) ntRecordDir(cwd);
332
+ const preset = NT_PRESETS[_ntPreset];
333
+ const done = (res) => {
334
+ if (res && res.ok && res.session) { closeNewTask(); refreshSessions(true).then(()=>addPane(res.session.id)); }
335
+ else alert('创建失败:' + ((res && res.error) || '未知错误'));
336
+ };
337
+ if (preset.type === 'agent') socket.emit('create-agent', { title, cwd }, done);
338
+ else socket.emit('create', { title, initCmd: cmd, cwd }, done);
207
339
  };
340
+ document.getElementById('nt-presets').addEventListener('click', (e) => {
341
+ const b = e.target.closest('[data-i]'); if (!b) return;
342
+ _ntPreset = +b.dataset.i; const p = NT_PRESETS[_ntPreset];
343
+ const t = document.getElementById('nt-title');
344
+ if (!t.value.trim() || NT_PRESETS.some(x => x.title === t.value.trim())) t.value = p.title;
345
+ document.getElementById('nt-cmd').value = p.cmd || '';
346
+ ntRenderPresets();
347
+ });
348
+ document.getElementById('nt-recent').addEventListener('click', (e) => {
349
+ const b = e.target.closest('[data-dir]'); if (!b) return;
350
+ document.getElementById('nt-cwd').value = b.dataset.dir;
351
+ });
208
352
 
209
353
  // ── 初始化 ──
210
354
  (async function init() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wendongfly/myhi",
3
- "version": "1.3.110",
3
+ "version": "1.3.112",
4
4
  "description": "Web-based terminal sharing with chat UI — control your terminal from phone via LAN/Tailscale",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",