@wendongfly/myhi 1.3.109 → 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.
package/dist/chat.html CHANGED
@@ -331,6 +331,7 @@
331
331
  <body>
332
332
 
333
333
  <div id="top-bar">
334
+ <button class="top-btn" onclick="location.href='/grid'" title="分屏工作台(多任务并排)" style="font-size:1.05rem;padding:0 0.35rem;color:var(--blue2)">▦</button>
334
335
  <div id="title-group">
335
336
  <div id="session-title">加载中...</div>
336
337
  <div id="session-cwd"></div>
@@ -341,7 +342,6 @@
341
342
  <button class="top-btn" onclick="showUsage()" title="用量" style="font-size:0.7rem;color:var(--muted)">用量</button>
342
343
  <button class="top-btn" onclick="openFilePanel()" title="文件" style="font-size:0.7rem;color:var(--muted)">文件</button>
343
344
  <button class="top-btn" onclick="openSwitchSheet()" title="切换会话" style="font-size:0.7rem;color:var(--muted)">切换</button>
344
- <button class="top-btn" onclick="location.href='/grid'" title="分屏工作台(多任务并排)" style="font-size:0.7rem;color:var(--muted)">▦ 分屏</button>
345
345
  <button class="top-btn" onclick="goBack()" title="返回">
346
346
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18l6-6-6-6"/></svg>
347
347
  </button>
@@ -365,11 +365,21 @@
365
365
  <span class="img-info">已添加图片,输入文字后发送</span>
366
366
  <button class="img-remove" onclick="clearPendingImage()" title="移除图片">&times;</button>
367
367
  </div>
368
+ <div id="file-preview" style="display:none;align-items:center;gap:0.5rem;padding:0.5rem 0.75rem;border-bottom:1px solid var(--surface2);background:var(--surface)">
369
+ <span style="font-size:1.1rem;flex-shrink:0">📎</span>
370
+ <span id="file-preview-name" style="flex:1;font-size:0.78rem;color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></span>
371
+ <span style="font-size:0.66rem;color:var(--muted);flex-shrink:0">临时·仅本会话分析</span>
372
+ <button class="img-remove" onclick="clearPendingFile()" title="移除文件">&times;</button>
373
+ </div>
368
374
  <textarea id="cmd-input" rows="1" placeholder="输入消息..." autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
369
375
  <div id="input-toolbar">
370
376
  <button class="tb-btn" id="btn-photo" onclick="openCamera()" title="拍照">
371
377
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"/><circle cx="12" cy="13" r="4"/></svg>
372
378
  </button>
379
+ <button class="tb-btn" id="btn-file" onclick="document.getElementById('file-input').click()" title="上传临时文件让 AI 分析(不进任务文件树,仅本会话)">
380
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/></svg>
381
+ </button>
382
+ <input type="file" id="file-input" style="display:none" onchange="uploadTempFile(this.files[0]); this.value='';">
373
383
  <span id="enter-hint" style="display:none;font-size:0.68rem;color:var(--muted);white-space:nowrap;align-self:center;margin-left:0.15rem"></span>
374
384
  <div class="spacer"></div>
375
385
  <button class="tb-btn" id="voice-btn" onclick="openVaSheet()" title="语音输入 / 需求助手">
@@ -2036,11 +2046,44 @@
2036
2046
  cmdInput.focus();
2037
2047
  }
2038
2048
 
2049
+ // ── 待发送临时文件(存 ~/.myhi/uploads/,**不进项目文件树**,仅本会话即时分析)──
2050
+ let _pendingFile = null; // { path, name }
2051
+ window.clearPendingFile = function() {
2052
+ _pendingFile = null;
2053
+ const fp = document.getElementById('file-preview');
2054
+ if (fp) fp.style.display = 'none';
2055
+ };
2056
+ window.uploadTempFile = async function(file) {
2057
+ if (!file) return;
2058
+ const fp = document.getElementById('file-preview');
2059
+ const nameEl = document.getElementById('file-preview-name');
2060
+ if (nameEl) nameEl.textContent = '上传中… ' + file.name;
2061
+ if (fp) fp.style.display = 'flex';
2062
+ try {
2063
+ const form = new FormData();
2064
+ form.append('file', file, file.name);
2065
+ const r = await fetch(`/upload-file?sessionId=${SESSION_ID}`, { method: 'POST', body: form });
2066
+ const d = await r.json();
2067
+ if (d.path) {
2068
+ _pendingFile = { path: d.path, name: d.name || file.name };
2069
+ if (nameEl) nameEl.textContent = _pendingFile.name;
2070
+ cmdInput.placeholder = '输入分析要求后发送(可留空)…';
2071
+ cmdInput.focus();
2072
+ } else {
2073
+ if (nameEl) nameEl.textContent = '上传失败: ' + (d.error || '未知');
2074
+ _pendingFile = null;
2075
+ }
2076
+ } catch (e) {
2077
+ if (nameEl) nameEl.textContent = '上传出错: ' + e.message;
2078
+ _pendingFile = null;
2079
+ }
2080
+ };
2081
+
2039
2082
  // ── 输入处理 ──────────────────────────────────
