@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 +57 -6
- package/dist/grid.html +123 -24
- package/dist/index.js +1 -1
- package/dist/index.min.js +8 -8
- package/package.json +1 -1
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="移除图片">×</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="移除文件">×</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 || '分析图片'}`
|
|
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
|
|
2065
|
-
|
|
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"
|
|
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">点「+
|
|
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
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
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(
|
|
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='
|
|
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.
|
|
164
|
+
pane._picker = picker; pane._body = body; pane._empty = empty; pane._iframe = null;
|
|
144
165
|
|
|
145
|
-
|
|
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
|
-
|
|
172
|
+
updatePickerLabel(pane);
|
|
173
|
+
if (sid) setPaneSession(pane, sid);
|
|
152
174
|
if (!skipSave) { saveState(); }
|
|
153
175
|
applyLayout();
|
|
154
176
|
}
|
|
155
|
-
function setPaneSession(pane, sid
|
|
177
|
+
function setPaneSession(pane, sid) {
|
|
156
178
|
pane._sid = sid || '';
|
|
157
179
|
pane._key = sid ? keyOf(sessById(sid)) : null; // 记住稳定标识,供重启后找回
|
|
158
|
-
|
|
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(
|
|
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 (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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() {
|