@luckydraw/cumulus 1.0.7 → 1.0.8
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/CHANGELOG.md +10 -0
- package/dist/gateway/adapters/webchat.d.ts.map +1 -1
- package/dist/gateway/adapters/webchat.js +173 -2
- package/dist/gateway/adapters/webchat.js.map +1 -1
- package/dist/gateway/gateway-agents-mcp.js +50 -0
- package/dist/gateway/gateway-agents-mcp.js.map +1 -1
- package/dist/gateway/server.d.ts +65 -0
- package/dist/gateway/server.d.ts.map +1 -1
- package/dist/gateway/server.js +222 -14
- package/dist/gateway/server.js.map +1 -1
- package/dist/gateway/static/widget.js +581 -66
- package/dist/lib/config.d.ts +34 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +43 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/content-store.d.ts +64 -1
- package/dist/lib/content-store.d.ts.map +1 -1
- package/dist/lib/content-store.js +161 -48
- package/dist/lib/content-store.js.map +1 -1
- package/dist/lib/embeddings.d.ts +11 -1
- package/dist/lib/embeddings.d.ts.map +1 -1
- package/dist/lib/embeddings.js +301 -40
- package/dist/lib/embeddings.js.map +1 -1
- package/dist/lib/gateway.d.ts +18 -1
- package/dist/lib/gateway.d.ts.map +1 -1
- package/dist/lib/gateway.js +37 -5
- package/dist/lib/gateway.js.map +1 -1
- package/dist/lib/retriever.d.ts +34 -2
- package/dist/lib/retriever.d.ts.map +1 -1
- package/dist/lib/retriever.js +9 -2
- package/dist/lib/retriever.js.map +1 -1
- package/dist/lib/tool-inventory.d.ts.map +1 -1
- package/dist/lib/tool-inventory.js +2 -0
- package/dist/lib/tool-inventory.js.map +1 -1
- package/dist/mcp/index.js +7 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/server.d.ts +4 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +4 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tool-handler.d.ts.map +1 -1
- package/dist/mcp/tool-handler.js +11 -0
- package/dist/mcp/tool-handler.js.map +1 -1
- package/package.json +1 -1
|
@@ -970,6 +970,30 @@
|
|
|
970
970
|
'.cumulus-send-btn.stop:hover:not(:disabled) { background: #a02828; }',
|
|
971
971
|
'.cumulus-send-btn.interject { background: #b8860b; color: #fff; }',
|
|
972
972
|
'.cumulus-send-btn.interject:hover:not(:disabled) { background: #d4a017; }',
|
|
973
|
+
/* Queue (task 144): teal, deliberately NOT the interject gold — the two
|
|
974
|
+
are different acts and the button is the only place that says which. */
|
|
975
|
+
'.cumulus-send-btn.queue { background: #1f6f6b; color: #dff5f3; }',
|
|
976
|
+
'.cumulus-send-btn.queue:hover:not(:disabled) { background: #2a8f8a; }',
|
|
977
|
+
|
|
978
|
+
/* ── Deferred user messages (task 144) ── */
|
|
979
|
+
'.cumulus-msg.queued {',
|
|
980
|
+
' align-self: flex-end; max-width: 80%; background: #1a2733;',
|
|
981
|
+
' border: 1px dashed #3a5566; border-radius: 10px; padding: 8px 10px;',
|
|
982
|
+
' color: #9fb3c4; font-size: 13px; line-height: 1.45;',
|
|
983
|
+
'}',
|
|
984
|
+
'.cumulus-queued-head { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }',
|
|
985
|
+
'.cumulus-queued-badge {',
|
|
986
|
+
' font-size: 10px; font-weight: 700; letter-spacing: 0.08em; color: #7fd6cf;',
|
|
987
|
+
' border: 1px solid #2a8f8a; border-radius: 3px; padding: 1px 5px;',
|
|
988
|
+
'}',
|
|
989
|
+
'.cumulus-queued-note { font-size: 11px; color: #6b8199; font-style: italic; }',
|
|
990
|
+
'.cumulus-queued-body { white-space: pre-wrap; word-break: break-word; }',
|
|
991
|
+
'.cumulus-queued-actions { display: flex; gap: 6px; margin-top: 6px; }',
|
|
992
|
+
'.cumulus-queued-btn {',
|
|
993
|
+
' background: #22323f; border: 1px solid #3a5566; color: #9fb3c4;',
|
|
994
|
+
' border-radius: 4px; padding: 2px 8px; font-size: 11px; cursor: pointer;',
|
|
995
|
+
'}',
|
|
996
|
+
'.cumulus-queued-btn:hover { background: #2d4252; color: #e0e0e0; }',
|
|
973
997
|
|
|
974
998
|
/* ── Auth panel ── */
|
|
975
999
|
'.cumulus-auth {',
|
|
@@ -1047,13 +1071,14 @@
|
|
|
1047
1071
|
'}',
|
|
1048
1072
|
'.cumulus-thread-item {',
|
|
1049
1073
|
' display: flex; align-items: center; gap: 8px;',
|
|
1050
|
-
' padding: 7px 12px;',
|
|
1074
|
+
' padding: 7px 12px 7px 4px;',
|
|
1051
1075
|
' cursor: pointer;',
|
|
1052
1076
|
' font-size: 13px; color: #ccc;',
|
|
1053
1077
|
' border-left: 2px solid transparent;',
|
|
1054
1078
|
' user-select: none;',
|
|
1055
1079
|
' transition: background 0.1s ease;',
|
|
1056
1080
|
'}',
|
|
1081
|
+
'.cumulus-thread-item.co-thread { padding-left: 20px; }',
|
|
1057
1082
|
'.cumulus-thread-item:hover { background: #2a2a2a; }',
|
|
1058
1083
|
'.cumulus-thread-item.selected {',
|
|
1059
1084
|
' border-left-color: #0066cc;',
|
|
@@ -1073,10 +1098,24 @@
|
|
|
1073
1098
|
' flex: 1; overflow: hidden;',
|
|
1074
1099
|
' text-overflow: ellipsis; white-space: nowrap;',
|
|
1075
1100
|
'}',
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
'
|
|
1101
|
+
/* Disclosure triangle. Always occupies its slot (even when empty) so
|
|
1102
|
+
sibling names stay aligned; only the with-children state is clickable. */
|
|
1103
|
+
'.cumulus-thread-twisty {',
|
|
1104
|
+
' width: 12px; flex-shrink: 0;',
|
|
1105
|
+
' font-size: 9px; color: #666; line-height: 1;',
|
|
1106
|
+
' text-align: center;',
|
|
1079
1107
|
'}',
|
|
1108
|
+
'.cumulus-thread-twisty.has-children { cursor: pointer; }',
|
|
1109
|
+
'.cumulus-thread-twisty.has-children:hover { color: #ccc; }',
|
|
1110
|
+
'.cumulus-co-thread-input {',
|
|
1111
|
+
' width: 100%; box-sizing: border-box;',
|
|
1112
|
+
' margin: 10px 0 6px;',
|
|
1113
|
+
' padding: 7px 9px;',
|
|
1114
|
+
' background: #1a1a1a; color: #e0e0e0;',
|
|
1115
|
+
' border: 1px solid #444; border-radius: 4px;',
|
|
1116
|
+
' font-size: 13px; font-family: inherit;',
|
|
1117
|
+
'}',
|
|
1118
|
+
'.cumulus-co-thread-input:focus { outline: none; border-color: #0066cc; }',
|
|
1080
1119
|
'.cumulus-sidebar-footer {',
|
|
1081
1120
|
' padding: 8px 10px;',
|
|
1082
1121
|
' border-top: 1px solid #333;',
|
|
@@ -1822,8 +1861,11 @@
|
|
|
1822
1861
|
|
|
1823
1862
|
/* Bigger touch targets for thread items */
|
|
1824
1863
|
' .cumulus-thread-item {',
|
|
1825
|
-
' padding: 12px 14px; font-size: 15px; min-height: 44px;',
|
|
1864
|
+
' padding: 12px 14px 12px 6px; font-size: 15px; min-height: 44px;',
|
|
1826
1865
|
' }',
|
|
1866
|
+
/* A 12px triangle is not a touch target — widen it, and indent to match. */
|
|
1867
|
+
' .cumulus-thread-twisty { width: 24px; font-size: 12px; }',
|
|
1868
|
+
' .cumulus-thread-item.co-thread { padding-left: 30px; }',
|
|
1827
1869
|
' .cumulus-new-thread-btn { padding: 12px 10px; font-size: 15px; min-height: 44px; }',
|
|
1828
1870
|
' .cumulus-new-thread-input { padding: 12px 10px; font-size: 15px; min-height: 44px; }',
|
|
1829
1871
|
' .cumulus-sidebar-footer { padding: 10px; }',
|
|
@@ -1994,7 +2036,42 @@
|
|
|
1994
2036
|
return AGENT_COLORS[Math.abs(hash) % AGENT_COLORS.length];
|
|
1995
2037
|
}
|
|
1996
2038
|
|
|
2039
|
+
// A drained BATCH of agent messages (server.ts drainAgentQueue, >1 message).
|
|
2040
|
+
// It carries no "[sender → recipient]:" prefix, so AGENT_MSG_RE never matched
|
|
2041
|
+
// it and the whole batch fell through to the plain user bubble — which is
|
|
2042
|
+
// exactly what a batch looked like in the UI. Senders are named per entry
|
|
2043
|
+
// inside the body instead, so they are collected from there.
|
|
2044
|
+
var AGENT_BATCH_RE = /^\[While you were busy, (\d+) messages arrived\]\s*/;
|
|
2045
|
+
var AGENT_BATCH_SENDER_RE = /^\[From (?:agent|automated sender) "([^"]+)"\]/gm;
|
|
2046
|
+
|
|
2047
|
+
function parseAgentBatchMessage(content) {
|
|
2048
|
+
var match = content.match(AGENT_BATCH_RE);
|
|
2049
|
+
if (!match) return null;
|
|
2050
|
+
var body = content
|
|
2051
|
+
.slice(match[0].length)
|
|
2052
|
+
.replace(/\n*\(Review the above messages[\s\S]*$/, '')
|
|
2053
|
+
.trim();
|
|
2054
|
+
|
|
2055
|
+
var senders = [];
|
|
2056
|
+
AGENT_BATCH_SENDER_RE.lastIndex = 0;
|
|
2057
|
+
var m;
|
|
2058
|
+
while ((m = AGENT_BATCH_SENDER_RE.exec(content)) !== null) {
|
|
2059
|
+
if (senders.indexOf(m[1]) === -1) senders.push(m[1]);
|
|
2060
|
+
}
|
|
2061
|
+
var label = senders.length === 0 ? 'agents' : senders.slice(0, 2).join(', ');
|
|
2062
|
+
if (senders.length > 2) label += ' +' + (senders.length - 2);
|
|
2063
|
+
|
|
2064
|
+
return {
|
|
2065
|
+
sender: label,
|
|
2066
|
+
recipient: null,
|
|
2067
|
+
body: body,
|
|
2068
|
+
batchCount: parseInt(match[1], 10),
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
|
|
1997
2072
|
function parseAgentMessage(content) {
|
|
2073
|
+
var batch = parseAgentBatchMessage(content);
|
|
2074
|
+
if (batch) return batch;
|
|
1998
2075
|
var match = content.match(AGENT_MSG_RE);
|
|
1999
2076
|
if (!match) return null;
|
|
2000
2077
|
var body = content
|
|
@@ -2036,7 +2113,10 @@
|
|
|
2036
2113
|
header.appendChild(nameEl);
|
|
2037
2114
|
var ctx = document.createElement('span');
|
|
2038
2115
|
ctx.className = 'cumulus-agent-context';
|
|
2039
|
-
|
|
2116
|
+
// A drained batch names no recipient \u2014 it says how many arrived instead.
|
|
2117
|
+
ctx.textContent = agent.recipient
|
|
2118
|
+
? '\u2192 ' + agent.recipient
|
|
2119
|
+
: agent.batchCount + ' messages while busy';
|
|
2040
2120
|
header.appendChild(ctx);
|
|
2041
2121
|
el.appendChild(header);
|
|
2042
2122
|
|
|
@@ -4532,6 +4612,10 @@
|
|
|
4532
4612
|
currentSegments: [],
|
|
4533
4613
|
expandedChips: {}, // msgKey → true if chip expanded
|
|
4534
4614
|
promptCache: {}, // promptRef → fetched capture record (task 088)
|
|
4615
|
+
// Deferred user messages awaiting the current response (task 144).
|
|
4616
|
+
// Mirrors the server's queue — never written locally except from a
|
|
4617
|
+
// `queued_messages` broadcast, so two tabs cannot disagree.
|
|
4618
|
+
queued: [],
|
|
4535
4619
|
};
|
|
4536
4620
|
}
|
|
4537
4621
|
return threadStates[threadName];
|
|
@@ -5038,8 +5122,10 @@
|
|
|
5038
5122
|
var threadInfo = allThreads.find(function (t) {
|
|
5039
5123
|
return t.name === name;
|
|
5040
5124
|
});
|
|
5125
|
+
// The Active section lists open panels in panel order, so it stays
|
|
5126
|
+
// flat — nesting there would fight the ordering it exists to show.
|
|
5041
5127
|
activeSection.appendChild(
|
|
5042
|
-
buildThreadItem(name, true, true,
|
|
5128
|
+
buildThreadItem(threadInfo || { name: name }, true, true, null)
|
|
5043
5129
|
);
|
|
5044
5130
|
});
|
|
5045
5131
|
sidebarScroll.appendChild(activeSection);
|
|
@@ -5069,27 +5155,90 @@
|
|
|
5069
5155
|
var sorted = allThreads.slice().sort(function (a, b) {
|
|
5070
5156
|
return (b.lastActivity || 0) - (a.lastActivity || 0);
|
|
5071
5157
|
});
|
|
5072
|
-
sorted.forEach(function (
|
|
5158
|
+
buildThreadTree(sorted, collapsedMasters).forEach(function (row) {
|
|
5073
5159
|
allSection.appendChild(
|
|
5074
|
-
buildThreadItem(
|
|
5075
|
-
t.name,
|
|
5076
|
-
visibleSet[t.name] || false,
|
|
5077
|
-
false,
|
|
5078
|
-
t.messageCount,
|
|
5079
|
-
t.projectDir
|
|
5080
|
-
)
|
|
5160
|
+
buildThreadItem(row.thread, visibleSet[row.thread.name] || false, false, row)
|
|
5081
5161
|
);
|
|
5082
5162
|
});
|
|
5083
5163
|
}
|
|
5084
5164
|
sidebarScroll.appendChild(allSection);
|
|
5085
5165
|
}
|
|
5086
5166
|
|
|
5087
|
-
|
|
5167
|
+
/**
|
|
5168
|
+
* Flatten the thread list into sidebar render rows, nesting co-threads
|
|
5169
|
+
* under their master (task 143).
|
|
5170
|
+
*
|
|
5171
|
+
* Membership comes from the server's `master` field, never from the name.
|
|
5172
|
+
* Every app's visitor threads are named `<app>-<deviceId>` and would nest
|
|
5173
|
+
* under a prefix rule, claiming a relationship that does not exist.
|
|
5174
|
+
*
|
|
5175
|
+
* A co-thread whose master is absent from this list (a scoped viewer, or a
|
|
5176
|
+
* deleted master) renders at top level. Nesting is a presentation of a real
|
|
5177
|
+
* relationship; it must never be the reason a thread disappears.
|
|
5178
|
+
*
|
|
5179
|
+
* Pure: no DOM, no globals. `collapsed` maps masterName -> true.
|
|
5180
|
+
*/
|
|
5181
|
+
function buildThreadTree(sortedThreads, collapsed) {
|
|
5182
|
+
var present = {};
|
|
5183
|
+
var i;
|
|
5184
|
+
for (i = 0; i < sortedThreads.length; i++) present[sortedThreads[i].name] = true;
|
|
5185
|
+
|
|
5186
|
+
var childrenOf = {};
|
|
5187
|
+
for (i = 0; i < sortedThreads.length; i++) {
|
|
5188
|
+
var t = sortedThreads[i];
|
|
5189
|
+
if (t.master && present[t.master] && t.master !== t.name) {
|
|
5190
|
+
(childrenOf[t.master] = childrenOf[t.master] || []).push(t);
|
|
5191
|
+
}
|
|
5192
|
+
}
|
|
5193
|
+
|
|
5194
|
+
var rows = [];
|
|
5195
|
+
for (i = 0; i < sortedThreads.length; i++) {
|
|
5196
|
+
var thread = sortedThreads[i];
|
|
5197
|
+
if (thread.master && present[thread.master] && thread.master !== thread.name) continue;
|
|
5198
|
+
|
|
5199
|
+
var kids = childrenOf[thread.name] || [];
|
|
5200
|
+
var isCollapsed = kids.length > 0 && !!(collapsed || {})[thread.name];
|
|
5201
|
+
rows.push({
|
|
5202
|
+
thread: thread,
|
|
5203
|
+
depth: 0,
|
|
5204
|
+
childCount: kids.length,
|
|
5205
|
+
collapsed: isCollapsed,
|
|
5206
|
+
});
|
|
5207
|
+
if (isCollapsed) continue;
|
|
5208
|
+
for (var k = 0; k < kids.length; k++) {
|
|
5209
|
+
rows.push({ thread: kids[k], depth: 1, childCount: 0, collapsed: false });
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
return rows;
|
|
5213
|
+
}
|
|
5214
|
+
|
|
5215
|
+
function buildThreadItem(thread, isSelected, isActive, row) {
|
|
5216
|
+
var name = thread.name;
|
|
5217
|
+
var projectDir = thread.projectDir;
|
|
5088
5218
|
var item = document.createElement('div');
|
|
5089
|
-
item.className =
|
|
5219
|
+
item.className =
|
|
5220
|
+
'cumulus-thread-item' +
|
|
5221
|
+
(isSelected ? ' selected' : '') +
|
|
5222
|
+
(row && row.depth ? ' co-thread' : '');
|
|
5090
5223
|
item.setAttribute('data-testid', 'webchat-thread-item');
|
|
5091
5224
|
item.setAttribute('data-thread-name', name);
|
|
5092
5225
|
|
|
5226
|
+
// Disclosure triangle — occupies its slot even with no children, so the
|
|
5227
|
+
// names of siblings stay aligned instead of jittering by a few pixels.
|
|
5228
|
+
var twisty = document.createElement('span');
|
|
5229
|
+
twisty.className = 'cumulus-thread-twisty';
|
|
5230
|
+
if (row && row.childCount > 0) {
|
|
5231
|
+
twisty.classList.add('has-children');
|
|
5232
|
+
twisty.textContent = row.collapsed ? '▸' : '▾';
|
|
5233
|
+
twisty.setAttribute('data-testid', 'webchat-thread-twisty');
|
|
5234
|
+
twisty.setAttribute('title', row.collapsed ? 'Show co-threads' : 'Hide co-threads');
|
|
5235
|
+
twisty.addEventListener('click', function (e) {
|
|
5236
|
+
e.stopPropagation(); // do not also open the thread
|
|
5237
|
+
toggleMasterCollapsed(name);
|
|
5238
|
+
});
|
|
5239
|
+
}
|
|
5240
|
+
item.appendChild(twisty);
|
|
5241
|
+
|
|
5093
5242
|
var dot = document.createElement('span');
|
|
5094
5243
|
dot.className = unreadThreads[name]
|
|
5095
5244
|
? 'cumulus-thread-unread'
|
|
@@ -5098,7 +5247,13 @@
|
|
|
5098
5247
|
|
|
5099
5248
|
var nameEl = document.createElement('span');
|
|
5100
5249
|
nameEl.className = 'cumulus-thread-name';
|
|
5101
|
-
|
|
5250
|
+
// A co-thread's name repeats its master's prefix; showing only the label
|
|
5251
|
+
// keeps the tree readable. The full name stays in the tooltip and in
|
|
5252
|
+
// data-thread-name, which is what everything else keys off.
|
|
5253
|
+
nameEl.textContent =
|
|
5254
|
+
row && row.depth && thread.master && name.indexOf(thread.master + '-') === 0
|
|
5255
|
+
? name.slice(thread.master.length + 1)
|
|
5256
|
+
: name;
|
|
5102
5257
|
nameEl.setAttribute('title', projectDir ? name + '\n\u{1f4c1} ' + projectDir : name);
|
|
5103
5258
|
item.appendChild(nameEl);
|
|
5104
5259
|
|
|
@@ -5109,18 +5264,11 @@
|
|
|
5109
5264
|
item.appendChild(folderEl);
|
|
5110
5265
|
}
|
|
5111
5266
|
|
|
5112
|
-
if (messageCount !== undefined && messageCount !== null) {
|
|
5113
|
-
var countEl = document.createElement('span');
|
|
5114
|
-
countEl.className = 'cumulus-thread-count';
|
|
5115
|
-
countEl.textContent = messageCount;
|
|
5116
|
-
item.appendChild(countEl);
|
|
5117
|
-
}
|
|
5118
|
-
|
|
5119
5267
|
// Right-click context menu
|
|
5120
5268
|
item.addEventListener('contextmenu', function (e) {
|
|
5121
5269
|
e.preventDefault();
|
|
5122
5270
|
e.stopPropagation();
|
|
5123
|
-
showThreadContextMenu(name, e.clientX, e.clientY);
|
|
5271
|
+
showThreadContextMenu(name, e.clientX, e.clientY, thread.master);
|
|
5124
5272
|
});
|
|
5125
5273
|
|
|
5126
5274
|
item.addEventListener('click', function (e) {
|
|
@@ -5143,7 +5291,7 @@
|
|
|
5143
5291
|
document.removeEventListener('contextmenu', dismissContextMenu);
|
|
5144
5292
|
}
|
|
5145
5293
|
|
|
5146
|
-
function showThreadContextMenu(threadName, x, y) {
|
|
5294
|
+
function showThreadContextMenu(threadName, x, y, master) {
|
|
5147
5295
|
dismissContextMenu();
|
|
5148
5296
|
|
|
5149
5297
|
var menu = document.createElement('div');
|
|
@@ -5151,6 +5299,22 @@
|
|
|
5151
5299
|
menu.style.left = x + 'px';
|
|
5152
5300
|
menu.style.top = y + 'px';
|
|
5153
5301
|
|
|
5302
|
+
// Co-threads are created from the master ONLY (task 143), so the item is
|
|
5303
|
+
// absent rather than disabled on a thread that is itself a co-thread —
|
|
5304
|
+
// offering an action the server answers with 409 is worse than not
|
|
5305
|
+
// offering it. The server still enforces; this only avoids the dead end.
|
|
5306
|
+
if (!master) {
|
|
5307
|
+
var coItem = document.createElement('button');
|
|
5308
|
+
coItem.className = 'cumulus-context-menu-item';
|
|
5309
|
+
coItem.textContent = 'New co-thread…';
|
|
5310
|
+
coItem.setAttribute('data-testid', 'webchat-thread-new-co-thread');
|
|
5311
|
+
coItem.addEventListener('click', function () {
|
|
5312
|
+
dismissContextMenu();
|
|
5313
|
+
showCoThreadDialog(threadName);
|
|
5314
|
+
});
|
|
5315
|
+
menu.appendChild(coItem);
|
|
5316
|
+
}
|
|
5317
|
+
|
|
5154
5318
|
var deleteItem = document.createElement('button');
|
|
5155
5319
|
deleteItem.className = 'cumulus-context-menu-item danger';
|
|
5156
5320
|
deleteItem.textContent = 'Delete thread';
|
|
@@ -5177,6 +5341,163 @@
|
|
|
5177
5341
|
}, 0);
|
|
5178
5342
|
}
|
|
5179
5343
|
|
|
5344
|
+
// ── Co-thread creation ──
|
|
5345
|
+
|
|
5346
|
+
/**
|
|
5347
|
+
* Which masters are collapsed. Default is EXPANDED (absent = open) so a
|
|
5348
|
+
* co-thread created a moment ago is visible without a second click.
|
|
5349
|
+
*/
|
|
5350
|
+
var collapsedMasters = (function () {
|
|
5351
|
+
try {
|
|
5352
|
+
return JSON.parse(localStorage.getItem('cumulus-collapsed-masters') || '{}') || {};
|
|
5353
|
+
} catch {
|
|
5354
|
+
return {};
|
|
5355
|
+
}
|
|
5356
|
+
})();
|
|
5357
|
+
|
|
5358
|
+
function toggleMasterCollapsed(name) {
|
|
5359
|
+
if (collapsedMasters[name]) delete collapsedMasters[name];
|
|
5360
|
+
else collapsedMasters[name] = true;
|
|
5361
|
+
try {
|
|
5362
|
+
localStorage.setItem('cumulus-collapsed-masters', JSON.stringify(collapsedMasters));
|
|
5363
|
+
} catch {
|
|
5364
|
+
/* private mode — collapse still works, it just does not persist */
|
|
5365
|
+
}
|
|
5366
|
+
renderSidebar();
|
|
5367
|
+
}
|
|
5368
|
+
|
|
5369
|
+
function showCoThreadDialog(masterName) {
|
|
5370
|
+
var existing = document.querySelector('.cumulus-confirm-dialog[data-context="co-thread"]');
|
|
5371
|
+
if (existing) existing.remove();
|
|
5372
|
+
|
|
5373
|
+
var dialog = document.createElement('div');
|
|
5374
|
+
dialog.className = 'cumulus-confirm-dialog';
|
|
5375
|
+
dialog.setAttribute('data-context', 'co-thread');
|
|
5376
|
+
dialog.style.cssText =
|
|
5377
|
+
'position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); z-index: 200; display: flex; align-items: center; justify-content: center;';
|
|
5378
|
+
|
|
5379
|
+
var box = document.createElement('div');
|
|
5380
|
+
box.className = 'cumulus-confirm-dialog-box';
|
|
5381
|
+
|
|
5382
|
+
var title = document.createElement('div');
|
|
5383
|
+
title.className = 'cumulus-confirm-dialog-title';
|
|
5384
|
+
title.textContent = 'New co-thread';
|
|
5385
|
+
box.appendChild(title);
|
|
5386
|
+
|
|
5387
|
+
var text = document.createElement('div');
|
|
5388
|
+
text.className = 'cumulus-confirm-dialog-text';
|
|
5389
|
+
text.textContent =
|
|
5390
|
+
'A co-thread shares "' +
|
|
5391
|
+
masterName +
|
|
5392
|
+
'" memory but keeps its own conversation, so both can work at once.';
|
|
5393
|
+
box.appendChild(text);
|
|
5394
|
+
|
|
5395
|
+
var input = document.createElement('input');
|
|
5396
|
+
input.type = 'text';
|
|
5397
|
+
input.className = 'cumulus-co-thread-input';
|
|
5398
|
+
input.placeholder = 'frontend';
|
|
5399
|
+
input.setAttribute('data-testid', 'webchat-co-thread-label');
|
|
5400
|
+
box.appendChild(input);
|
|
5401
|
+
|
|
5402
|
+
var preview = document.createElement('div');
|
|
5403
|
+
preview.className = 'cumulus-confirm-dialog-warn';
|
|
5404
|
+
preview.setAttribute('data-testid', 'webchat-co-thread-preview');
|
|
5405
|
+
box.appendChild(preview);
|
|
5406
|
+
|
|
5407
|
+
// Mirrors CO_THREAD_LABEL in server.ts. The server is authoritative; this
|
|
5408
|
+
// only turns a 400 into a message written before the request is sent.
|
|
5409
|
+
var LABEL_OK = /^[a-zA-Z0-9_]+$/;
|
|
5410
|
+
function refreshPreview() {
|
|
5411
|
+
var label = input.value.trim();
|
|
5412
|
+
if (!label) {
|
|
5413
|
+
preview.textContent = 'Letters, numbers and underscores.';
|
|
5414
|
+
return false;
|
|
5415
|
+
}
|
|
5416
|
+
if (!LABEL_OK.test(label)) {
|
|
5417
|
+
preview.textContent = 'Letters, numbers and underscores only — no spaces or hyphens.';
|
|
5418
|
+
return false;
|
|
5419
|
+
}
|
|
5420
|
+
preview.textContent = 'Creates: ' + masterName + '-' + label;
|
|
5421
|
+
return true;
|
|
5422
|
+
}
|
|
5423
|
+
refreshPreview();
|
|
5424
|
+
input.addEventListener('input', refreshPreview);
|
|
5425
|
+
|
|
5426
|
+
var actions = document.createElement('div');
|
|
5427
|
+
actions.className = 'cumulus-confirm-dialog-actions';
|
|
5428
|
+
|
|
5429
|
+
var cancelBtn = document.createElement('button');
|
|
5430
|
+
cancelBtn.className = 'cumulus-confirm-dialog-cancel';
|
|
5431
|
+
cancelBtn.textContent = 'Cancel';
|
|
5432
|
+
cancelBtn.addEventListener('click', function () {
|
|
5433
|
+
dialog.remove();
|
|
5434
|
+
});
|
|
5435
|
+
actions.appendChild(cancelBtn);
|
|
5436
|
+
|
|
5437
|
+
var createBtn = document.createElement('button');
|
|
5438
|
+
createBtn.className = 'cumulus-confirm-dialog-confirm';
|
|
5439
|
+
createBtn.setAttribute('data-testid', 'webchat-co-thread-create');
|
|
5440
|
+
createBtn.textContent = 'Create';
|
|
5441
|
+
createBtn.addEventListener('click', function () {
|
|
5442
|
+
if (!refreshPreview()) return;
|
|
5443
|
+
createBtn.disabled = true;
|
|
5444
|
+
createBtn.textContent = 'Creating…';
|
|
5445
|
+
createCoThread(masterName, input.value.trim(), function (err, threadName) {
|
|
5446
|
+
if (err) {
|
|
5447
|
+
createBtn.disabled = false;
|
|
5448
|
+
createBtn.textContent = 'Create';
|
|
5449
|
+
preview.textContent = err;
|
|
5450
|
+
return;
|
|
5451
|
+
}
|
|
5452
|
+
dialog.remove();
|
|
5453
|
+
// The listing is what carries the `master` field, so refresh it
|
|
5454
|
+
// rather than synthesising an entry — a locally-invented row would
|
|
5455
|
+
// render at top level until the next real listing arrived.
|
|
5456
|
+
if (connection) connection.send({ type: 'threads' });
|
|
5457
|
+
soloThread(threadName);
|
|
5458
|
+
});
|
|
5459
|
+
});
|
|
5460
|
+
actions.appendChild(createBtn);
|
|
5461
|
+
|
|
5462
|
+
box.appendChild(actions);
|
|
5463
|
+
dialog.appendChild(box);
|
|
5464
|
+
|
|
5465
|
+
dialog.addEventListener('click', function (e) {
|
|
5466
|
+
if (e.target === dialog) dialog.remove();
|
|
5467
|
+
});
|
|
5468
|
+
|
|
5469
|
+
document.body.appendChild(dialog);
|
|
5470
|
+
input.focus();
|
|
5471
|
+
input.addEventListener('keydown', function (e) {
|
|
5472
|
+
if (e.key === 'Enter') createBtn.click();
|
|
5473
|
+
else if (e.key === 'Escape') dialog.remove();
|
|
5474
|
+
});
|
|
5475
|
+
}
|
|
5476
|
+
|
|
5477
|
+
function createCoThread(masterName, label, cb) {
|
|
5478
|
+
var xhr = new XMLHttpRequest();
|
|
5479
|
+
xhr.open('POST', '/api/thread/' + encodeURIComponent(masterName) + '/co-threads');
|
|
5480
|
+
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
5481
|
+
xhr.setRequestHeader('X-API-Key', activeApiKey);
|
|
5482
|
+
xhr.onload = function () {
|
|
5483
|
+
var body = {};
|
|
5484
|
+
try {
|
|
5485
|
+
body = JSON.parse(xhr.responseText || '{}');
|
|
5486
|
+
} catch {
|
|
5487
|
+
/* fall through to the status-based message below */
|
|
5488
|
+
}
|
|
5489
|
+
if (xhr.status === 200 || xhr.status === 201) {
|
|
5490
|
+
cb(null, body.thread || masterName + '-' + label);
|
|
5491
|
+
return;
|
|
5492
|
+
}
|
|
5493
|
+
cb(body.error || 'Could not create the co-thread (' + xhr.status + ').');
|
|
5494
|
+
};
|
|
5495
|
+
xhr.onerror = function () {
|
|
5496
|
+
cb('Could not reach the gateway.');
|
|
5497
|
+
};
|
|
5498
|
+
xhr.send(JSON.stringify({ label: label }));
|
|
5499
|
+
}
|
|
5500
|
+
|
|
5180
5501
|
// ── Thread delete confirmation ──
|
|
5181
5502
|
function showThreadDeleteConfirm(threadName) {
|
|
5182
5503
|
// Remove any existing confirm dialogs
|
|
@@ -5308,6 +5629,10 @@
|
|
|
5308
5629
|
if (!state.historyLoaded && connection) {
|
|
5309
5630
|
state.historyLoaded = true;
|
|
5310
5631
|
connection.send({ type: 'history', threadName: name, limit: 200 });
|
|
5632
|
+
// Deferred messages live on the server, so a fresh tab (or one that
|
|
5633
|
+
// reconnected) has to ask. This also flushes a batch that was queued
|
|
5634
|
+
// while no client was watching the thread (task 144).
|
|
5635
|
+
connection.send({ type: 'queue_list', threadName: name });
|
|
5311
5636
|
}
|
|
5312
5637
|
}
|
|
5313
5638
|
|
|
@@ -7647,6 +7972,121 @@
|
|
|
7647
7972
|
return chip;
|
|
7648
7973
|
}
|
|
7649
7974
|
|
|
7975
|
+
// A message the user chose to QUEUE rather than interject with (task 144).
|
|
7976
|
+
// Shown in the conversation the moment it is accepted — pressing Queue and
|
|
7977
|
+
// seeing nothing happen reads as broken — and stays editable, because it
|
|
7978
|
+
// has not been said yet.
|
|
7979
|
+
function buildQueuedMsgEl(item) {
|
|
7980
|
+
var el = document.createElement('div');
|
|
7981
|
+
el.className = 'cumulus-msg queued';
|
|
7982
|
+
el.setAttribute('data-testid', 'webchat-queued-msg');
|
|
7983
|
+
el.setAttribute('data-queued-id', item.id);
|
|
7984
|
+
|
|
7985
|
+
var head = document.createElement('div');
|
|
7986
|
+
head.className = 'cumulus-queued-head';
|
|
7987
|
+
var badge = document.createElement('span');
|
|
7988
|
+
badge.className = 'cumulus-queued-badge';
|
|
7989
|
+
badge.textContent = 'QUEUED';
|
|
7990
|
+
head.appendChild(badge);
|
|
7991
|
+
var note = document.createElement('span');
|
|
7992
|
+
note.className = 'cumulus-queued-note';
|
|
7993
|
+
note.textContent = 'waiting — sends when this response finishes';
|
|
7994
|
+
head.appendChild(note);
|
|
7995
|
+
el.appendChild(head);
|
|
7996
|
+
|
|
7997
|
+
var body = document.createElement('div');
|
|
7998
|
+
body.className = 'cumulus-queued-body';
|
|
7999
|
+
body.textContent = item.text;
|
|
8000
|
+
el.appendChild(body);
|
|
8001
|
+
|
|
8002
|
+
var actions = document.createElement('div');
|
|
8003
|
+
actions.className = 'cumulus-queued-actions';
|
|
8004
|
+
|
|
8005
|
+
function actionBtn(label, testid, onClick) {
|
|
8006
|
+
var b = document.createElement('button');
|
|
8007
|
+
b.className = 'cumulus-queued-btn';
|
|
8008
|
+
b.textContent = label;
|
|
8009
|
+
b.setAttribute('data-testid', testid);
|
|
8010
|
+
b.addEventListener('click', onClick);
|
|
8011
|
+
actions.appendChild(b);
|
|
8012
|
+
return b;
|
|
8013
|
+
}
|
|
8014
|
+
|
|
8015
|
+
actionBtn('Send Now', 'webchat-queued-send-now', function () {
|
|
8016
|
+
if (connection)
|
|
8017
|
+
connection.send({ type: 'queue_send_now', threadName: threadName, id: item.id });
|
|
8018
|
+
});
|
|
8019
|
+
actionBtn('Edit', 'webchat-queued-edit', function () {
|
|
8020
|
+
showQueuedEditPopover(el, item);
|
|
8021
|
+
});
|
|
8022
|
+
actionBtn('×', 'webchat-queued-cancel', function () {
|
|
8023
|
+
if (connection)
|
|
8024
|
+
connection.send({ type: 'queue_cancel', threadName: threadName, id: item.id });
|
|
8025
|
+
});
|
|
8026
|
+
|
|
8027
|
+
el.appendChild(actions);
|
|
8028
|
+
return el;
|
|
8029
|
+
}
|
|
8030
|
+
|
|
8031
|
+
// Edit a queued message in place. Same shape as the annotation popover —
|
|
8032
|
+
// quote, textarea, Cancel/Update — because it is the same act: revising
|
|
8033
|
+
// something you have staged but not yet sent.
|
|
8034
|
+
function showQueuedEditPopover(anchorEl, item) {
|
|
8035
|
+
var existing = panel.querySelector('.cumulus-queued-popover');
|
|
8036
|
+
if (existing) existing.remove();
|
|
8037
|
+
|
|
8038
|
+
var pop = document.createElement('div');
|
|
8039
|
+
pop.className = 'cumulus-annotation-popover cumulus-queued-popover';
|
|
8040
|
+
pop.setAttribute('data-testid', 'webchat-queued-popover');
|
|
8041
|
+
|
|
8042
|
+
var panelRect = panel.getBoundingClientRect();
|
|
8043
|
+
var anchorRect = anchorEl.getBoundingClientRect();
|
|
8044
|
+
var top = anchorRect.top - panelRect.top + panel.scrollTop - 170;
|
|
8045
|
+
if (top < 4) top = anchorRect.bottom - panelRect.top + panel.scrollTop + 4;
|
|
8046
|
+
var left = anchorRect.left - panelRect.left;
|
|
8047
|
+
if (left + 260 > panelRect.width) left = panelRect.width - 270;
|
|
8048
|
+
if (left < 4) left = 4;
|
|
8049
|
+
pop.style.top = top + 'px';
|
|
8050
|
+
pop.style.left = left + 'px';
|
|
8051
|
+
|
|
8052
|
+
var ta = document.createElement('textarea');
|
|
8053
|
+
ta.rows = 4;
|
|
8054
|
+
ta.value = item.text;
|
|
8055
|
+
ta.setAttribute('data-testid', 'webchat-queued-edit-input');
|
|
8056
|
+
pop.appendChild(ta);
|
|
8057
|
+
|
|
8058
|
+
var acts = document.createElement('div');
|
|
8059
|
+
acts.className = 'annotation-actions';
|
|
8060
|
+
var cancel = document.createElement('button');
|
|
8061
|
+
cancel.className = 'annotation-cancel';
|
|
8062
|
+
cancel.textContent = 'Cancel';
|
|
8063
|
+
cancel.addEventListener('click', function () {
|
|
8064
|
+
pop.remove();
|
|
8065
|
+
});
|
|
8066
|
+
var save = document.createElement('button');
|
|
8067
|
+
save.className = 'annotation-submit';
|
|
8068
|
+
save.textContent = 'Update';
|
|
8069
|
+
save.setAttribute('data-testid', 'webchat-queued-edit-submit');
|
|
8070
|
+
save.addEventListener('click', function () {
|
|
8071
|
+
var next = ta.value.trim();
|
|
8072
|
+
pop.remove();
|
|
8073
|
+
if (!next || next === item.text) return;
|
|
8074
|
+
if (connection)
|
|
8075
|
+
connection.send({
|
|
8076
|
+
type: 'queue_edit',
|
|
8077
|
+
threadName: threadName,
|
|
8078
|
+
id: item.id,
|
|
8079
|
+
message: next,
|
|
8080
|
+
});
|
|
8081
|
+
});
|
|
8082
|
+
acts.appendChild(cancel);
|
|
8083
|
+
acts.appendChild(save);
|
|
8084
|
+
pop.appendChild(acts);
|
|
8085
|
+
|
|
8086
|
+
panel.appendChild(pop);
|
|
8087
|
+
ta.focus();
|
|
8088
|
+
}
|
|
8089
|
+
|
|
7650
8090
|
function renderPanelMessages() {
|
|
7651
8091
|
// Destroy existing blex blocks before clearing DOM
|
|
7652
8092
|
messagesEl.querySelectorAll('.cumulus-msg.assistant').forEach(function (m) {
|
|
@@ -7731,6 +8171,16 @@
|
|
|
7731
8171
|
);
|
|
7732
8172
|
messagesEl.appendChild(row);
|
|
7733
8173
|
}
|
|
8174
|
+
// Deferred messages sit below the in-flight response — they are what
|
|
8175
|
+
// happens next (task 144).
|
|
8176
|
+
if (state.queued && state.queued.length) {
|
|
8177
|
+
for (var qi = 0; qi < state.queued.length; qi++) {
|
|
8178
|
+
var qRow = document.createElement('div');
|
|
8179
|
+
qRow.className = 'cumulus-msg-row';
|
|
8180
|
+
qRow.appendChild(buildQueuedMsgEl(state.queued[qi]));
|
|
8181
|
+
messagesEl.appendChild(qRow);
|
|
8182
|
+
}
|
|
8183
|
+
}
|
|
7734
8184
|
renderActivityBar();
|
|
7735
8185
|
scrollToBottom();
|
|
7736
8186
|
updateSelectionBar();
|
|
@@ -7877,15 +8327,19 @@
|
|
|
7877
8327
|
sendBtn.classList.add('stop');
|
|
7878
8328
|
sendBtn.disabled = false;
|
|
7879
8329
|
} else if (state.streaming && hasInput) {
|
|
7880
|
-
// Streaming with input —
|
|
7881
|
-
|
|
8330
|
+
// Streaming with input — the button QUEUES (task 144). Return still
|
|
8331
|
+
// interjects, so both intents stay reachable; the label is what makes
|
|
8332
|
+
// which one you are about to get visible before you commit.
|
|
8333
|
+
sendBtn.textContent = uploading ? 'Uploading…' : 'Queue';
|
|
7882
8334
|
sendBtn.classList.remove('stop');
|
|
7883
|
-
sendBtn.classList.
|
|
8335
|
+
sendBtn.classList.remove('interject');
|
|
8336
|
+
sendBtn.classList.add('queue');
|
|
7884
8337
|
sendBtn.disabled = uploading;
|
|
7885
8338
|
} else {
|
|
7886
8339
|
sendBtn.textContent = uploading ? 'Uploading…' : 'Send';
|
|
7887
8340
|
sendBtn.classList.remove('stop');
|
|
7888
8341
|
sendBtn.classList.remove('interject');
|
|
8342
|
+
sendBtn.classList.remove('queue');
|
|
7889
8343
|
sendBtn.disabled = uploading;
|
|
7890
8344
|
}
|
|
7891
8345
|
}
|
|
@@ -8036,12 +8490,69 @@
|
|
|
8036
8490
|
// Initial render (messages may already be loaded from a previous view)
|
|
8037
8491
|
renderPanelMessages();
|
|
8038
8492
|
|
|
8493
|
+
// Build the outgoing body + image payload from the current draft. Shared
|
|
8494
|
+
// by the send/interject path and the queue path (task 144) so a file
|
|
8495
|
+
// attachment behaves identically whichever one you use.
|
|
8496
|
+
function composePanelOutgoing(text) {
|
|
8497
|
+
var sendable = state.pendingAttachments.filter(function (a) {
|
|
8498
|
+
return a.isImage || a.status === 'done';
|
|
8499
|
+
});
|
|
8500
|
+
var images = sendable
|
|
8501
|
+
.filter(function (a) {
|
|
8502
|
+
return a.isImage;
|
|
8503
|
+
})
|
|
8504
|
+
.map(function (a) {
|
|
8505
|
+
return { mimeType: a.mimeType, base64: a.base64 };
|
|
8506
|
+
});
|
|
8507
|
+
var files = sendable.filter(function (a) {
|
|
8508
|
+
return !a.isImage && a.status === 'done' && a.path;
|
|
8509
|
+
});
|
|
8510
|
+
var body = text || '';
|
|
8511
|
+
if (files.length > 0) {
|
|
8512
|
+
var lines = files.map(function (a) {
|
|
8513
|
+
return '- ' + a.name + ' (' + formatFileSize(a.size || 0) + ') → ' + a.path;
|
|
8514
|
+
});
|
|
8515
|
+
body = 'Attached files:\n' + lines.join('\n') + (body ? '\n\n' + body : '');
|
|
8516
|
+
}
|
|
8517
|
+
return { body: body, images: images, attachments: sendable.slice() };
|
|
8518
|
+
}
|
|
8519
|
+
|
|
8520
|
+
// Clear the composer after a draft has been handed off.
|
|
8521
|
+
function clearPanelComposer() {
|
|
8522
|
+
inputEl.value = '';
|
|
8523
|
+
inputEl.style.height = 'auto';
|
|
8524
|
+
inputEl.style.overflowY = 'hidden';
|
|
8525
|
+
state.draft = '';
|
|
8526
|
+
state.pendingAttachments = [];
|
|
8527
|
+
renderPanelAttachStrip();
|
|
8528
|
+
}
|
|
8529
|
+
|
|
8039
8530
|
// ── Send message for this panel ──
|
|
8040
|
-
|
|
8531
|
+
// mode: 'queue' (the Queue button while streaming) or undefined/'send'
|
|
8532
|
+
// (Return, and the ordinary Send button) — see task 144.
|
|
8533
|
+
function sendPanelMessage(mode) {
|
|
8041
8534
|
// Prepend any deferred blex interaction chips to the message
|
|
8042
8535
|
var chipText = collectAndClearChips(panel);
|
|
8043
8536
|
var text = (chipText ? chipText + '\n' : '') + inputEl.value.trim();
|
|
8044
8537
|
|
|
8538
|
+
// ── Queue (task 144): wait for the current response to finish ──
|
|
8539
|
+
if (mode === 'queue' && state.streaming) {
|
|
8540
|
+
if (!text && state.pendingAttachments.length === 0) return;
|
|
8541
|
+
if (!connection) return;
|
|
8542
|
+
if (hasPanelUploading()) return;
|
|
8543
|
+
var q = composePanelOutgoing(text);
|
|
8544
|
+
var qPayload = {
|
|
8545
|
+
type: 'queue_message',
|
|
8546
|
+
threadName: threadName,
|
|
8547
|
+
message: q.body || ' ',
|
|
8548
|
+
};
|
|
8549
|
+
if (q.images.length > 0) qPayload.images = q.images;
|
|
8550
|
+
connection.send(qPayload);
|
|
8551
|
+
clearPanelComposer();
|
|
8552
|
+
updatePanelSendBtn();
|
|
8553
|
+
return;
|
|
8554
|
+
}
|
|
8555
|
+
|
|
8045
8556
|
// ── Interjection: user sends while streaming ──
|
|
8046
8557
|
// Freeze partial response, send the new message — server aborts the old stream
|
|
8047
8558
|
if (state.streaming) {
|
|
@@ -8094,10 +8605,8 @@
|
|
|
8094
8605
|
if (hasPanelUploading()) return; // button is disabled while uploading
|
|
8095
8606
|
|
|
8096
8607
|
// Drop attachments that failed to upload
|
|
8097
|
-
var
|
|
8098
|
-
|
|
8099
|
-
});
|
|
8100
|
-
var attachSnapshot = sendable.slice();
|
|
8608
|
+
var outgoing = composePanelOutgoing(text);
|
|
8609
|
+
var attachSnapshot = outgoing.attachments;
|
|
8101
8610
|
var displayText = text || '(attachment)';
|
|
8102
8611
|
state.messages.push({
|
|
8103
8612
|
role: 'user',
|
|
@@ -8107,12 +8616,7 @@
|
|
|
8107
8616
|
});
|
|
8108
8617
|
// Sending re-pins to the bottom — the user expects to see their turn (task 094).
|
|
8109
8618
|
state.pinnedToBottom = true;
|
|
8110
|
-
|
|
8111
|
-
inputEl.style.height = 'auto';
|
|
8112
|
-
inputEl.style.overflowY = 'hidden';
|
|
8113
|
-
state.draft = '';
|
|
8114
|
-
state.pendingAttachments = [];
|
|
8115
|
-
renderPanelAttachStrip();
|
|
8619
|
+
clearPanelComposer();
|
|
8116
8620
|
state.streaming = true;
|
|
8117
8621
|
state.stopRequested = false;
|
|
8118
8622
|
state.streamBuffer = '';
|
|
@@ -8121,43 +8625,25 @@
|
|
|
8121
8625
|
updatePanelSendBtn();
|
|
8122
8626
|
renderPanelMessages();
|
|
8123
8627
|
|
|
8124
|
-
var imagePayload = attachSnapshot
|
|
8125
|
-
.filter(function (a) {
|
|
8126
|
-
return a.isImage;
|
|
8127
|
-
})
|
|
8128
|
-
.map(function (a) {
|
|
8129
|
-
return { mimeType: a.mimeType, base64: a.base64 };
|
|
8130
|
-
});
|
|
8131
|
-
|
|
8132
|
-
// Build "Attached files:" prefix for non-image uploads
|
|
8133
|
-
var fileAttachments = attachSnapshot.filter(function (a) {
|
|
8134
|
-
return !a.isImage && a.status === 'done' && a.path;
|
|
8135
|
-
});
|
|
8136
|
-
var messageBody = text || '';
|
|
8137
|
-
if (fileAttachments.length > 0) {
|
|
8138
|
-
var lines = fileAttachments.map(function (a) {
|
|
8139
|
-
return '- ' + a.name + ' (' + formatFileSize(a.size || 0) + ') → ' + a.path;
|
|
8140
|
-
});
|
|
8141
|
-
messageBody =
|
|
8142
|
-
'Attached files:\n' + lines.join('\n') + (messageBody ? '\n\n' + messageBody : '');
|
|
8143
|
-
}
|
|
8144
|
-
|
|
8145
8628
|
var payload = {
|
|
8146
8629
|
type: 'message',
|
|
8147
8630
|
threadName: threadName,
|
|
8148
|
-
message:
|
|
8631
|
+
message: outgoing.body || ' ',
|
|
8149
8632
|
};
|
|
8150
|
-
if (
|
|
8633
|
+
if (outgoing.images.length > 0) payload.images = outgoing.images;
|
|
8151
8634
|
connection.send(payload);
|
|
8152
8635
|
}
|
|
8153
8636
|
|
|
8154
8637
|
// ── Panel event listeners ──
|
|
8155
|
-
|
|
8638
|
+
// The button queues while streaming; Return always interjects (task 144).
|
|
8639
|
+
sendBtn.addEventListener('click', function () {
|
|
8640
|
+
sendPanelMessage(state.streaming ? 'queue' : 'send');
|
|
8641
|
+
});
|
|
8156
8642
|
|
|
8157
8643
|
inputEl.addEventListener('keydown', function (e) {
|
|
8158
8644
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
8159
8645
|
e.preventDefault();
|
|
8160
|
-
sendPanelMessage();
|
|
8646
|
+
sendPanelMessage('send');
|
|
8161
8647
|
}
|
|
8162
8648
|
});
|
|
8163
8649
|
|
|
@@ -8989,6 +9475,35 @@
|
|
|
8989
9475
|
}
|
|
8990
9476
|
break;
|
|
8991
9477
|
|
|
9478
|
+
// ── Deferred user messages (task 144) ──
|
|
9479
|
+
// The server owns the queue, so its list is authoritative: every tab on
|
|
9480
|
+
// the thread renders exactly what is pending, including the one that
|
|
9481
|
+
// just queued it.
|
|
9482
|
+
case 'queued_messages':
|
|
9483
|
+
if (data.threadName) {
|
|
9484
|
+
var qState = getThreadState(data.threadName);
|
|
9485
|
+
qState.queued = data.messages || [];
|
|
9486
|
+
refreshThreadPanel(data.threadName);
|
|
9487
|
+
}
|
|
9488
|
+
break;
|
|
9489
|
+
|
|
9490
|
+
// The queue drained and is now a real turn. Nobody sent it from a
|
|
9491
|
+
// client, so nobody rendered the user half — do it here, or the chip
|
|
9492
|
+
// vanishes and the reply answers a question the transcript never shows.
|
|
9493
|
+
case 'queue_delivered':
|
|
9494
|
+
if (data.threadName) {
|
|
9495
|
+
var deliveredState = getThreadState(data.threadName);
|
|
9496
|
+
deliveredState.queued = [];
|
|
9497
|
+
deliveredState.messages.push({
|
|
9498
|
+
role: 'user',
|
|
9499
|
+
content: data.text,
|
|
9500
|
+
timestamp: Date.now(),
|
|
9501
|
+
});
|
|
9502
|
+
deliveredState.pinnedToBottom = true;
|
|
9503
|
+
refreshThreadPanel(data.threadName);
|
|
9504
|
+
}
|
|
9505
|
+
break;
|
|
9506
|
+
|
|
8992
9507
|
// ── Include/Revert responses ──
|
|
8993
9508
|
case 'include_list':
|
|
8994
9509
|
if (data.threadName && data.files) {
|