2040
2083
  window.sendCommand = sendCommand;
2041
2084
  function sendCommand() {
2042
2085
  const text = cmdInput.value.trim();
2043
- if (!text && !_pendingImage) return;
2086
+ if (!text && !_pendingImage && !_pendingFile) return;
2044
2087
 
2045
2088
  if (!isController) {
2046
2089
  if (canTakeControl()) { socket.emit('take-control', { sessionId: SESSION_ID }); }
@@ -2049,29 +2092,37 @@
2049
2092
  endStream();
2050
2093
 
2051
2094
  const imgPath = _pendingImage?.path || null;
2095
+ const filePath = _pendingFile?.path || null;
2096
+ const fileName = _pendingFile?.name || '';
2052
2097
  const userText = text;
2053
- const displayText = imgPath ? `📷 ${userText || '分析图片'}` : text;
2098
+ const displayText = imgPath ? `📷 ${userText || '分析图片'}`
2099
+ : filePath ? `📎 ${fileName}${userText ? ' ' + userText : ''}`
2100
+ : text;
2054
2101
 
2055
2102
  addInputMessage(displayText);
2056
2103
 
2057
2104
  if (currentSession?.mode === 'agent') {
2058
2105
  const prompt = imgPath
2059
2106
  ? `用户上传了一张图片,路径是 ${imgPath},请用 Read 工具读取这张图片。用户的要求是:${userText || '描述图片内容'}`
2107
+ : filePath
2108
+ ? `用户上传了一个临时文件用于本次分析(不在项目文件树里),路径是 ${filePath}(原文件名 ${fileName}),请用合适的工具(如 Read)读取并分析它。用户的要求是:${userText || '分析这个文件'}`
2060
2109
  : text;
2061
2110
  socket.emit('agent:query', { prompt });
2062
2111
  showThinking();
2063
2112
  } else {
2064
- const input = imgPath
2065
- ? (userText ? `${userText} ${imgPath}` : imgPath)
2113
+ const attach = imgPath || filePath;
2114
+ const input = attach
2115
+ ? (userText ? `${userText} ${attach}` : attach)
2066
2116
  : text;
2067
2117
  socket.emit('input', input + '\r');
2068
2118
  }
2069
- cmdHistory.unshift(imgPath ? `[图片] ${userText}` : text);
2119
+ cmdHistory.unshift(imgPath ? `[图片] ${userText}` : filePath ? `[文件] ${fileName} ${userText}` : text);
2070
2120
  if (cmdHistory.length > 100) cmdHistory.pop();
2071
2121
  historyIdx = -1;
2072
2122
  cmdInput.value = '';
2073
2123
  cmdInput.style.height = 'auto';
2074
2124
  clearPendingImage();
2125
+ clearPendingFile();
2075
2126
  cmdInput.focus();
2076
2127
  }
2077
2128
 
package/dist/grid.html CHANGED
@@ -5,6 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
6
6
  <meta name="theme-color" content="#0d1117">
7
7
  <title>myhi 分屏</title>
8
+ <script src="/sio-client/socket.io.min.js"></script>
8
9
  <script>
9
10
  (function(){try{
10
11
  var t=localStorage.getItem('myhi_theme')||'dark';
@@ -44,6 +45,8 @@
44
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; }
45
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); }
46
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; }
47
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; }
48
51
  .pane-head button:hover { color:var(--text); border-color:var(--blue2); }
49
52
  .pane-body { flex:1; min-height:0; position:relative; }
@@ -65,10 +68,29 @@
65
68
  </div>
66
69
  <span class="spacer"></span>
67
70
  <button class="bar-btn" onclick="refreshSessions(true)" title="刷新任务列表">↻</button>
68
- <button class="bar-btn" style="border-color:var(--blue2);color:var(--blue)" onclick="addPane('')">+ 添加任务</button>
71
+ <button class="bar-btn" onclick="addPane('')" title="加一个空窗格,从下拉选已有任务">+ 窗格</button>
72
+ <button class="bar-btn" style="border-color:var(--green);color:var(--green)" onclick="newTask()" title="新建一个任务并放进新窗格">🆕 新建任务</button>
69
73
  </div>
