@wendongfly/myhi 1.3.110 → 1.3.111

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 +106 -23
  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; }
@@ -70,7 +72,25 @@
70
72
  <button class="bar-btn" style="border-color:var(--green);color:var(--green)" onclick="newTask()" 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>
74
94
 
75
95
  <script>
76
96
  const LS_KEY = 'myhi_grid_v1';
@@ -125,39 +145,39 @@
125
145
  }));
126
146
 
127
147
  // ── 窗格 ──
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;
148
+ function updatePickerLabel(pane) {
149
+ const s = pane._sid ? sessById(pane._sid) : null;
150
+ pane._picker.innerHTML = s
151
+ ? `<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(sName(s))}</span><span style="color:var(--dim);flex-shrink:0">▾</span>`
152
+ : `<span style="color:var(--muted)">+ 选择任务</span><span style="color:var(--dim);flex-shrink:0">▾</span>`;
134
153
  }
135
154
  function addPane(sid, skipSave) {
136
155
  const pane = document.createElement('div'); pane.className = 'pane'; pane._sid = sid || '';
137
156
  const head = document.createElement('div'); head.className = 'pane-head';
138
- const sel = buildSelect(sid);
157
+ const picker = document.createElement('button'); picker.className = 'pane-picker';
139
158
  const full = document.createElement('button'); full.textContent='↗'; full.title='在新标签页打开';
140
159
  const close = document.createElement('button'); close.textContent='✕'; close.title='关闭此格'; close.style.color='var(--red)';
141
- head.appendChild(sel); head.appendChild(full); head.appendChild(close);
160
+ head.appendChild(picker); head.appendChild(full); head.appendChild(close);
142
161
  const body = document.createElement('div'); body.className = 'pane-body';
143
- const empty = document.createElement('div'); empty.className='pane-empty'; empty.textContent='从上方下拉选一个任务'; body.appendChild(empty);
162
+ const empty = document.createElement('div'); empty.className='pane-empty'; empty.textContent='点上方「选择任务」挑一个(可搜索/分组/按标签)'; body.appendChild(empty);
144
163
  pane.appendChild(head); pane.appendChild(body);
145
- pane._sel = sel; pane._body = body; pane._empty = empty; pane._iframe = null;
164
+ pane._picker = picker; pane._body = body; pane._empty = empty; pane._iframe = null;
146
165
 
147
- sel.addEventListener('change', () => setPaneSession(pane, sel.value));
166
+ picker.addEventListener('click', () => openTaskPicker(pane));
148
167
  full.addEventListener('click', () => { if (pane._sid) window.open('/terminal/' + pane._sid, '_blank'); });
149
168
  close.addEventListener('click', () => removePane(pane));
150
169
 
151
170
  gridEl.appendChild(pane);
152
171
  paneEls.push(pane);
153
- if (sid) setPaneSession(pane, sid, true);
172
+ updatePickerLabel(pane);
173
+ if (sid) setPaneSession(pane, sid);
154
174
  if (!skipSave) { saveState(); }
155
175
  applyLayout();
156
176
  }
