@oar-sh/oar 0.9.10 → 0.9.11
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 +314 -1
- package/README.md +156 -972
- package/package.json +13 -13
- package/server/README.md +95 -22
- package/server/claude-worker/claude-session-process.mjs +89 -11
- package/server/claude-worker/claude-session-worker.mjs +9 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +76 -11
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +6 -0
- package/server/db-schema.mjs +59 -2
- package/server/grok-worker/acp-client.mjs +2 -1
- package/server/grok-worker/acp-host-services.mjs +4 -3
- package/server/migrations/0013-previews.mjs +130 -0
- package/server/migrations/0014-conversation-groups.mjs +89 -0
- package/server/migrations/0015-conversation-group-position.mjs +76 -0
- package/server/migrations/0016-share-access-log.mjs +110 -0
- package/server/public/app/about-ui.js +73 -0
- package/server/public/app/about-view.mjs +234 -0
- package/server/public/app/action-confirmations.js +31 -14
- package/server/public/app/agent-exchange-fold-view.mjs +40 -101
- package/server/public/app/api-client.js +251 -16
- package/server/public/app/ask-user-view.js +17 -15
- package/server/public/app/bootstrap.js +158 -32
- package/server/public/app/chat-actions-menu-state.mjs +20 -10
- package/server/public/app/context-menu.mjs +90 -6
- package/server/public/app/conversation-group-drag.mjs +191 -0
- package/server/public/app/conversation-group-order.mjs +265 -0
- package/server/public/app/conversation-group-state.mjs +100 -0
- package/server/public/app/conversation-list-drag.mjs +449 -0
- package/server/public/app/conversation-turn-state.mjs +81 -0
- package/server/public/app/conversation-view.js +136 -93
- package/server/public/app/cwd-picker.js +1 -1
- package/server/public/app/hidden-messages-view.mjs +312 -0
- package/server/public/app/host-suspend-ui.js +7 -1
- package/server/public/app/journal-view.js +787 -22
- package/server/public/app/preview-cards.mjs +117 -31
- package/server/public/app/preview-command.mjs +10 -1
- package/server/public/app/relay-board-view.js +19 -6
- package/server/public/app/relay-restart-error.mjs +16 -0
- package/server/public/app/settings-modal.js +40 -8
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/share-format.mjs +49 -0
- package/server/public/app/share-preview-marks.mjs +167 -0
- package/server/public/app/share-state.js +109 -0
- package/server/public/app/shared-conversations-settings.js +480 -0
- package/server/public/app/socket-handlers.js +80 -14
- package/server/public/app/store.js +96 -30
- package/server/public/app/subagent-row.mjs +3 -3
- package/server/public/app/subagent-viewer.mjs +30 -6
- package/server/public/app/thinking-follow.js +0 -4
- package/server/public/app/transcript-jump-bottom-view.mjs +194 -20
- package/server/public/app/transcript-jump-bottom.mjs +132 -21
- package/server/public/app/transcript-viewport-anchor.mjs +140 -0
- package/server/public/index.html +386 -15
- package/server/relay-tools.md +2 -1
- package/server/relay.mjs +2 -1
- package/server/repositories/conversation-group-repository.mjs +444 -0
- package/server/repositories/message-repository.mjs +1 -0
- package/server/repositories/session-repository.mjs +12 -37
- package/server/repositories/share-repository.mjs +322 -0
- package/server/repositories/share-visibility-statements.mjs +10 -0
- package/server/routes/about-routes.mjs +15 -0
- package/server/routes/cache-routes.mjs +4 -0
- package/server/routes/conversation-group-routes.mjs +171 -0
- package/server/routes/messages-routes.mjs +184 -65
- package/server/routes/preview-routes.mjs +9 -1
- package/server/routes/sessions-routes.mjs +771 -193
- package/server/server-runtime.mjs +316 -100
- package/server/services/about-service.mjs +461 -0
- package/server/services/background-task-store.mjs +128 -0
- package/server/services/background-work-view.mjs +228 -0
- package/server/services/cache-rebuild-service.mjs +30 -0
- package/server/services/delete-archive-service.mjs +4 -1
- package/server/services/detached-worker-watch-service.mjs +150 -0
- package/server/services/hidden-message-list.mjs +21 -0
- package/server/services/host-suspend-command.mjs +2 -1
- package/server/services/message-pin-list.mjs +1 -1
- package/server/services/preview-health-probe.mjs +8 -1
- package/server/services/preview-proxy-server.mjs +8 -4
- package/server/services/preview-registry-service.mjs +676 -26
- package/server/services/preview-static-handler.mjs +40 -7
- package/server/services/preview-store.mjs +274 -0
- package/server/services/rate-limit-service.mjs +12 -1
- package/server/services/relay-auth.mjs +183 -0
- package/server/services/relay-cli-launcher-service.mjs +3 -1
- package/server/services/relay-event-service.mjs +17 -0
- package/server/services/remote-relay-dispatcher.mjs +57 -5
- package/server/services/request-boundary.mjs +183 -0
- package/server/services/session-activity-tracker.mjs +227 -19
- package/server/services/session-worker-launch-service.mjs +190 -6
- package/server/services/session-worker-process-service.mjs +10 -6
- package/server/services/share-access-log-service.mjs +123 -0
- package/server/services/share-path-redaction.mjs +424 -0
- package/server/services/status-event-service.mjs +7 -3
- package/server/services/tunnel-worker-path-guard.mjs +24 -41
- package/server/services/windows-autostart-service.mjs +6 -1
- package/server/services/windows-boot-autostart-service.mjs +207 -23
- package/shared/preview-tool-core.mjs +15 -5
- package/shared/remote-relay-contract.mjs +3 -1
- package/shared/share-path-privacy.mjs +30 -0
- package/shared/windows-system-tools.mjs +106 -0
- package/shared/worker-runtime/background-task-digest.mjs +30 -0
- package/shared/worker-runtime/heartbeat.mjs +24 -0
- package/shared/worker-runtime/kept-posts.mjs +83 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +47 -15
|
@@ -23,7 +23,6 @@ import {
|
|
|
23
23
|
updateWorkspaceRootHints,
|
|
24
24
|
applyContextUsageBar,
|
|
25
25
|
scrollBottom,
|
|
26
|
-
scrollBottomAfterSend,
|
|
27
26
|
isMobileComposerViewport,
|
|
28
27
|
releaseComposerFocusAfterSend,
|
|
29
28
|
autoResize,
|
|
@@ -47,9 +46,6 @@ import {
|
|
|
47
46
|
getSessionWorkerState,
|
|
48
47
|
openSummaryModal,
|
|
49
48
|
closeSummaryModal,
|
|
50
|
-
parseTimestampMs,
|
|
51
|
-
relayQuestions,
|
|
52
|
-
relayBoards,
|
|
53
49
|
toggleSidebar,
|
|
54
50
|
} from './store.js';
|
|
55
51
|
import { getPendingQuestionCountsByConversation } from './ask-user-view.js';
|
|
@@ -62,6 +58,7 @@ import { buildWorkflowRunCard } from './background-tasks-view.mjs';
|
|
|
62
58
|
import { mergeLiveSubagentRun, subagentRowsMarkup } from './subagent-row.mjs';
|
|
63
59
|
import { rememberSubagentRuns, syncSubagentRowClock } from './subagent-viewer.mjs';
|
|
64
60
|
import { isMessagePinned, toggleMessagePin } from './pinned-messages-view.mjs';
|
|
61
|
+
import { isMessageHiddenFromShares, setConversationHiddenMessages } from './hidden-messages-view.mjs';
|
|
65
62
|
import { parsePreviewCommand, runPreviewCommand } from './preview-command.mjs';
|
|
66
63
|
import { buildTranscriptPreviewCard } from './preview-cards.mjs';
|
|
67
64
|
import { closeSlashAutocomplete, handleSlashAutocompleteKey, updateSlashAutocomplete } from './slash-autocomplete.mjs';
|
|
@@ -71,7 +68,6 @@ import { isRelayEventMessage, renderRelayEventRowHtml } from './relay-event-view
|
|
|
71
68
|
import {
|
|
72
69
|
AGENT_EXCHANGE_CLOSED_CLASS,
|
|
73
70
|
AGENT_EXCHANGE_HEAD_CLASS,
|
|
74
|
-
AGENT_EXCHANGE_MEMBER_CLASS,
|
|
75
71
|
agentExchangeFoldOpen,
|
|
76
72
|
agentExchangeRowOf,
|
|
77
73
|
agentExchangeSummary,
|
|
@@ -135,6 +131,7 @@ import {
|
|
|
135
131
|
shouldApplyIncomingDraftToComposer,
|
|
136
132
|
} from './conversation-draft-sync.mjs';
|
|
137
133
|
import { isChatInteractionHeld, selectionIntersectsNode } from './selection-guard.mjs';
|
|
134
|
+
import { captureReadingBlock, restoreReadingBlock } from './transcript-viewport-anchor.mjs';
|
|
138
135
|
import {
|
|
139
136
|
applyThinkingFollow,
|
|
140
137
|
getThinkingFollowMode,
|
|
@@ -145,6 +142,7 @@ import {
|
|
|
145
142
|
toggleThinkingFollowMode,
|
|
146
143
|
unobserveThinkingBubble,
|
|
147
144
|
} from './thinking-follow.js';
|
|
145
|
+
import { beginOwnSend, followsTranscriptEnd, nameOwnSendConversation, ownSendFollows, scrollAfterOwnSend } from './transcript-jump-bottom-view.mjs';
|
|
148
146
|
import { buildInFlightSnapshotKey } from './in-flight-snapshot.mjs';
|
|
149
147
|
import { computeStablePrefixLength, planListPatch } from './streaming-dom-patch.mjs';
|
|
150
148
|
import { prefixToolActivityEmoji } from './tool-activity-emoji.mjs';
|
|
@@ -1289,38 +1287,20 @@ function createRelayEventNode(msg, msgId) {
|
|
|
1289
1287
|
return div;
|
|
1290
1288
|
}
|
|
1291
1289
|
|
|
1292
|
-
// Agent exchanges (agent-exchange-fold-view.mjs): an agent's prompt
|
|
1293
|
-
// session's reply
|
|
1294
|
-
// kept on its node when the row is built, so the pass
|
|
1295
|
-
// itself and not the payload it came from.
|
|
1290
|
+
// Agent exchanges (agent-exchange-fold-view.mjs): an agent's prompt folds
|
|
1291
|
+
// into one row; this session's reply to it always shows. What the fold pass
|
|
1292
|
+
// needs of a message is kept on its node when the row is built, so the pass
|
|
1293
|
+
// reads the transcript itself and not the payload it came from.
|
|
1296
1294
|
const AGENT_EXCHANGE_ROW_KEY = '__agentExchangeRow';
|
|
1297
|
-
// The user's toggle of
|
|
1295
|
+
// The user's toggle of a folded prompt, by its id: true opened, false
|
|
1298
1296
|
// closed, unset none (agentExchangeFoldOpen: open while it works, else
|
|
1299
1297
|
// folded). Kept through any re-render.
|
|
1300
1298
|
const agentExchangeFoldChoices = new Map();
|
|
1301
|
-
//
|
|
1302
|
-
// card, or ran while a message of the user's waited, stays shown once that
|
|
1303
|
-
// has passed instead of folding away under them. A reload folds it again.
|
|
1304
|
-
const agentExchangesShownForUser = new Set();
|
|
1305
|
-
// Each folded row's prompt id, by message id, and each folded exchange's
|
|
1306
|
-
// status, by prompt id, as of the last pass.
|
|
1307
|
-
let agentExchangePromptIdByMessageId = new Map();
|
|
1299
|
+
// Each folded prompt's status, by its id, as of the last pass.
|
|
1308
1300
|
let agentExchangeStatusByPromptId = new Map();
|
|
1309
1301
|
|
|
1310
1302
|
function setAgentExchangeRow(div, msg, msgId) {
|
|
1311
|
-
div[AGENT_EXCHANGE_ROW_KEY] = agentExchangeRowOf(msg, { id: msgId || msg?.id || ''
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
// The turns an open question or approval card (a plan board) belongs to.
|
|
1315
|
-
function openCardMessageIds() {
|
|
1316
|
-
const ids = new Set();
|
|
1317
|
-
for (const card of [...relayQuestions.values(), ...relayBoards.values()]) {
|
|
1318
|
-
if (card?.status !== 'pending') continue;
|
|
1319
|
-
for (const id of [card.messageId, card.queueId]) {
|
|
1320
|
-
if (id) ids.add(String(id).trim());
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
return ids;
|
|
1303
|
+
div[AGENT_EXCHANGE_ROW_KEY] = agentExchangeRowOf(msg, { id: msgId || msg?.id || '' });
|
|
1324
1304
|
}
|
|
1325
1305
|
|
|
1326
1306
|
// The rows the plan reads, in order, and the node of each. The live bubble
|
|
@@ -1345,58 +1325,45 @@ function agentExchangeTranscriptRows(el) {
|
|
|
1345
1325
|
}
|
|
1346
1326
|
|
|
1347
1327
|
/**
|
|
1348
|
-
* Fold the transcript's agent
|
|
1328
|
+
* Fold the transcript's agent prompts. One idempotent pass, run after every
|
|
1349
1329
|
* render and update of the list like the separator and merge passes. The
|
|
1350
1330
|
* folded row lives in the prompt's own node, as its first child, so no
|
|
1351
|
-
* insertion path has to place it; closed,
|
|
1352
|
-
* the
|
|
1331
|
+
* insertion path has to place it; closed, a class hides the prompt's bubble.
|
|
1332
|
+
* The replies and the live bubble are read for the row's status and are
|
|
1333
|
+
* otherwise left alone.
|
|
1353
1334
|
*/
|
|
1354
1335
|
function syncAgentExchangeFolds(el = getMessagesElement()) {
|
|
1355
1336
|
if (!el) return;
|
|
1356
1337
|
const { rows, nodes } = agentExchangeTranscriptRows(el);
|
|
1357
1338
|
const plan = planAgentExchangeFolds(rows, {
|
|
1358
1339
|
runningIds: [getActiveTurnForConversation(currentConvId)?.messageId],
|
|
1359
|
-
openCardIds: openCardMessageIds(),
|
|
1360
|
-
keptShownIds: agentExchangesShownForUser,
|
|
1361
1340
|
});
|
|
1362
1341
|
const states = new Map();
|
|
1363
|
-
const promptIdByMessageId = new Map();
|
|
1364
1342
|
const statusByPromptId = new Map();
|
|
1365
1343
|
for (const group of plan) {
|
|
1366
|
-
if (group.transient) agentExchangesShownForUser.add(group.promptId);
|
|
1367
1344
|
if (!group.fold) continue;
|
|
1368
1345
|
const open = agentExchangeFoldOpen(group.status, agentExchangeFoldChoices.get(group.promptId));
|
|
1369
|
-
states.set(nodes.get(group.prompt), { group,
|
|
1370
|
-
promptIdByMessageId.set(group.promptId, group.promptId);
|
|
1346
|
+
states.set(nodes.get(group.prompt), { group, open });
|
|
1371
1347
|
statusByPromptId.set(group.promptId, group.status);
|
|
1372
|
-
for (const member of group.members) {
|
|
1373
|
-
states.set(nodes.get(member), { group, head: false, open });
|
|
1374
|
-
promptIdByMessageId.set(member.id, group.promptId);
|
|
1375
|
-
}
|
|
1376
1348
|
}
|
|
1377
|
-
agentExchangePromptIdByMessageId = promptIdByMessageId;
|
|
1378
1349
|
agentExchangeStatusByPromptId = statusByPromptId;
|
|
1379
1350
|
for (const node of el.children) {
|
|
1380
1351
|
if (node.classList?.contains('msg')) applyAgentExchangeState(node, states.get(node) || null);
|
|
1381
1352
|
}
|
|
1382
1353
|
}
|
|
1383
1354
|
|
|
1384
|
-
// For the paths that change the list outside a render: the live bubble
|
|
1385
|
-
//
|
|
1355
|
+
// For the paths that change the list outside a render: the live bubble
|
|
1356
|
+
// coming and going. A bulk render runs the pass once at its end.
|
|
1386
1357
|
export function syncAgentExchangeFoldsNow() {
|
|
1387
1358
|
if (separatorSyncSuspended > 0) return;
|
|
1388
1359
|
syncAgentExchangeFolds();
|
|
1389
1360
|
}
|
|
1390
1361
|
|
|
1391
1362
|
function applyAgentExchangeState(node, state) {
|
|
1392
|
-
node.classList.toggle(AGENT_EXCHANGE_HEAD_CLASS, !!state
|
|
1393
|
-
node.classList.toggle(AGENT_EXCHANGE_MEMBER_CLASS, !!state && !state.head);
|
|
1363
|
+
node.classList.toggle(AGENT_EXCHANGE_HEAD_CLASS, !!state);
|
|
1394
1364
|
node.classList.toggle(AGENT_EXCHANGE_CLOSED_CLASS, !!state && !state.open);
|
|
1395
|
-
// The jump-to-bottom badge counts a fold's rows as one by this id.
|
|
1396
|
-
if (state) node.dataset.agentExchangePromptId = state.group.promptId;
|
|
1397
|
-
else delete node.dataset.agentExchangePromptId;
|
|
1398
1365
|
const existing = node.querySelector(':scope > .agent-exchange-row');
|
|
1399
|
-
if (!state
|
|
1366
|
+
if (!state) {
|
|
1400
1367
|
existing?.remove();
|
|
1401
1368
|
return;
|
|
1402
1369
|
}
|
|
@@ -1430,11 +1397,11 @@ function onAgentExchangeToggle(row) {
|
|
|
1430
1397
|
syncAgentExchangeFolds();
|
|
1431
1398
|
}
|
|
1432
1399
|
|
|
1433
|
-
// A search result, a pin or a jump
|
|
1434
|
-
//
|
|
1400
|
+
// A search result, a pin or a jump to a folded prompt opens it first, so
|
|
1401
|
+
// the message jumped to is the one on screen.
|
|
1435
1402
|
function revealAgentExchangeMessage(messageId) {
|
|
1436
|
-
const promptId =
|
|
1437
|
-
if (!promptId || agentExchangeFoldChoices.get(promptId) === true) return;
|
|
1403
|
+
const promptId = String(messageId || '').trim();
|
|
1404
|
+
if (!agentExchangeStatusByPromptId.has(promptId) || agentExchangeFoldChoices.get(promptId) === true) return;
|
|
1438
1405
|
agentExchangeFoldChoices.set(promptId, true);
|
|
1439
1406
|
syncAgentExchangeFolds();
|
|
1440
1407
|
}
|
|
@@ -1523,10 +1490,11 @@ function createMessageNode(msg, msgId = null, force = false) {
|
|
|
1523
1490
|
// (e.g. the Copilot relay answering a Cursor conversation) must be visible,
|
|
1524
1491
|
// not silent: it ran on another plan than the header indicates.
|
|
1525
1492
|
const executedProvider = String(msg?.executedProvider || '').trim().toLowerCase();
|
|
1526
|
-
const boundProvider = String(conversations[currentConvId]?.runtimeProviderType || '
|
|
1493
|
+
const boundProvider = String(conversations[currentConvId]?.runtimeProviderType || '').trim().toLowerCase();
|
|
1527
1494
|
// 'unknown' means the responder identity did not resolve, not that another
|
|
1528
|
-
// provider ran the turn — no chip for it.
|
|
1529
|
-
|
|
1495
|
+
// provider ran the turn — no chip for it. Nor without a bound provider to
|
|
1496
|
+
// compare with (a shared view of a conversation that has none).
|
|
1497
|
+
const crossProviderTag = (msg.role === 'assistant' && executedProvider && executedProvider !== 'unknown' && boundProvider && executedProvider !== boundProvider)
|
|
1530
1498
|
? ` <span class="msg-provider-mismatch" title="This turn was executed by the ${escHtml(executedProvider)} provider, not the conversation's ${escHtml(boundProvider)} provider.">ran on ${escHtml(executedProvider)}</span>`
|
|
1531
1499
|
: '';
|
|
1532
1500
|
const usage = (msg.role === 'assistant' && msg?.usage && typeof msg.usage === 'object') ? msg.usage : null;
|
|
@@ -1585,7 +1553,7 @@ function createMessageNode(msg, msgId = null, force = false) {
|
|
|
1585
1553
|
rememberSubagentRuns(subagentRuns);
|
|
1586
1554
|
const subagentHtml = subagentRowsMarkup(
|
|
1587
1555
|
subagentRuns.map((run) => mergeLiveSubagentRun(run, getSubagentRun(run.subagentRunId))),
|
|
1588
|
-
{ clickable:
|
|
1556
|
+
{ clickable: true },
|
|
1589
1557
|
);
|
|
1590
1558
|
// Finished background workflows persisted with this assistant message
|
|
1591
1559
|
// (docs/plans/workflow-progress-tree.md, Phase 4). The template only
|
|
@@ -1614,7 +1582,9 @@ function createMessageNode(msg, msgId = null, force = false) {
|
|
|
1614
1582
|
&& cancellableSteerIdsForConversation(currentConvId).has(msgId);
|
|
1615
1583
|
const showsCancelButton = isQueuedUserMessage || isCancellableSteer;
|
|
1616
1584
|
const isCancelInFlight = showsCancelButton && bubbleCancelInFlight.has(msgId);
|
|
1617
|
-
|
|
1585
|
+
// The conversation's hidden list once it is known (a change on another
|
|
1586
|
+
// device arrives there), else the message's own mark.
|
|
1587
|
+
const hiddenFromShares = isMessageHiddenFromShares(currentConvId, msgId) ?? (msg?.hiddenFromShares === true);
|
|
1618
1588
|
const activeTurnMessageId = String(getActiveTurnForConversation(currentConvId)?.messageId || '').trim();
|
|
1619
1589
|
const sourceMessageId = String(msg?.sourceMessageId || '').trim();
|
|
1620
1590
|
const belongsToActiveTurn = !!activeTurnMessageId
|
|
@@ -1627,8 +1597,8 @@ function createMessageNode(msg, msgId = null, force = false) {
|
|
|
1627
1597
|
const shareVisibilityActionHtml = canToggleShareVisibility
|
|
1628
1598
|
? `<div class="msg-share-visibility">
|
|
1629
1599
|
${buildMessagePinControlsHtml(msgId, pinned)}
|
|
1630
|
-
${hiddenFromShares ?
|
|
1631
|
-
<button type="button" class="msg-share-visibility-btn" data-action="toggle-share-visibility" data-message-id="${escHtml(msgId)}" data-hidden-from-shares="${hiddenFromShares ? 'true' : 'false'}" title="${hiddenFromShares
|
|
1600
|
+
${hiddenFromShares ? HIDDEN_LABEL_HTML : ''}
|
|
1601
|
+
<button type="button" class="msg-share-visibility-btn" data-action="toggle-share-visibility" data-message-id="${escHtml(msgId)}" data-hidden-from-shares="${hiddenFromShares ? 'true' : 'false'}" title="${shareVisibilityButtonTitle(hiddenFromShares)}">${hiddenFromShares ? 'Unhide' : 'Hide'}</button>
|
|
1632
1602
|
</div>`
|
|
1633
1603
|
: '';
|
|
1634
1604
|
// A terminal failure whose fix lives in the relay UI gets the fix as a button
|
|
@@ -1763,21 +1733,12 @@ function prependMessageNodes(msgs) {
|
|
|
1763
1733
|
return { inserted, firstMessageId };
|
|
1764
1734
|
}
|
|
1765
1735
|
|
|
1736
|
+
// A shared view's tool lines arrive filtered by the relay's path privacy
|
|
1737
|
+
// setting (server/services/share-path-redaction.mjs); they are shown as sent.
|
|
1766
1738
|
export function decorateActivityText(text) {
|
|
1767
1739
|
const value = String(text || '').trim();
|
|
1768
1740
|
if (!value) return '';
|
|
1769
|
-
|
|
1770
|
-
const source = String(input || '');
|
|
1771
|
-
const tokenMasked = source.replace(/@(file|folder):([^\s`]+)/gi, (_m, kind, rawPath) => {
|
|
1772
|
-
const normalized = String(rawPath || '').replace(/\\/g, '/').replace(/\/+$/, '');
|
|
1773
|
-
const segments = normalized.split('/').filter(Boolean);
|
|
1774
|
-
const basename = segments[segments.length - 1] || normalized;
|
|
1775
|
-
return `@${String(kind || '').toLowerCase()}:${basename}`;
|
|
1776
|
-
});
|
|
1777
|
-
return tokenMasked.replace(/([A-Za-z]:)?(?:[\\/~.]?[\\/])(?:[^\\/\s]+[\\/])+([^\\/\s]+)/g, (_m, _prefix, basename) => basename);
|
|
1778
|
-
};
|
|
1779
|
-
const sharedSafeValue = IS_SHARED_VIEW ? maskSharedPathSegments(value) : value;
|
|
1780
|
-
return prefixToolActivityEmoji(sharedSafeValue);
|
|
1741
|
+
return prefixToolActivityEmoji(value);
|
|
1781
1742
|
}
|
|
1782
1743
|
|
|
1783
1744
|
export function renderThoughtsMarkup(thoughts) {
|
|
@@ -1895,8 +1856,7 @@ function anchorThinkingBubble(div, messageId) {
|
|
|
1895
1856
|
if (next) el.insertBefore(div, next);
|
|
1896
1857
|
else el.appendChild(div);
|
|
1897
1858
|
if (div.dataset.compactBoundary) syncSeparatorsNow();
|
|
1898
|
-
//
|
|
1899
|
-
// the turn is working.
|
|
1859
|
+
// A folded agent prompt's row says the turn is working.
|
|
1900
1860
|
else syncAgentExchangeFoldsNow();
|
|
1901
1861
|
} else if (div.parentNode !== el) {
|
|
1902
1862
|
el.appendChild(div);
|
|
@@ -1985,6 +1945,43 @@ export function removeThinking() {
|
|
|
1985
1945
|
if (hadBubble) syncAgentExchangeFoldsNow();
|
|
1986
1946
|
}
|
|
1987
1947
|
|
|
1948
|
+
// The turn ends under a reader who is not at the end: the live bubble is
|
|
1949
|
+
// taken off the page and the reply row is put in its place. The page is
|
|
1950
|
+
// shorter for a moment (scrollTop can be clamped), and the reply is not as
|
|
1951
|
+
// tall as the bubble was (its thoughts are folded). Whatever the reader looks
|
|
1952
|
+
// at stays where it is: the block of text at the top of the viewport is noted
|
|
1953
|
+
// before the swap and put back after it. Read in the bubble's stream, it is
|
|
1954
|
+
// found again in the reply; in a row above or below the bubble, in that row.
|
|
1955
|
+
export function captureLiveReplyReadingPosition() {
|
|
1956
|
+
const el = getMessagesElement();
|
|
1957
|
+
if (!el) return null;
|
|
1958
|
+
const bubble = document.getElementById('thinking-indicator');
|
|
1959
|
+
return {
|
|
1960
|
+
scrollTop: el.scrollTop,
|
|
1961
|
+
block: captureReadingBlock(el),
|
|
1962
|
+
// Below the bubble the pixel position is the wrong fallback: everything
|
|
1963
|
+
// the reader sees moves up or down with the swap, and scrollTop with it.
|
|
1964
|
+
belowBubble: !!bubble && bubble.getBoundingClientRect().bottom <= el.getBoundingClientRect().top,
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
export function restoreLiveReplyReadingPosition(position) {
|
|
1969
|
+
const el = getMessagesElement();
|
|
1970
|
+
if (!el || !position || isChatInteractionHeld()) return;
|
|
1971
|
+
if (restoreReadingBlock(el, position.block)) return;
|
|
1972
|
+
if (!position.belowBubble) el.scrollTop = position.scrollTop;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
// A rebuild of the transcript on the page that nobody asked to scroll: the
|
|
1976
|
+
// reader keeps the text they are at, or failing that the pixel position.
|
|
1977
|
+
function renderMessagesInPlace(msgs, meta) {
|
|
1978
|
+
const el = getMessagesElement();
|
|
1979
|
+
const reading = el ? { scrollTop: el.scrollTop, block: captureReadingBlock(el) } : null;
|
|
1980
|
+
const rendered = renderMessages(msgs, false, meta);
|
|
1981
|
+
if (el && reading && rendered && !restoreReadingBlock(el, reading.block)) el.scrollTop = reading.scrollTop;
|
|
1982
|
+
return rendered;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1988
1985
|
// The live turn's compaction line: the bubble stamps the turn's compaction
|
|
1989
1986
|
// for the separator pass — "Compacting context…" while the last compaction
|
|
1990
1987
|
// entry is a start without an end, the final label once the boundary is in —
|
|
@@ -2749,11 +2746,16 @@ export function applyRelayStreamEvent({
|
|
|
2749
2746
|
|
|
2750
2747
|
export function flushDeferredMessageRender() {
|
|
2751
2748
|
if (!deferredMessageRender) return;
|
|
2752
|
-
const { msgs,
|
|
2749
|
+
const { msgs, meta } = deferredMessageRender;
|
|
2753
2750
|
deferredMessageRender = null;
|
|
2754
2751
|
const deferredConversationId = String(meta?.conversationId || '').trim();
|
|
2755
2752
|
if (deferredConversationId && deferredConversationId !== String(currentConvId || '').trim()) return;
|
|
2756
|
-
|
|
2753
|
+
// Whoever asked for this render has long returned, and with it whatever
|
|
2754
|
+
// would have put the reader back; and where the reader was when it was
|
|
2755
|
+
// held back is not where they are now. Decided as it runs: at the end it
|
|
2756
|
+
// follows, anywhere else it leaves them at the text they read.
|
|
2757
|
+
if (followsTranscriptEnd()) renderMessages(msgs, true, meta);
|
|
2758
|
+
else renderMessagesInPlace(msgs, meta);
|
|
2757
2759
|
}
|
|
2758
2760
|
|
|
2759
2761
|
// Blacklists the id from further live relay_stream frames. Kept separate from
|
|
@@ -3077,6 +3079,7 @@ export function replaceRenderedMessage(msg, msgId) {
|
|
|
3077
3079
|
}
|
|
3078
3080
|
|
|
3079
3081
|
async function resendStoppedSteer(conversationId, markerId) {
|
|
3082
|
+
const send = beginOwnSend();
|
|
3080
3083
|
const conversationKey = String(conversationId || '').trim();
|
|
3081
3084
|
const id = String(markerId || '').trim();
|
|
3082
3085
|
if (!conversationKey || !id || stoppedSteerResends.has(id)) return;
|
|
@@ -3128,7 +3131,7 @@ async function resendStoppedSteer(conversationId, markerId) {
|
|
|
3128
3131
|
mode: selectedMode,
|
|
3129
3132
|
timestamp: new Date().toISOString(),
|
|
3130
3133
|
attachments,
|
|
3131
|
-
},
|
|
3134
|
+
}, ownSendFollows(send), clientMessageId, true);
|
|
3132
3135
|
bubbleAppended = true;
|
|
3133
3136
|
}
|
|
3134
3137
|
const r = await sendMessageApi({
|
|
@@ -3172,7 +3175,7 @@ async function resendStoppedSteer(conversationId, markerId) {
|
|
|
3172
3175
|
showTransientRelayNotice(`Resent without ${dropped} attachment${dropped === 1 ? '' : 's'} that ${dropped === 1 ? 'is' : 'are'} no longer available.`, 7000);
|
|
3173
3176
|
}
|
|
3174
3177
|
if (cliOnline && isCurrentConversation(conversationKey) && !getActiveTurnForConversation(conversationKey)?.messageId) {
|
|
3175
|
-
showThinking(r.messageId || null);
|
|
3178
|
+
showThinking(r.messageId || null, ownSendFollows(send));
|
|
3176
3179
|
}
|
|
3177
3180
|
} catch (error) {
|
|
3178
3181
|
if (!hasSendEchoArrived(clientMessageId)) {
|
|
@@ -3341,6 +3344,35 @@ export function syncRenderedPinState(pinnedIds) {
|
|
|
3341
3344
|
}
|
|
3342
3345
|
}
|
|
3343
3346
|
|
|
3347
|
+
const HIDDEN_LABEL_HTML = '<span class="msg-hidden-label">Hidden from shared viewers</span>';
|
|
3348
|
+
|
|
3349
|
+
function shareVisibilityButtonTitle(hidden) {
|
|
3350
|
+
return hidden ? 'Shows this message in shared conversations' : 'Hides this message from shared conversations';
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
// The hidden list changed (this page, another device): bring the bubbles on
|
|
3354
|
+
// the page in line without rebuilding them. A button whose own round trip is
|
|
3355
|
+
// still out keeps its "Hiding…" until the answer.
|
|
3356
|
+
export function syncRenderedShareVisibility(hiddenIds) {
|
|
3357
|
+
const el = getMessagesElement();
|
|
3358
|
+
if (!el) return;
|
|
3359
|
+
const ids = hiddenIds instanceof Set ? hiddenIds : new Set(hiddenIds || []);
|
|
3360
|
+
for (const button of el.querySelectorAll('.msg-share-visibility-btn')) {
|
|
3361
|
+
const messageId = String(button.dataset.messageId || '').trim();
|
|
3362
|
+
if (shareVisibilityInFlight.has(messageId)) continue;
|
|
3363
|
+
const hidden = ids.has(messageId);
|
|
3364
|
+
if ((button.dataset.hiddenFromShares === 'true') === hidden) continue;
|
|
3365
|
+
button.dataset.hiddenFromShares = hidden ? 'true' : 'false';
|
|
3366
|
+
button.disabled = false;
|
|
3367
|
+
button.textContent = hidden ? 'Unhide' : 'Hide';
|
|
3368
|
+
button.title = shareVisibilityButtonTitle(hidden);
|
|
3369
|
+
const sibling = button.previousElementSibling;
|
|
3370
|
+
const label = sibling?.classList?.contains('msg-hidden-label') ? sibling : null;
|
|
3371
|
+
if (hidden && !label) button.insertAdjacentHTML('beforebegin', HIDDEN_LABEL_HTML);
|
|
3372
|
+
if (!hidden && label) label.remove();
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3344
3376
|
async function toggleMessageShareVisibility(conversationId, messageId, hiddenFromShares) {
|
|
3345
3377
|
const conversationKey = String(conversationId || '').trim();
|
|
3346
3378
|
const targetMessageId = String(messageId || '').trim();
|
|
@@ -3359,13 +3391,17 @@ async function toggleMessageShareVisibility(conversationId, messageId, hiddenFro
|
|
|
3359
3391
|
button.disabled = false;
|
|
3360
3392
|
button.textContent = hiddenFromShares ? 'Unhide' : 'Hide';
|
|
3361
3393
|
}
|
|
3362
|
-
showTransientRelayNotice('Could not update shared-message visibility.');
|
|
3394
|
+
showTransientRelayNotice(String(result?.error || '').trim() || 'Could not update shared-message visibility.');
|
|
3363
3395
|
return;
|
|
3364
3396
|
}
|
|
3365
3397
|
|
|
3366
3398
|
showTransientRelayNotice(result.hiddenFromShares
|
|
3367
3399
|
? 'Message hidden from shared viewers.'
|
|
3368
3400
|
: 'Message visible to shared viewers.');
|
|
3401
|
+
// Stored under its own conversation: the 🙈 count, even after a switch.
|
|
3402
|
+
if ('hiddenMessages' in result) {
|
|
3403
|
+
setConversationHiddenMessages(conversationKey, result.hiddenMessages, result.hiddenMessagesRevision);
|
|
3404
|
+
}
|
|
3369
3405
|
// The reload below only refreshes the open view; if the user switched
|
|
3370
3406
|
// conversations while the toggle round trip was in flight, rendering it
|
|
3371
3407
|
// would replace the new conversation's messages with this one's.
|
|
@@ -3375,7 +3411,7 @@ async function toggleMessageShareVisibility(conversationId, messageId, hiddenFro
|
|
|
3375
3411
|
});
|
|
3376
3412
|
if (String(currentConvId || '').trim() !== conversationKey) return;
|
|
3377
3413
|
if (response?.messages) {
|
|
3378
|
-
|
|
3414
|
+
renderMessagesInPlace(response.messages, response);
|
|
3379
3415
|
}
|
|
3380
3416
|
}
|
|
3381
3417
|
|
|
@@ -3545,8 +3581,8 @@ export function appendMessage(msg, scroll = true, msgId = null, force = false, i
|
|
|
3545
3581
|
const node = createMessageNode(msg, msgId, force);
|
|
3546
3582
|
const isNewNode = !!node && !node.parentNode;
|
|
3547
3583
|
// A reply the socket delivers without its prompt's id in the message (a
|
|
3548
|
-
// failed turn's note) is placed under that prompt, and
|
|
3549
|
-
//
|
|
3584
|
+
// failed turn's note) is placed under that prompt, and an agent prompt's
|
|
3585
|
+
// row reads its status from it.
|
|
3550
3586
|
const exchangeRow = isNewNode ? node[AGENT_EXCHANGE_ROW_KEY] : null;
|
|
3551
3587
|
const anchorId = String(insertAfterId || '').trim();
|
|
3552
3588
|
if (exchangeRow?.role === 'assistant' && !exchangeRow.sourceMessageId && anchorId && anchorId !== String(msgId || '').trim()) {
|
|
@@ -3947,7 +3983,7 @@ export function focusConversationMessageById(messageId, { behavior = 'smooth', b
|
|
|
3947
3983
|
if (!id) return false;
|
|
3948
3984
|
const el = getMessagesElement();
|
|
3949
3985
|
if (!el) return false;
|
|
3950
|
-
// A
|
|
3986
|
+
// A folded agent prompt is opened before it is scrolled to.
|
|
3951
3987
|
revealAgentExchangeMessage(id);
|
|
3952
3988
|
const target = el.querySelector(`.msg[data-message-id="${CSS.escape(id)}"]`);
|
|
3953
3989
|
if (!target) return false;
|
|
@@ -3990,6 +4026,9 @@ export function compactCurrentConversation() {
|
|
|
3990
4026
|
let unknownCommandWarned = null;
|
|
3991
4027
|
|
|
3992
4028
|
export async function sendMessage() {
|
|
4029
|
+
// The reader's own send, as of now: what follows it scrolls to the end
|
|
4030
|
+
// only while they have not scrolled since.
|
|
4031
|
+
const send = beginOwnSend();
|
|
3993
4032
|
const input = document.getElementById('msg-input');
|
|
3994
4033
|
const originalComposerText = String(input?.value || '');
|
|
3995
4034
|
const text = originalComposerText.trim();
|
|
@@ -4030,7 +4069,7 @@ export async function sendMessage() {
|
|
|
4030
4069
|
releaseComposerFocusAfterSend(input);
|
|
4031
4070
|
void flushConversationDraft(currentConvId);
|
|
4032
4071
|
compactCurrentConversation();
|
|
4033
|
-
|
|
4072
|
+
scrollAfterOwnSend(send);
|
|
4034
4073
|
return;
|
|
4035
4074
|
}
|
|
4036
4075
|
|
|
@@ -4107,8 +4146,10 @@ export async function sendMessage() {
|
|
|
4107
4146
|
input.value = '';
|
|
4108
4147
|
autoResize(input);
|
|
4109
4148
|
releaseComposerFocusAfterSend(input);
|
|
4110
|
-
|
|
4111
|
-
|
|
4149
|
+
// Asked before the bubble is on the page.
|
|
4150
|
+
const follow = ownSendFollows(send);
|
|
4151
|
+
appendMessage({ role: 'user', text, model: selectedModel, mode: selectedMode, timestamp: msgTimestamp, attachments }, follow, clientMessageId, true);
|
|
4152
|
+
scrollAfterOwnSend(send);
|
|
4112
4153
|
}
|
|
4113
4154
|
|
|
4114
4155
|
const body = {
|
|
@@ -4196,7 +4237,8 @@ export async function sendMessage() {
|
|
|
4196
4237
|
await window.openConversation?.(r.compactedConversationId);
|
|
4197
4238
|
removeComposerAttachments(draftAttachments);
|
|
4198
4239
|
if (!mobileSend) input.focus();
|
|
4199
|
-
|
|
4240
|
+
// The send goes on in the conversation the compaction opened.
|
|
4241
|
+
scrollAfterOwnSend({ ...send, conversationId: String(r.compactedConversationId || '').trim() });
|
|
4200
4242
|
}
|
|
4201
4243
|
return;
|
|
4202
4244
|
}
|
|
@@ -4217,6 +4259,7 @@ export async function sendMessage() {
|
|
|
4217
4259
|
if (adoptNewConversation) {
|
|
4218
4260
|
setCurrentConv(r.conversationId);
|
|
4219
4261
|
composerConversationId = r.conversationId;
|
|
4262
|
+
nameOwnSendConversation(send, r.conversationId);
|
|
4220
4263
|
}
|
|
4221
4264
|
conversations[r.conversationId] = {
|
|
4222
4265
|
id: r.conversationId,
|
|
@@ -4263,7 +4306,7 @@ export async function sendMessage() {
|
|
|
4263
4306
|
// pinned to the newest message. Re-pin now rather than after the draft
|
|
4264
4307
|
// save's round trip below, or the new bubble sits under the composer
|
|
4265
4308
|
// for as long as that request takes.
|
|
4266
|
-
if (viewingSendConversation()) scrollBottom();
|
|
4309
|
+
if (viewingSendConversation() && ownSendFollows(send)) scrollBottom();
|
|
4267
4310
|
const persistedConversationId = String(r.conversationId || targetConversationId || '').trim();
|
|
4268
4311
|
if (persistedConversationId) {
|
|
4269
4312
|
restorableDraftByConversation.delete(persistedConversationId);
|
|
@@ -4295,7 +4338,7 @@ export async function sendMessage() {
|
|
|
4295
4338
|
// A turn already running here owns the live bubble; adopting the
|
|
4296
4339
|
// just-queued id would re-anchor it under the newest user message.
|
|
4297
4340
|
const activeTurn = getActiveTurnForConversation(composerConversationId);
|
|
4298
|
-
if (!activeTurn?.messageId) showThinking(r.messageId || null);
|
|
4341
|
+
if (!activeTurn?.messageId) showThinking(r.messageId || null, ownSendFollows(send));
|
|
4299
4342
|
}
|
|
4300
4343
|
|
|
4301
4344
|
// Only what this message took leaves the composer: attachments added (or
|
|
@@ -4304,7 +4347,7 @@ export async function sendMessage() {
|
|
|
4304
4347
|
if (viewingSendConversation()) {
|
|
4305
4348
|
removeComposerAttachments(draftAttachments);
|
|
4306
4349
|
if (!mobileSend) input.focus();
|
|
4307
|
-
|
|
4350
|
+
scrollAfterOwnSend(send);
|
|
4308
4351
|
}
|
|
4309
4352
|
} catch (e) {
|
|
4310
4353
|
const sendLanded = hasSendEchoArrived(clientMessageId);
|
|
@@ -608,7 +608,7 @@ async function submitChangeCwd(launchAfterChange = false) {
|
|
|
608
608
|
? `Next launch CWD saved as ${updatedPath}. The running CLI keeps its current CWD.`
|
|
609
609
|
: `Next launch CWD saved as ${updatedPath}.`);
|
|
610
610
|
} catch (error) {
|
|
611
|
-
alert(error?.message
|
|
611
|
+
alert(error?.message ? `Failed to update the launch CWD: ${error.message}` : 'Failed to update the launch CWD');
|
|
612
612
|
} finally {
|
|
613
613
|
changeCwdInFlight = false;
|
|
614
614
|
setSummaryModalLoading(false);
|