70
74
  <div id="grid"></div>
71
- <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>
72
94
 
73
95
  <script>
74
96
  const LS_KEY = 'myhi_grid_v1';
@@ -123,39 +145,39 @@
123
145
  }));
124
146
 
125
147
  // ── 窗格 ──
126
- function buildSelect(sid) {
127
- const sel = document.createElement('select');
128
- const opt0 = document.createElement('option'); opt0.value=''; opt0.textContent='— 选择任务 —'; sel.appendChild(opt0);
129
- for (const s of sessions) { const o = document.createElement('option'); o.value = sId(s); o.textContent = sName(s); sel.appendChild(o); }
130
- sel.value = sid || '';
131
- 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>`;
132
153
  }
133
154
  function addPane(sid, skipSave) {
134
155
  const pane = document.createElement('div'); pane.className = 'pane'; pane._sid = sid || '';
135
156
  const head = document.createElement('div'); head.className = 'pane-head';
136
- const sel = buildSelect(sid);
157
+ const picker = document.createElement('button'); picker.className = 'pane-picker';
137
158
  const full = document.createElement('button'); full.textContent='↗'; full.title='在新标签页打开';
138
159
  const close = document.createElement('button'); close.textContent='✕'; close.title='关闭此格'; close.style.color='var(--red)';
139
- head.appendChild(sel); head.appendChild(full); head.appendChild(close);
160
+ head.appendChild(picker); head.appendChild(full); head.appendChild(close);
140
161
  const body = document.createElement('div'); body.className = 'pane-body';
141
- 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);
142
163
  pane.appendChild(head); pane.appendChild(body);
143
- 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;
144
165
 
145
- sel.addEventListener('change', () => setPaneSession(pane, sel.value));
166
+ picker.addEventListener('click', () => openTaskPicker(pane));
146
167
  full.addEventListener('click', () => { if (pane._sid) window.open('/terminal/' + pane._sid, '_blank'); });
147
168
  close.addEventListener('click', () => removePane(pane));
148
169
 
149
170
  gridEl.appendChild(pane);
150
171
  paneEls.push(pane);
151
- if (sid) setPaneSession(pane, sid, true);
172
+ updatePickerLabel(pane);
173
+ if (sid) setPaneSession(pane, sid);
152
174
  if (!skipSave) { saveState(); }
153
175
  applyLayout();
154
176
  }
155
- function setPaneSession(pane, sid, keepSelect) {
177
+ function setPaneSession(pane, sid) {
156
178
  pane._sid = sid || '';
157
179
  pane._key = sid ? keyOf(sessById(sid)) : null; // 记住稳定标识,供重启后找回
158
- if (!keepSelect) pane._sel.value = sid || '';
180
+ updatePickerLabel(pane);
159
181
  // 只重建这一格的 iframe,其它格不受影响
160
182
  if (pane._iframe) { pane._iframe.remove(); pane._iframe = null; }
161
183
  if (sid) {
@@ -174,21 +196,98 @@
174
196
  }
175
197
 
176
198
  // ── 任务列表 ──
177
- async function refreshSessions(rebuildSelects) {
199
+ async function refreshSessions(rebuild) {
178
200
  try {
179
201
  const r = await fetch('/api/sessions'); if (!r.ok) return;
180
202
  const data = await r.json();
181
203
  sessions = (Array.isArray(data) ? data : (data.sessions || [])).filter(s => sId(s));
182
204
  } catch {}
183
- if (rebuildSelects) {
184
- for (const pane of paneEls) {
185
- const cur = pane._sid;
186
- const fresh = buildSelect(cur);
187
- fresh.addEventListener('change', () => setPaneSession(pane, fresh.value));
188
- 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('');
189
259
  }
190
- }
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>';
191
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);
277
+
278
+ // ── 新建任务(走 socket create-agent,和首页一致)──
279
+ const socket = io();
280
+ window.newTask = function() {
281
+ const title = (prompt('新任务名称:') || '').trim();
282
+ if (!title) return;
283
+ socket.emit('create-agent', { title }, (res) => {
284
+ if (res && res.ok && res.session) {
285
+ refreshSessions(true).then(() => addPane(res.session.id));
286
+ } else {
287
+ alert('创建失败:' + ((res && res.error) || '未知错误'));
288
+ }
289
+ });
290
+ };
192
291
 
193
292
  // ── 初始化 ──
194
293
  (async function init() {