157
- function setPaneSession(pane, sid, keepSelect) {
177
+ function setPaneSession(pane, sid) {
158
178
  pane._sid = sid || '';
159
179
  pane._key = sid ? keyOf(sessById(sid)) : null; // 记住稳定标识,供重启后找回
160
- if (!keepSelect) pane._sel.value = sid || '';
180
+ updatePickerLabel(pane);
161
181
  // 只重建这一格的 iframe,其它格不受影响
162
182
  if (pane._iframe) { pane._iframe.remove(); pane._iframe = null; }
163
183
  if (sid) {
@@ -176,21 +196,84 @@
176
196
  }
177
197
 
178
198
  // ── 任务列表 ──
179
- async function refreshSessions(rebuildSelects) {
199
+ async function refreshSessions(rebuild) {
180
200
  try {
181
201
  const r = await fetch('/api/sessions'); if (!r.ok) return;
182
202
  const data = await r.json();
183
203
  sessions = (Array.isArray(data) ? data : (data.sessions || [])).filter(s => sId(s));
184
204
  } 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;
205
+ if (rebuild) for (const pane of paneEls) updatePickerLabel(pane);
206
+ }
207
+
208
+ // ── 任务选择弹窗(搜索 + 最近/项目/标签分组 + 标签过滤,借鉴首页)──
209
+ let _pickTarget = null;
210
+ let _tpGroup = localStorage.getItem('myhi_grid_group') || 'time';
211
+ let _tpTag = null;
212
+ function sessionTime(s){ return s.lastActivityAt || s.createdAt || 0; }
213
+ window.openTaskPicker = function(pane) {
214
+ _pickTarget = pane;
215
+ document.getElementById('task-picker').style.display = '';
216
+ document.getElementById('tp-search').value = '';
217
+ refreshSessions(false).then(renderPicker);
218
+ renderPicker();
219
+ setTimeout(() => document.getElementById('tp-search').focus(), 30);
220
+ };
221
+ window.closeTaskPicker = function(){ document.getElementById('task-picker').style.display='none'; _pickTarget=null; };
222
+ function pushMap(m,k,v){ if(!m.has(k)) m.set(k,[]); m.get(k).push(v); }
223
+ function renderPicker() {
224
+ document.querySelectorAll('#tp-group button').forEach(b => b.classList.toggle('active', b.dataset.g === _tpGroup));
225
+ const tagbar = document.getElementById('tp-tagbar');
226
+ if (_tpGroup === 'tag') {
227
+ const allTags = [...new Set(sessions.flatMap(s => s.tags || []))].sort();
228
+ 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>`;
229
+ tagbar.innerHTML = chip('全部',null,!_tpTag) + allTags.map(t=>chip(t,t,_tpTag===t)).join('');
230
+ tagbar.style.display = 'flex';
231
+ } else { tagbar.style.display='none'; }
232
+
233
+ const kw = (document.getElementById('tp-search').value||'').trim().toLowerCase();
234
+ let items = sessions.filter(s => !kw || sName(s).toLowerCase().includes(kw) || (s.cwd||'').toLowerCase().includes(kw) || (s.tags||[]).join(' ').toLowerCase().includes(kw));
235
+ items.sort((a,b)=>sessionTime(b)-sessionTime(a));
236
+ const itemHtml = (s) => {
237
+ const cur = _pickTarget && _pickTarget._sid === sId(s);
238
+ 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(' ');
239
+ 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'}">
240
+ <div style="font-size:0.82rem;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(sName(s))} ${tags}</div>
241
+ <div style="font-size:0.66rem;color:var(--dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(s.cwd||'(无目录)')}</div>
242
+ </div>`;
243
+ };
244
+ 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>`;
245
+ let html = '';
246
+ if (_tpGroup === 'project') {
247
+ const groups = new Map();
248
+ for (const s of items) pushMap(groups, s.cwd || '(无目录)', s);
249
+ const ordered = [...groups.entries()].sort((a,b)=>sessionTime(b[1][0])-sessionTime(a[1][0]));
250
+ for (const [k,arr] of ordered) html += header(k) + arr.map(itemHtml).join('');
251
+ } else if (_tpGroup === 'tag') {
252
+ if (_tpTag) { html += items.filter(s => (s.tags||[]).includes(_tpTag)).map(itemHtml).join(''); }
253
+ else {
254
+ const tagged = new Map(); const untagged = [];
255
+ for (const s of items) { const ts=s.tags||[]; if(!ts.length) untagged.push(s); for(const t of ts) pushMap(tagged,t,s); }
256
+ const ordered=[...tagged.entries()].sort((a,b)=>sessionTime(b[1][0])-sessionTime(a[1][0]));
257
+ for (const [t,arr] of ordered) html += header('# '+t) + arr.map(itemHtml).join('');
258
+ if (untagged.length) html += header('无标签') + untagged.map(itemHtml).join('');
191
259
  }
192
- }
260
+ } else { html = items.map(itemHtml).join(''); }
261
+ document.getElementById('tp-list').innerHTML = html || '<div style="color:var(--muted);text-align:center;padding:1.5rem 0;font-size:0.8rem">没有匹配的任务</div>';
193
262
  }
263
+ // 事件委托:选任务 / 选标签 / 切分组 / 搜索
264
+ document.getElementById('tp-list').addEventListener('click', (e) => {
265
+ const el = e.target.closest('[data-id]'); if (!el) return;
266
+ if (_pickTarget) setPaneSession(_pickTarget, el.dataset.id);
267
+ closeTaskPicker();
268
+ });
269
+ document.getElementById('tp-tagbar').addEventListener('click', (e) => {
270
+ const el = e.target.closest('[data-tag]'); if (!el) return;
271
+ _tpTag = el.dataset.tag || null; renderPicker();
272
+ });
273
+ document.querySelectorAll('#tp-group button').forEach(b => b.addEventListener('click', () => {
274
+ _tpGroup = b.dataset.g; _tpTag = null; localStorage.setItem('myhi_grid_group', _tpGroup); renderPicker();
275
+ }));
276
+ document.getElementById('tp-search').addEventListener('input', renderPicker);
194
277
 
195
278
  // ── 新建任务(走 socket create-agent,和首页一致)──
196
279
  const socket = io();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wendongfly/myhi",
3
- "version": "1.3.110",
3
+ "version": "1.3.111",
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",