newmark-agent 0.4.9 → 0.5.0
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/conversation-utility-host.bundle.cjs +118 -11
- package/dist/core/conversationKernel.d.ts +2 -0
- package/dist/core/conversationKernel.js +68 -10
- package/dist/core/workEventCoalescer.d.ts +16 -0
- package/dist/core/workEventCoalescer.js +52 -0
- package/dist/llm/provider.d.ts +5 -0
- package/dist/llm/provider.js +63 -1
- package/dist/main.js +21 -2
- package/dist/server.d.ts +1 -0
- package/dist/server.js +181 -3
- package/dist/ui/index.html +145 -37
- package/dist/wsl-agent-host.bundle.cjs +118 -11
- package/package.json +6 -4
package/dist/server.js
CHANGED
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.MOBILE_WORKSPACE_UPLOAD_MAX_BYTES = void 0;
|
|
36
37
|
exports.configureHostedServer = configureHostedServer;
|
|
37
38
|
exports.stopHostedServer = stopHostedServer;
|
|
38
39
|
exports.hostedServerStatus = hostedServerStatus;
|
|
@@ -43,6 +44,7 @@ const http = __importStar(require("http"));
|
|
|
43
44
|
const fs = __importStar(require("fs"));
|
|
44
45
|
const path = __importStar(require("path"));
|
|
45
46
|
const os = __importStar(require("os"));
|
|
47
|
+
const crypto_1 = require("crypto");
|
|
46
48
|
const uiPreferences_1 = require("./core/uiPreferences");
|
|
47
49
|
const child_process_1 = require("child_process");
|
|
48
50
|
const agent_1 = require("./core/agent");
|
|
@@ -53,6 +55,7 @@ const workspaceFileRouter_1 = require("./core/workspaceFileRouter");
|
|
|
53
55
|
const nativeBash_1 = require("./core/nativeBash");
|
|
54
56
|
const installUpdate_1 = require("./core/installUpdate");
|
|
55
57
|
const mobilePairing_1 = require("./core/mobilePairing");
|
|
58
|
+
const workEventCoalescer_1 = require("./core/workEventCoalescer");
|
|
56
59
|
const PORT = 47890;
|
|
57
60
|
let agent = null;
|
|
58
61
|
let automation = null;
|
|
@@ -86,9 +89,12 @@ function publishMobileWorkEvent(event) {
|
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
function publishServerWorkEvent(event) {
|
|
92
|
+
serverWorkEventCoalescer.push(event);
|
|
93
|
+
}
|
|
94
|
+
const serverWorkEventCoalescer = new workEventCoalescer_1.WorkEventCoalescer(event => {
|
|
89
95
|
publishMobileWorkEvent(event);
|
|
90
96
|
hostedWorkEventSink?.(event);
|
|
91
|
-
}
|
|
97
|
+
});
|
|
92
98
|
function mobileAuthorized(req) {
|
|
93
99
|
if (!mobileToken)
|
|
94
100
|
return false;
|
|
@@ -124,6 +130,8 @@ const MOBILE_EDITABLE_EXTENSIONS = new Set([
|
|
|
124
130
|
'.bash', '.zsh', '.ps1', '.bat', '.cmd', '.sql', '.tex', '.typ', '.properties', '.gradle', '.gitignore',
|
|
125
131
|
]);
|
|
126
132
|
const MOBILE_EDITOR_MAX_BYTES = 1024 * 1024;
|
|
133
|
+
const MAX_API_BODY_BYTES = 30 * 1024 * 1024;
|
|
134
|
+
exports.MOBILE_WORKSPACE_UPLOAD_MAX_BYTES = 64 * 1024 * 1024;
|
|
127
135
|
function resolveMobileWorkspacePath(ws, relativePath) {
|
|
128
136
|
const root = path.resolve(ws.path);
|
|
129
137
|
const clean = String(relativePath || '').replace(/\\/g, '/').replace(/^\/+/, '');
|
|
@@ -142,6 +150,156 @@ function mobileEditableFile(target) {
|
|
|
142
150
|
const base = path.basename(target).toLowerCase();
|
|
143
151
|
return MOBILE_EDITABLE_EXTENSIONS.has(path.extname(base)) || MOBILE_EDITABLE_EXTENSIONS.has(base);
|
|
144
152
|
}
|
|
153
|
+
function validMobileUploadDirectory(value) {
|
|
154
|
+
const clean = String(value || '').replace(/\\/g, '/');
|
|
155
|
+
return !path.isAbsolute(clean)
|
|
156
|
+
&& !/^[A-Za-z]:\//.test(clean)
|
|
157
|
+
&& !clean.split('/').some(segment => segment === '..' || segment.includes('\0'));
|
|
158
|
+
}
|
|
159
|
+
function validMobileUploadFileName(value) {
|
|
160
|
+
const clean = String(value || '');
|
|
161
|
+
return clean.length > 0
|
|
162
|
+
&& clean.length <= 255
|
|
163
|
+
&& clean !== '.'
|
|
164
|
+
&& clean !== '..'
|
|
165
|
+
&& !clean.includes('/')
|
|
166
|
+
&& !clean.includes('\\')
|
|
167
|
+
&& !clean.includes('\0')
|
|
168
|
+
&& path.basename(clean) === clean;
|
|
169
|
+
}
|
|
170
|
+
function numberedUploadName(fileName, index) {
|
|
171
|
+
if (index <= 0)
|
|
172
|
+
return fileName;
|
|
173
|
+
const extension = path.extname(fileName);
|
|
174
|
+
const stem = fileName.slice(0, fileName.length - extension.length);
|
|
175
|
+
return `${stem} (${index})${extension}`;
|
|
176
|
+
}
|
|
177
|
+
async function handleMobileWorkspaceUpload(req, res) {
|
|
178
|
+
if (agent && !agent.config.getBool('remote', 'touch_enabled')) {
|
|
179
|
+
mobileJson(res, { error: 'Remote touch disabled' }, 403);
|
|
180
|
+
req.resume();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (!mobileAuthorized(req)) {
|
|
184
|
+
mobileJson(res, { error: 'Unauthorized' }, 401);
|
|
185
|
+
req.resume();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (!agent) {
|
|
189
|
+
mobileJson(res, { error: 'Agent not initialized' }, 500);
|
|
190
|
+
req.resume();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const url = new URL(req.url || '/', `http://localhost:${PORT}`);
|
|
194
|
+
const workspaceId = url.searchParams.get('workspaceId') || '';
|
|
195
|
+
const directory = url.searchParams.get('directory') || '';
|
|
196
|
+
const fileName = url.searchParams.get('fileName') || '';
|
|
197
|
+
const ws = resolveMobileWorkspace(agent, workspaceId);
|
|
198
|
+
if (!ws) {
|
|
199
|
+
mobileJson(res, { error: 'Unknown workspace' }, 404);
|
|
200
|
+
req.resume();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (!validMobileUploadDirectory(directory) || !validMobileUploadFileName(fileName)) {
|
|
204
|
+
mobileJson(res, { error: 'Invalid upload path or file name' }, 400);
|
|
205
|
+
req.resume();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const declaredLength = Number(req.headers['content-length'] || -1);
|
|
209
|
+
if (Number.isFinite(declaredLength) && declaredLength > exports.MOBILE_WORKSPACE_UPLOAD_MAX_BYTES) {
|
|
210
|
+
mobileJson(res, { error: 'Upload exceeds the 64 MiB limit' }, 413);
|
|
211
|
+
req.resume();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
let temporaryPath = '';
|
|
215
|
+
try {
|
|
216
|
+
const resolved = resolveMobileWorkspacePath(ws, directory);
|
|
217
|
+
const realRoot = await fs.promises.realpath(resolved.root);
|
|
218
|
+
if (directory === 'Uploaded') {
|
|
219
|
+
await fs.promises.mkdir(resolved.target, { recursive: true });
|
|
220
|
+
}
|
|
221
|
+
const realDirectory = await fs.promises.realpath(resolved.target);
|
|
222
|
+
const canonicalRelative = path.relative(realRoot, realDirectory);
|
|
223
|
+
if (canonicalRelative === '..' || canonicalRelative.startsWith(`..${path.sep}`) || path.isAbsolute(canonicalRelative)) {
|
|
224
|
+
throw new Error('Path escapes workspace through a symbolic link');
|
|
225
|
+
}
|
|
226
|
+
const directoryStat = await fs.promises.stat(realDirectory);
|
|
227
|
+
if (!directoryStat.isDirectory()) {
|
|
228
|
+
mobileJson(res, { error: 'Upload destination is not a directory' }, 400);
|
|
229
|
+
req.resume();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
temporaryPath = path.join(realDirectory, `.newmark-upload-${(0, crypto_1.randomUUID)()}.tmp`);
|
|
233
|
+
const handle = await fs.promises.open(temporaryPath, 'wx', 0o600);
|
|
234
|
+
const hash = (0, crypto_1.createHash)('sha256');
|
|
235
|
+
let size = 0;
|
|
236
|
+
try {
|
|
237
|
+
for await (const part of req) {
|
|
238
|
+
const chunk = Buffer.isBuffer(part) ? part : Buffer.from(part);
|
|
239
|
+
size += chunk.length;
|
|
240
|
+
if (size > exports.MOBILE_WORKSPACE_UPLOAD_MAX_BYTES)
|
|
241
|
+
throw new RangeError('Upload exceeds the 64 MiB limit');
|
|
242
|
+
hash.update(chunk);
|
|
243
|
+
let offset = 0;
|
|
244
|
+
while (offset < chunk.length) {
|
|
245
|
+
const { bytesWritten } = await handle.write(chunk, offset, chunk.length - offset);
|
|
246
|
+
if (bytesWritten <= 0)
|
|
247
|
+
throw new Error('Unable to write the complete upload');
|
|
248
|
+
offset += bytesWritten;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
await handle.sync();
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
await handle.close();
|
|
255
|
+
}
|
|
256
|
+
let finalPath = '';
|
|
257
|
+
let finalName = '';
|
|
258
|
+
let published = false;
|
|
259
|
+
for (let index = 0; index < 10_000; index++) {
|
|
260
|
+
finalName = numberedUploadName(fileName, index);
|
|
261
|
+
finalPath = path.join(realDirectory, finalName);
|
|
262
|
+
try {
|
|
263
|
+
// A hard link publishes the already-complete inode atomically and fails
|
|
264
|
+
// with EEXIST, so parallel uploads can never overwrite one another.
|
|
265
|
+
await fs.promises.link(temporaryPath, finalPath);
|
|
266
|
+
published = true;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
if (error?.code === 'EEXIST')
|
|
271
|
+
continue;
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (!published)
|
|
276
|
+
throw new Error('Unable to allocate a unique upload name');
|
|
277
|
+
await fs.promises.unlink(temporaryPath);
|
|
278
|
+
temporaryPath = '';
|
|
279
|
+
const relativePath = path.posix.join(resolved.relative, finalName).replace(/^\.\//, '');
|
|
280
|
+
const suppliedMime = String(req.headers['content-type'] || 'application/octet-stream').split(';')[0].trim();
|
|
281
|
+
const mimeType = /^[\w.+-]+\/[\w.+-]+$/.test(suppliedMime) ? suppliedMime : 'application/octet-stream';
|
|
282
|
+
mobileJson(res, {
|
|
283
|
+
ok: true,
|
|
284
|
+
file: { name: finalName, path: relativePath, size, mimeType, sha256: hash.digest('hex'), created: true },
|
|
285
|
+
}, 201);
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
if (temporaryPath)
|
|
289
|
+
await fs.promises.unlink(temporaryPath).catch(() => { });
|
|
290
|
+
if (error instanceof RangeError)
|
|
291
|
+
mobileJson(res, { error: error.message }, 413);
|
|
292
|
+
else if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') {
|
|
293
|
+
mobileJson(res, { error: 'Upload destination does not exist' }, 400);
|
|
294
|
+
}
|
|
295
|
+
else if (String(error?.message || '').toLowerCase().includes('workspace')) {
|
|
296
|
+
mobileJson(res, { error: 'Invalid upload destination' }, 400);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
mobileJson(res, { error: 'Upload could not be stored' }, 500);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
145
303
|
function mobileRightSidebarState(current, ws, conversationId) {
|
|
146
304
|
const scoped = mobileScopedAgent(ws, conversationId);
|
|
147
305
|
return {
|
|
@@ -1483,10 +1641,30 @@ function startServer(root, options = {}) {
|
|
|
1483
1641
|
handleMobileEvents(req, res);
|
|
1484
1642
|
return;
|
|
1485
1643
|
}
|
|
1644
|
+
if (req.method === 'POST' && url.pathname === '/api/mobile/workspace-file-upload') {
|
|
1645
|
+
await handleMobileWorkspaceUpload(req, res);
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1486
1648
|
if (req.method === 'POST' && url.pathname.startsWith('/api/')) {
|
|
1487
1649
|
let body = '';
|
|
1488
|
-
|
|
1489
|
-
|
|
1650
|
+
let bodyBytes = 0;
|
|
1651
|
+
let rejected = false;
|
|
1652
|
+
req.on('data', chunk => {
|
|
1653
|
+
if (rejected)
|
|
1654
|
+
return;
|
|
1655
|
+
bodyBytes += Buffer.byteLength(chunk);
|
|
1656
|
+
if (bodyBytes > MAX_API_BODY_BYTES) {
|
|
1657
|
+
rejected = true;
|
|
1658
|
+
if (url.pathname.startsWith('/api/mobile/'))
|
|
1659
|
+
mobileJson(res, { error: 'Request body too large' }, 413);
|
|
1660
|
+
else
|
|
1661
|
+
jsonResponse(res, { error: 'Request body too large' }, 413);
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
body += chunk;
|
|
1665
|
+
});
|
|
1666
|
+
req.on('end', () => { if (!rejected)
|
|
1667
|
+
handleApi(req, res, body); });
|
|
1490
1668
|
return;
|
|
1491
1669
|
}
|
|
1492
1670
|
if (url.pathname.startsWith('/api/')) {
|
package/dist/ui/index.html
CHANGED
|
@@ -6156,6 +6156,8 @@ var state = {
|
|
|
6156
6156
|
agentWorkUiByConversation: {},
|
|
6157
6157
|
workRunsByTarget: {},
|
|
6158
6158
|
workRunsByBranch: {},
|
|
6159
|
+
conversationMessagesByTarget: {},
|
|
6160
|
+
pendingConversationActivations: {},
|
|
6159
6161
|
runtimeBranchPathsByTarget: {},
|
|
6160
6162
|
workRunAnchorIndexesByTarget: {},
|
|
6161
6163
|
guideMessagesByTarget: {},
|
|
@@ -9656,11 +9658,18 @@ function recordGuideUiMessage(input, target) {
|
|
|
9656
9658
|
createdAt: String(receipt.createdAt || input.createdAt || (prior && prior.createdAt) || new Date().toISOString()),
|
|
9657
9659
|
updatedAt: String(receipt.updatedAt || input.updatedAt || new Date().toISOString()),
|
|
9658
9660
|
reason: explicitReason || (status === 'deferred' || status === 'rejected' ? String(prior && prior.reason || '') : ''),
|
|
9661
|
+
awaitingAck: input.awaitingAck === true || receipt.awaitingAck === true,
|
|
9659
9662
|
attachments: normalizeConversationImageAttachments(input.attachments || receipt.attachments || (prior && prior.attachments) || [])
|
|
9660
9663
|
};
|
|
9661
9664
|
messages[clientMessageId] = record;
|
|
9662
9665
|
if (isActiveConversationTarget(target)) {
|
|
9663
9666
|
var element = findGuideMessageElement(clientMessageId);
|
|
9667
|
+
if (element) {
|
|
9668
|
+
if (prior && prior.awaitingAck && !record.awaitingAck && element.closest && element.closest('.work-run-pending-guides')) {
|
|
9669
|
+
element.remove();
|
|
9670
|
+
element = null;
|
|
9671
|
+
}
|
|
9672
|
+
}
|
|
9664
9673
|
if (element) {
|
|
9665
9674
|
applyGuideMessageMeta(element, record);
|
|
9666
9675
|
if (record.runId && !element.classList.contains('work-run-guide-message')) element.remove();
|
|
@@ -9701,6 +9710,26 @@ function renderPendingGuideMessages(target, persistedGuideIds) {
|
|
|
9701
9710
|
});
|
|
9702
9711
|
pending.forEach(function(item) {
|
|
9703
9712
|
var element = findGuideMessageElement(item.clientMessageId);
|
|
9713
|
+
if (item.awaitingAck && item.runId) {
|
|
9714
|
+
var runElement = findWorkRunElement({ runId: item.runId });
|
|
9715
|
+
var runBody = runElement && runElement.querySelector('.conversation-work-run-body');
|
|
9716
|
+
if (runBody) {
|
|
9717
|
+
var pendingStack = runBody.querySelector(':scope > .work-run-pending-guides');
|
|
9718
|
+
if (!pendingStack) {
|
|
9719
|
+
pendingStack = document.createElement('div');
|
|
9720
|
+
pendingStack.className = 'work-run-pending-guides';
|
|
9721
|
+
runBody.appendChild(pendingStack);
|
|
9722
|
+
}
|
|
9723
|
+
if (!element || !pendingStack.contains(element)) {
|
|
9724
|
+
var holder = document.createElement('div');
|
|
9725
|
+
holder.innerHTML = renderWorkRunGuideMessage(Object.assign({}, item, { status: 'accepted' }));
|
|
9726
|
+
element = holder.firstElementChild;
|
|
9727
|
+
if (element) pendingStack.appendChild(element);
|
|
9728
|
+
}
|
|
9729
|
+
if (element) applyGuideMessageMeta(element, item);
|
|
9730
|
+
return;
|
|
9731
|
+
}
|
|
9732
|
+
}
|
|
9704
9733
|
if (element) applyGuideMessageMeta(element, item);
|
|
9705
9734
|
else addMsg('user', item.content || 'Guide', 'guide', item.model || state.model, undefined, {
|
|
9706
9735
|
clientMessageId: item.clientMessageId,
|
|
@@ -9964,7 +9993,7 @@ function normalizedWorkRun(run, target) {
|
|
|
9964
9993
|
var status = String(run.status || 'running');
|
|
9965
9994
|
var live = ['running', 'stopping', 'force_restarting'].indexOf(status) >= 0;
|
|
9966
9995
|
var expanded = run.expanded === undefined
|
|
9967
|
-
? live && state.expandToolsDefault !== false
|
|
9996
|
+
? (status === 'error' || (live && state.expandToolsDefault !== false))
|
|
9968
9997
|
: !!run.expanded;
|
|
9969
9998
|
return {
|
|
9970
9999
|
runId: String(run.runId || run.id || ('run-' + Date.now() + '-' + Math.random().toString(16).slice(2))),
|
|
@@ -10632,6 +10661,7 @@ function updateConversationWorkRunElement(run, element) {
|
|
|
10632
10661
|
'<span class="conversation-work-run-status">' + esc(String(run.status || '')) + '</span>' +
|
|
10633
10662
|
'<span class="conversation-work-run-chevron" aria-hidden="true"></span></button>' +
|
|
10634
10663
|
'<div class="conversation-work-run-body">' + renderWorkRunEvents(run, expanded) + '</div>';
|
|
10664
|
+
renderPendingGuideMessages(run.target, {});
|
|
10635
10665
|
var wrapper = element.closest ? element.closest('.work-run-message') : null;
|
|
10636
10666
|
if (wrapper) {
|
|
10637
10667
|
wrapper.classList.toggle('flow-result-only', !!(run.flow && run.flow.activityVisibility === 'result-only'));
|
|
@@ -10778,14 +10808,14 @@ function applyAgentWorkEventToRun(event) {
|
|
|
10778
10808
|
}
|
|
10779
10809
|
run.sequence = Math.max(Number(run.sequence || 0), Number(event.sequence || 0));
|
|
10780
10810
|
if (type === 'done') { run.status = 'completed'; run.endedAt = event.endedAt || event.timestampIso || new Date().toISOString(); if (run.userToggled !== true) run.expanded = false; }
|
|
10781
|
-
else if (type === 'error') { run.status = 'error'; run.endedAt = event.endedAt || event.timestampIso || new Date().toISOString(); if (run.userToggled !== true) run.expanded =
|
|
10811
|
+
else if (type === 'error') { run.status = 'error'; run.endedAt = event.endedAt || event.timestampIso || new Date().toISOString(); if (run.userToggled !== true) run.expanded = true; }
|
|
10782
10812
|
else if (type === 'interrupted') { run.status = 'interrupted'; run.endedAt = event.endedAt || event.timestampIso || new Date().toISOString(); if (run.userToggled !== true) run.expanded = false; }
|
|
10783
10813
|
else if (type === 'force_interrupted') { run.status = 'force_interrupted'; run.endedAt = event.endedAt || event.timestampIso || new Date().toISOString(); if (run.userToggled !== true) run.expanded = false; }
|
|
10784
10814
|
else if (event.status && type.indexOf('guide') !== 0) {
|
|
10785
10815
|
run.status = String(event.status);
|
|
10786
10816
|
if (['completed', 'interrupted', 'force_interrupted', 'error'].indexOf(run.status) >= 0) {
|
|
10787
10817
|
run.endedAt = event.endedAt || event.timestampIso || new Date().toISOString();
|
|
10788
|
-
if (run.userToggled !== true) run.expanded =
|
|
10818
|
+
if (run.userToggled !== true) run.expanded = run.status === 'error';
|
|
10789
10819
|
}
|
|
10790
10820
|
}
|
|
10791
10821
|
if (isActiveConversationTarget(target) && (!branchIds.viewed || !eventBranchId || branchIds.viewed === eventBranchId)) {
|
|
@@ -11066,14 +11096,58 @@ function renderPersistedToolMessage(message) {
|
|
|
11066
11096
|
}, resultPrefix.test(content) || /\bcompleted\.?$/i.test(content) ? 'completed' : '');
|
|
11067
11097
|
}
|
|
11068
11098
|
|
|
11069
|
-
function
|
|
11070
|
-
|
|
11099
|
+
function conversationMessageCache(target) {
|
|
11100
|
+
var scopedTarget = target || currentConversationTarget();
|
|
11101
|
+
var key = runtimeKeyFor(scopedTarget.workspaceId, scopedTarget.conversationId);
|
|
11102
|
+
if (!state.conversationMessagesByTarget) state.conversationMessagesByTarget = {};
|
|
11103
|
+
return Array.isArray(state.conversationMessagesByTarget[key])
|
|
11104
|
+
? state.conversationMessagesByTarget[key].map(function(message) { return Object.assign({}, message); })
|
|
11105
|
+
: [];
|
|
11106
|
+
}
|
|
11107
|
+
|
|
11108
|
+
function cacheConversationMessages(messages, target) {
|
|
11109
|
+
var scopedTarget = target || currentConversationTarget();
|
|
11110
|
+
var key = runtimeKeyFor(scopedTarget.workspaceId, scopedTarget.conversationId);
|
|
11111
|
+
var cloned = Array.isArray(messages) ? messages.map(function(message) { return Object.assign({}, message); }) : [];
|
|
11112
|
+
if (!state.conversationMessagesByTarget) state.conversationMessagesByTarget = {};
|
|
11113
|
+
state.conversationMessagesByTarget[key] = cloned;
|
|
11114
|
+
return cloned.map(function(message) { return Object.assign({}, message); });
|
|
11115
|
+
}
|
|
11116
|
+
|
|
11117
|
+
function conversationHasReadableHistory(target) {
|
|
11118
|
+
return conversationMessageCache(target).length > 0 || workRunsForTarget(target).length > 0;
|
|
11119
|
+
}
|
|
11120
|
+
|
|
11121
|
+
function snapshotHasReadableConversationHistory(snapshot) {
|
|
11122
|
+
return !!(snapshot && ((Array.isArray(snapshot.chatMessages) && snapshot.chatMessages.length > 0)
|
|
11123
|
+
|| (Array.isArray(snapshot.workRuns) && snapshot.workRuns.length > 0)));
|
|
11124
|
+
}
|
|
11125
|
+
|
|
11126
|
+
function renderConversationHistoryFirst(target) {
|
|
11127
|
+
if (!isActiveConversationTarget(target)) return false;
|
|
11128
|
+
var cachedMessages = conversationMessageCache(target);
|
|
11129
|
+
if (cachedMessages.length > 0) {
|
|
11130
|
+
renderChatMessages(cachedMessages, target);
|
|
11131
|
+
renderLoadEarlierButton();
|
|
11132
|
+
return true;
|
|
11133
|
+
}
|
|
11134
|
+
if (workRunsForTarget(target).length > 0) {
|
|
11135
|
+
renderChatMessages([], target);
|
|
11136
|
+
renderLoadEarlierButton();
|
|
11137
|
+
return true;
|
|
11138
|
+
}
|
|
11139
|
+
return false;
|
|
11140
|
+
}
|
|
11141
|
+
|
|
11142
|
+
function renderChatMessages(messages, target) {
|
|
11143
|
+
var renderTarget = target || currentConversationTarget(activeConversationId());
|
|
11144
|
+
var normalizedMessages = cacheConversationMessages(messages, renderTarget);
|
|
11145
|
+
state.renderedChatMessages = normalizedMessages;
|
|
11071
11146
|
if (!els['chat-area']) return;
|
|
11072
11147
|
var chatWasAtBottom = shouldAutoScroll(els['chat-area']);
|
|
11073
11148
|
els['chat-area'].innerHTML = '';
|
|
11074
11149
|
if (chatWasAtBottom) _chatShouldAutoScroll = true;
|
|
11075
|
-
var renderConversationId =
|
|
11076
|
-
var renderTarget = currentConversationTarget(renderConversationId);
|
|
11150
|
+
var renderConversationId = renderTarget.conversationId;
|
|
11077
11151
|
var guideMessages = guideMessagesForTarget(renderTarget);
|
|
11078
11152
|
var persistedGuideIds = {};
|
|
11079
11153
|
var persistedRuns = workRunsForTarget(renderTarget).slice();
|
|
@@ -11099,7 +11173,7 @@ function renderChatMessages(messages) {
|
|
|
11099
11173
|
persistedRunIndexes[runId] = runIndex;
|
|
11100
11174
|
}
|
|
11101
11175
|
});
|
|
11102
|
-
|
|
11176
|
+
normalizedMessages.forEach(function(message, messageIndex) {
|
|
11103
11177
|
var runId = String(message && message.runId || '');
|
|
11104
11178
|
var role = String(message && message.role || '');
|
|
11105
11179
|
var indexedGuideId = String(message && message.clientMessageId || '');
|
|
@@ -11144,7 +11218,7 @@ function renderChatMessages(messages) {
|
|
|
11144
11218
|
var pendingWorkReview = conversationWorkUiState(renderConversationId).pendingWorkReview;
|
|
11145
11219
|
resetConversationWorkUi(renderConversationId, false);
|
|
11146
11220
|
conversationWorkUiState(renderConversationId).pendingWorkReview = pendingWorkReview;
|
|
11147
|
-
if (!
|
|
11221
|
+
if (!normalizedMessages.length) {
|
|
11148
11222
|
persistedRuns.forEach(function(run) { renderOwnedWorkRun(run, true); });
|
|
11149
11223
|
var pendingGuideCount = renderPendingGuideMessages(renderTarget, persistedGuideIds);
|
|
11150
11224
|
if (!persistedRuns.length && !pendingGuideCount) {
|
|
@@ -11582,8 +11656,19 @@ function loadActiveConversationMessages(conversationId) {
|
|
|
11582
11656
|
requestedConversationId === String(activeConversationId() || 'default') &&
|
|
11583
11657
|
responseConversationId === requestedConversationId;
|
|
11584
11658
|
if (!responseIsCurrent) return;
|
|
11585
|
-
applyConversationSnapshot(s, requestedConversationId);
|
|
11586
|
-
}).catch(function(){
|
|
11659
|
+
applyConversationSnapshot(s, requestedConversationId, { preserveReadableHistory: true, allowEmptyHistory: true });
|
|
11660
|
+
}).catch(function(error) {
|
|
11661
|
+
var stillCurrent = loadGeneration === state.conversationLoadGeneration &&
|
|
11662
|
+
requestedWorkspaceKey === currentWorkspaceKey() &&
|
|
11663
|
+
requestedConversationId === String(activeConversationId() || 'default');
|
|
11664
|
+
if (!stillCurrent) return;
|
|
11665
|
+
showUiNotice(
|
|
11666
|
+
currentLang() === 'zh' ? '历史记录增强加载失败,已保留当前内容。' : 'History enhancement failed; current content was preserved.',
|
|
11667
|
+
'error',
|
|
11668
|
+
'conversation-history-load-' + requestedWorkspaceKey + '-' + requestedConversationId
|
|
11669
|
+
);
|
|
11670
|
+
console.error('[Conversation] history enhancement failed:', error && error.message ? error.message : String(error));
|
|
11671
|
+
});
|
|
11587
11672
|
}
|
|
11588
11673
|
|
|
11589
11674
|
function hydrateConversationBranchState(snapshot) {
|
|
@@ -11600,7 +11685,7 @@ function hydrateConversationBranchState(snapshot) {
|
|
|
11600
11685
|
rememberRuntimeConversationBranchPath(currentConversationTarget(s.conversationId || activeConversationId()));
|
|
11601
11686
|
}
|
|
11602
11687
|
|
|
11603
|
-
function applyConversationSnapshot(s, requestedConversationId) {
|
|
11688
|
+
function applyConversationSnapshot(s, requestedConversationId, options) {
|
|
11604
11689
|
if (!s) return;
|
|
11605
11690
|
if (s.computerUse) {
|
|
11606
11691
|
var computerTarget = currentConversationTarget(s.conversationId || requestedConversationId || activeConversationId());
|
|
@@ -11616,18 +11701,21 @@ function applyConversationSnapshot(s, requestedConversationId) {
|
|
|
11616
11701
|
workspaceId: String((s && (s.workspaceId || (s.target && s.target.workspaceId))) || runtimeWorkspaceId('')),
|
|
11617
11702
|
conversationId: String((s && (s.conversationId || (s.target && s.target.conversationId))) || requestedConversationId)
|
|
11618
11703
|
};
|
|
11704
|
+
var preserveReadableHistory = !!(options && options.preserveReadableHistory
|
|
11705
|
+
&& !snapshotHasReadableConversationHistory(s)
|
|
11706
|
+
&& (!options.allowEmptyHistory || conversationHasReadableHistory(snapshotTarget)));
|
|
11619
11707
|
if (s && s.runtimeKey) registerRuntimeKey(snapshotTarget, s.runtimeKey);
|
|
11620
11708
|
hydrateConversationBranchState(s);
|
|
11621
11709
|
rebindQueueToRuntimeBranch(snapshotTarget);
|
|
11622
|
-
if (s && Array.isArray(s.workRuns)) {
|
|
11710
|
+
if (s && Array.isArray(s.workRuns) && !preserveReadableHistory) {
|
|
11623
11711
|
var viewedBranchIdForSync = Array.isArray(s.viewedBranchNodePath) && s.viewedBranchNodePath.length
|
|
11624
11712
|
? String(s.viewedBranchNodePath[s.viewedBranchNodePath.length - 1])
|
|
11625
11713
|
: '';
|
|
11626
11714
|
syncWorkRunsSnapshot(s.workRuns, snapshotTarget, viewedBranchIdForSync || undefined);
|
|
11627
11715
|
}
|
|
11628
11716
|
applyAutoRouteRatingState(s);
|
|
11629
|
-
if (s && s.chatMessages) {
|
|
11630
|
-
renderChatMessages(s.chatMessages);
|
|
11717
|
+
if (s && Array.isArray(s.chatMessages) && !preserveReadableHistory) {
|
|
11718
|
+
renderChatMessages(s.chatMessages, snapshotTarget);
|
|
11631
11719
|
renderLoadEarlierButton();
|
|
11632
11720
|
}
|
|
11633
11721
|
if (s && s.conversationId) {
|
|
@@ -13513,18 +13601,10 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
|
|
|
13513
13601
|
attachments: optimisticAttachments,
|
|
13514
13602
|
model: state.model,
|
|
13515
13603
|
createdAt: envelope.createdAt,
|
|
13604
|
+
awaitingAck: true,
|
|
13516
13605
|
allowStatusReset: retryingUnacknowledgedGuide
|
|
13517
13606
|
}, lockedTarget);
|
|
13518
|
-
|
|
13519
|
-
id: 'guide-' + clientMessageId + '-accepted',
|
|
13520
|
-
type: 'guide_accepted',
|
|
13521
|
-
content: displayText,
|
|
13522
|
-
workspaceId: lockedTarget.workspaceId,
|
|
13523
|
-
conversationId: lockedTarget.conversationId,
|
|
13524
|
-
runId: envelope.runId,
|
|
13525
|
-
clientMessageId: clientMessageId,
|
|
13526
|
-
guide: optimisticGuide
|
|
13527
|
-
});
|
|
13607
|
+
renderPendingGuideMessages(lockedTarget, {});
|
|
13528
13608
|
var enqueue = api.enqueueGuide ? api.enqueueGuide(envelope) : api.sendMessage(requestMessage, lockedConversationId);
|
|
13529
13609
|
return Promise.resolve(enqueue).then(function(receipt) {
|
|
13530
13610
|
if (!receipt || typeof receipt !== 'object' || !String(receipt.status || '').trim()) {
|
|
@@ -13680,6 +13760,8 @@ window.sendMessage = async function(modeOverride, queuedText, opts) {
|
|
|
13680
13760
|
var responseMsg = null;
|
|
13681
13761
|
var sendFailure = '';
|
|
13682
13762
|
try {
|
|
13763
|
+
var activationBeforeSend = pendingConversationActivation(lockedTarget);
|
|
13764
|
+
if (activationBeforeSend) await activationBeforeSend;
|
|
13683
13765
|
var sendPromise = api.sendMessage(requestMessage, lockedTarget);
|
|
13684
13766
|
if (requestedMode === 'goal') activateSubmittedGoal(opts.goalObjective || rawText);
|
|
13685
13767
|
var r = await sendPromise;
|
|
@@ -19348,6 +19430,12 @@ function holdForegroundConversation(id, ms) {
|
|
|
19348
19430
|
state.foregroundConversationHoldUntil = Date.now() + (ms || 4500);
|
|
19349
19431
|
}
|
|
19350
19432
|
|
|
19433
|
+
function pendingConversationActivation(target) {
|
|
19434
|
+
var scopedTarget = target || currentConversationTarget();
|
|
19435
|
+
var key = runtimeKeyFor(scopedTarget.workspaceId, scopedTarget.conversationId);
|
|
19436
|
+
return state.pendingConversationActivations && state.pendingConversationActivations[key];
|
|
19437
|
+
}
|
|
19438
|
+
|
|
19351
19439
|
function applyBackendConversations(items, activeId, workspaceId) {
|
|
19352
19440
|
if (!state.currentWorkspace || !items || !items.length) return;
|
|
19353
19441
|
var key = currentWorkspaceKey(workspaceId);
|
|
@@ -19409,14 +19497,24 @@ function syncBackendConversation() {
|
|
|
19409
19497
|
}
|
|
19410
19498
|
if (conv && api.ensureConversation) {
|
|
19411
19499
|
var requestedTarget = currentConversationTarget(conv.id);
|
|
19500
|
+
state.activeBackendConversationId = conv.id;
|
|
19501
|
+
var historyPromise = loadActiveConversationMessages(conv.id);
|
|
19412
19502
|
var activate = api.activateConversation ? api.activateConversation(requestedTarget) : api.ensureConversation(requestedTarget);
|
|
19413
|
-
|
|
19503
|
+
var activationPromise = activate.then(function(s) {
|
|
19414
19504
|
var id = (s && s.conversationId) || conv.id;
|
|
19415
19505
|
if (!requestIsCurrent() || String(id) !== requestedConversationId) return;
|
|
19416
19506
|
state.activeBackendConversationId = id;
|
|
19417
|
-
applyConversationSnapshot(s, id);
|
|
19418
|
-
|
|
19419
|
-
|
|
19507
|
+
applyConversationSnapshot(s, id, { preserveReadableHistory: true });
|
|
19508
|
+
}).catch(function(error) {
|
|
19509
|
+
if (!requestIsCurrent()) return;
|
|
19510
|
+
showUiNotice(
|
|
19511
|
+
currentLang() === 'zh' ? '对话运行时激活失败,历史记录仍可查看。' : 'Conversation runtime activation failed; history remains available.',
|
|
19512
|
+
'error',
|
|
19513
|
+
'conversation-activation-' + requestedWorkspaceKey + '-' + requestedConversationId
|
|
19514
|
+
);
|
|
19515
|
+
console.error('[Conversation] runtime activation failed:', error && error.message ? error.message : String(error));
|
|
19516
|
+
});
|
|
19517
|
+
return Promise.allSettled([historyPromise, activationPromise]);
|
|
19420
19518
|
}
|
|
19421
19519
|
if (conv) {
|
|
19422
19520
|
if (!requestIsCurrent()) return Promise.resolve();
|
|
@@ -19632,10 +19730,11 @@ window.newConversation = function(workspaceReference, branchCommunication) {
|
|
|
19632
19730
|
els['chat-area'].innerHTML = '';
|
|
19633
19731
|
var target = currentConversationTarget(id);
|
|
19634
19732
|
var activation = api.activateConversation ? api.activateConversation(target) : (api.ensureConversation ? api.ensureConversation(target) : Promise.resolve(null));
|
|
19635
|
-
|
|
19733
|
+
var activationKey = runtimeKeyFor(target.workspaceId, target.conversationId);
|
|
19734
|
+
var activationReady = activation.then(function(s) {
|
|
19636
19735
|
if (createdWorkspaceKey !== currentWorkspaceKey() || id !== String(activeConversationId() || 'default')) return;
|
|
19637
19736
|
state.activeBackendConversationId = String((s && s.conversationId) || id);
|
|
19638
|
-
if (s) applyConversationSnapshot(s, id);
|
|
19737
|
+
if (s) applyConversationSnapshot(s, id, { preserveReadableHistory: true });
|
|
19639
19738
|
if (branchCommunication && api.setConversationBranchCommunication) {
|
|
19640
19739
|
api.setConversationBranchCommunication(target, true).then(function() {
|
|
19641
19740
|
var convsNow = currentWorkspaceConversations();
|
|
@@ -19645,16 +19744,23 @@ window.newConversation = function(workspaceReference, branchCommunication) {
|
|
|
19645
19744
|
}
|
|
19646
19745
|
}).then(function() {
|
|
19647
19746
|
if (createdWorkspaceKey !== currentWorkspaceKey() || id !== String(activeConversationId() || 'default')) return;
|
|
19648
|
-
|
|
19649
|
-
|
|
19650
|
-
|
|
19651
|
-
|
|
19747
|
+
if (!conversationHasReadableHistory(target)) {
|
|
19748
|
+
renderChatMessages([], target);
|
|
19749
|
+
addMsg('system', '[System] ' + t('workspace.newConversation') + ': ' + summary, 'system', '');
|
|
19750
|
+
}
|
|
19652
19751
|
}).catch(function(error) {
|
|
19653
19752
|
if (createdWorkspaceKey === currentWorkspaceKey() && id === String(activeConversationId() || 'default')) {
|
|
19654
19753
|
showUiNotice((currentLang() === 'zh' ? '新对话持久化失败:' : 'Failed to persist the new conversation: ') + (error && error.message ? error.message : String(error)), 'error', 'new-conversation-activation-' + id);
|
|
19655
19754
|
}
|
|
19656
19755
|
throw error;
|
|
19756
|
+
}).finally(function() {
|
|
19757
|
+
if (state.pendingConversationActivations && state.pendingConversationActivations[activationKey] === activationReady) {
|
|
19758
|
+
delete state.pendingConversationActivations[activationKey];
|
|
19759
|
+
}
|
|
19657
19760
|
});
|
|
19761
|
+
if (!state.pendingConversationActivations) state.pendingConversationActivations = {};
|
|
19762
|
+
state.pendingConversationActivations[activationKey] = activationReady;
|
|
19763
|
+
return activationReady;
|
|
19658
19764
|
};
|
|
19659
19765
|
|
|
19660
19766
|
window.showNewConversationPage = function() {
|
|
@@ -19724,7 +19830,7 @@ window.switchConversation = function(idx) {
|
|
|
19724
19830
|
renderConversations();
|
|
19725
19831
|
var switchSeq = (state._conversationSwitchSeq || 0) + 1;
|
|
19726
19832
|
state._conversationSwitchSeq = switchSeq;
|
|
19727
|
-
|
|
19833
|
+
var restoredReadableHistory = renderConversationHistoryFirst(activeBrowserTarget);
|
|
19728
19834
|
// The Flow takeover bubble is conversation-local. Immediately reconcile it to
|
|
19729
19835
|
// the newly active conversation: if the incoming conversation owns a Flow,
|
|
19730
19836
|
// show it; otherwise hide the bubble instantly so no takeover ever leaks
|
|
@@ -19733,7 +19839,9 @@ window.switchConversation = function(idx) {
|
|
|
19733
19839
|
if (state._conversationLoadingMsgEl && state._conversationLoadingMsgEl.parentNode) {
|
|
19734
19840
|
state._conversationLoadingMsgEl.parentNode.removeChild(state._conversationLoadingMsgEl);
|
|
19735
19841
|
}
|
|
19736
|
-
state._conversationLoadingMsgEl =
|
|
19842
|
+
state._conversationLoadingMsgEl = restoredReadableHistory
|
|
19843
|
+
? null
|
|
19844
|
+
: addMsg('workflow running', t('conversation.loadingIsolated'), 'system', state.model);
|
|
19737
19845
|
syncBackendConversation().then(function() {
|
|
19738
19846
|
// Only the latest switch may touch the UI: an earlier switch's sync
|
|
19739
19847
|
// resolution must not restore drafts into or re-render the now-current
|