(function (root, factory) {
const createSearchPreview = factory();
if (typeof module === 'object' && module.exports) module.exports = createSearchPreview;
if (root) {
root.createSearchPreview = createSearchPreview;
root.safeSearchUrl = value => {
try {
const url = new URL(value);
return ['http:', 'https:'].includes(url.protocol) ? url.href : '';
} catch (_) { return ''; }
};
root.searchPreviewUrl = (state, defaultScope, location) => {
const { query, preview, scope = defaultScope } = state;
const params = new URLSearchParams({
search:query || '', preview:preview.kind + ':' + preview.repository + ':' + preview.number,
search_kind:scope.kind, search_state:scope.state,
});
if (scope.repository) params.set('search_repository', scope.repository);
return new URL('?' + params, location.origin + location.pathname).href;
};
root.searchPreviewPath = item => {
const repository = String(item.repository || '').split('/').map(encodeURIComponent).join('/');
return 'api/v1/repos/' + repository + '/issues/' + encodeURIComponent(item.number) +
'/preview?kind=' + encodeURIComponent(item.kind);
};
root.searchPreviewConversationPath = (item, page) => {
const repository = String(item.repository || '').split('/').map(encodeURIComponent).join('/');
const query = new URLSearchParams({ kind:item.kind, limit:'20' });
if (page) query.set('page', String(page));
return 'api/v1/repos/' + repository + '/issues/' + encodeURIComponent(item.number) +
'/preview/conversation?' + query.toString();
};
root.followingPullReviewPath = item => {
const repository = String(item.repository || '').split('/').map(encodeURIComponent).join('/');
return 'api/v1/following/' + repository + '/pulls/' + encodeURIComponent(item.number) + '/review-data';
};
root.searchPreviewReplyPath = item => root.searchPreviewPath(item).replace(/\?.*$/, '') +
'/comments?kind=' + encodeURIComponent(item.kind);
root.searchPreviewMutation = fetchJson => (detail, action) => {
if (action === 'reopen-pull') {
const path = root.searchPreviewPath(detail).replace('/issues/', '/pulls/').replace(/\/preview.*$/, '/reopen');
return fetchJson(path, {
method:'PATCH', headers:{'Content-Type':'application/json'},
body:JSON.stringify({expected_head_sha:detail.head_sha}),
});
}
return fetchJson(root.searchPreviewPath(detail).replace(/\?.*$/, '') + '/' + action, {method:'PATCH'});
};
root.searchPreviewSubscriptionPath = item => root.searchPreviewPath(item).replace(/\?.*$/, '') +
'/subscription?kind=' + encodeURIComponent(item.kind);
root.searchPreviewSubscriptionOptions = fetchJson => {
const options = {
load:async detail => {
if (['issue', 'pull'].includes(detail.kind) && detail.state === 'closed' && detail.following === true) {
return {...detail, watching:true};
}
if (!(['issue', 'pull'].includes(detail.kind) && detail.state === 'open')) return detail;
const result = await fetchJson(root.searchPreviewSubscriptionPath(detail), {headers:{Accept:'application/json'}});
return {...detail, watching:result.watching === true};
},
watch:(detail,watching) => fetchJson(root.searchPreviewSubscriptionPath(detail), {
method:watching ? 'PUT' : 'DELETE', headers:{Accept:'application/json'},
}),
review:item => fetchJson(root.followingPullReviewPath(item)),
};
options.preview = async item => options.load({...item, ...await fetchJson(root.searchPreviewPath(item), {
headers:{Accept:'application/json'},
})});
return options;
};
root.createDetailWatch = ({fetchJson,refreshFollowing,onState}) => {
const api=root.searchPreviewSubscriptionOptions(fetchJson);
let item,watching=false,mutation;
return {
async open(next) {
item=next; onState('loading',watching);
const result=await api.load(next).catch(error=>{
if(item===next)onState('error',watching,error);throw error;
});
if (item !== next) return;
watching=result.watching === true; onState('ready',watching);
},
toggle() {
if (mutation) return mutation;
const next=!watching;
onState(next?'watching':'unwatching',watching);
mutation=api.watch(item,next).then(async result => {
if (result?.watching !== next || result?.following_synced !== true)
throw new Error(result?.error || 'Unconfirmed.');
watching=next; await refreshFollowing();
onState(watching?'watched':'unwatched',watching);
}).catch(error => {onState('error',watching,error);throw error;})
.finally(()=>{mutation=null;});
return mutation;
},
};
};
root.searchPreviewWatchStatus = state => ({
watching:'Starting watch…', unwatching:'Stopping watch…',
watched:'Watching · available in Following. Future activity will appear in Updates.',
unwatched:'Stopped watching · removed from Following. Assignment and planning are unchanged.',
'watch-partial':'Watching in Gitea, but Following could not sync. Tap Stop watching, then Watch ' +
(state.detail?.kind === 'pull' ? 'pull request' : 'issue') + ' to repair.',
'watch-error':(state.error?.message || 'Watch status was not changed.') + ' Retry.',
})[state.status] || '';
root.renderSearchPreviewWatch = (detail, state, button) => {
const watchableKind = ['issue', 'pull'].includes(detail.kind);
const retiring = watchableKind && detail.state === 'closed' &&
detail.following === true && detail.watching === true;
button.hidden = !(watchableKind && detail.state === 'open') && !retiring;
button.textContent = retiring ? 'Stop watching & next' :
(detail.watching ? 'Stop watching' : 'Watch ' + (detail.kind === 'pull' ? 'pull request' : 'issue'));
button.disabled = state.status === 'watching' || state.status === 'unwatching';
};
root.renderSearchPreviewStart = (detail, state, button) => {
const pull = detail.kind === 'pull' && detail.state === 'closed' &&
detail.authored_pull_reopenable === true;
const issue = detail.kind === 'issue' &&
(detail.reopenable || (detail.state === 'open' && (detail.claimable || detail.assigned_to_me)));
button.hidden = !(detail.reviewable || pull || issue);
button.textContent = detail.reviewable ? 'Review now' : pull ? 'Reopen in My Work' :
detail.reopenable ? 'Reopen & resume' :
(detail.assigned_to_me ? 'Start in Today' : 'Assign & start');
button.disabled = state.status === 'claiming' || state.status === 'reopening';
};
root.createSearchAuthoredPullRecovery = o =>
async d => {
if (!o.confirm('Reopen ' + d.repository + ' #' + d.number + '?')) return 'canceled';
await o.reopen(d);
await o.refresh();
const item = o.find(d);
if (!item) {
o.unavailable('Reopened. Refresh My Work.');
return 'unavailable';
}
o.open(item);
return 'opened';
};
root.wireSearchPreviewWatch = (button, preview, getDetail) => button.addEventListener('click', () => {
const detail = getDetail();
if (detail) preview.setWatching(detail.watching !== true).catch(() => {});
});
root.searchPreviewReplyOptions = (fetchJson, storage, crypto) => ({
storage,
createOperationId:() => crypto?.randomUUID?.() || String(Date.now()) + '-' + Math.random(),
postReply:(item,body,operationId) => fetchJson(root.searchPreviewReplyPath(item), {
method:'POST', headers:{Accept:'application/json','Content-Type':'application/json','Idempotency-Key':operationId},
body:JSON.stringify({body}),
}),
});
root.renderSearchPreviewConversation = (conversation, document, escapeHtml, formatTime, renderMarkdown, actions) => {
const comments = document.querySelector('#search-preview-comments');
const status = document.querySelector('#search-preview-conversation-status');
const retry = document.querySelector('#retry-search-preview-conversation');
const older = document.querySelector('#load-older-search-preview-comments');
if (!conversation) {
comments.textContent = status.textContent = '';
retry.hidden = older.hidden = true;
return;
}
const isNew = comment => Boolean(conversation.reviewedAt && comment.created_at &&
new Date(comment.created_at).getTime() > new Date(conversation.reviewedAt).getTime());
const newCount = (conversation.comments || []).filter(isNew).length;
comments.innerHTML = (conversation.comments || []).map(comment =>
''
).join('');
retry.hidden = conversation.status !== 'error';
older.hidden = conversation.status !== 'ready' || !conversation.olderPage;
older.disabled = conversation.status === 'loading';
if (conversation.status === 'loading') status.textContent = conversation.comments?.length
? 'Loading older messages…' : 'Loading current conversation…';
else if (conversation.status === 'error') status.textContent =
'Conversation unavailable. Preview and planning actions still work.';
else if (!conversation.comments?.length) status.textContent = 'No conversation yet.';
else status.textContent = conversation.comments.length +
(conversation.comments.length === 1 ? ' message' : ' messages') +
(newCount ? ' · ' + newCount + ' new since last review' : '') + '.';
};
root.createSearchPreviewConversationActions = ({hydrator,rootNode,retry,paint,wire}) => {
let latest = null;
const show = conversation => {
latest = conversation;
if (!conversation) {
paint(null, null);
return Promise.resolve(false);
}
return hydrator.show({root:rootNode,state:conversation,paint,retry,wire});
};
retry.addEventListener('click', () => { if (latest) void show(latest); });
return show;
};
root.renderSearchPreviewReply = (state, detail, preview, document) => {
const section = document.querySelector('.search-preview-reply');
const input = document.querySelector('#search-preview-reply');
const status = document.querySelector('#search-preview-reply-status');
const buttons = ['#send-search-preview-reply', '#send-search-preview-reply-next']
.map(selector => document.querySelector(selector));
section.hidden = detail?.commentable !== true;
status.textContent = '';
if (section.hidden) {
buttons.forEach(button => { button.disabled = true; });
return;
}
input.value = preview.replyDraft();
const replying = state.status === 'replying';
buttons.forEach(button => {
button.disabled = replying || (!input.value.trim() && !preview.hasReplyAttachments?.());
});
if (replying) status.textContent = 'Saving reply for delivery…';
else if (state.status === 'queued') status.textContent = 'Reply queued. It will send when connected.';
else if (state.status === 'replied') status.textContent = 'Reply posted.';
else if (state.status === 'reply-error') status.textContent =
state.error?.message || 'Reply failed. Your draft is safe; retry when ready.';
};
root.renderSearchPreviewReview = (review, document, escapeHtml) => {
const section = document.querySelector('#search-preview-review');
const status = document.querySelector('#search-preview-review-status');
const files = document.querySelector('#search-preview-files');
const retry = document.querySelector('#retry-search-preview-review');
section.hidden = !review;
retry.hidden = review?.status !== 'error';
files.innerHTML = '';
if (!review) {
status.textContent = '';
return;
}
if (review.status === 'loading') {
status.textContent = 'Loading CI and changed files…';
return;
}
if (review.status === 'error') {
status.textContent = 'Changes unavailable. This revision has not been marked reviewed.';
return;
}
const data = review.data || {};
const changed = Array.isArray(data.files) ? data.files : [];
const ci = ({success:'CI passed', failure:'CI failed', error:'CI failed', pending:'CI pending'})[
data.ci_state
] || 'CI status unavailable';
status.textContent = ci + ' · ' + changed.length + ' changed ' +
(changed.length === 1 ? 'file.' : 'files.');
files.innerHTML = changed.map(file => {
const lines = (Array.isArray(file.diff_lines) ? file.diff_lines : []).map(raw => {
const line = String(raw);
const kind = line.startsWith('@@') ? 'hunk' : line.startsWith('+') ? 'added' :
line.startsWith('-') ? 'removed' : 'context';
return '' + escapeHtml(line) + '';
}).join('');
const diff = file.diff_available
? '
' + lines +
(file.diff_truncated ? 'Preview truncated · open in Gitea for the full diff.' : '') + ''
: '' +
(file.diff_binary ? 'Binary file · preview unavailable.' : 'Diff preview unavailable.') + '
';
return '' + escapeHtml(file.filename || 'Unknown file') +
'' + escapeHtml(file.status || 'changed') + ' · +' +
Number(file.additions || 0) + ' / −' + Number(file.deletions || 0) + '' + diff + '';
}).join('');
};
root.renderSearchPreviewWorkspaces = (state, detail, preview, document, escapeHtml) => {
root.renderSearchPreviewReply(state, detail, preview, document);
root.renderSearchPreviewReview(state.review, document, escapeHtml);
document.querySelector('[data-search-preview-section="changes"]').hidden = !state.review;
};
}
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
return function createSearchPreview({ fetchJson, fetchConversation, fetchReview, mutate, watch, share, postReply, queueReply, prepareReply, afterReply, afterUnwatch, hasAttachments, clearAttachments, storage, createOperationId, session, getSession, loadMore, onNavigate, onOpened, navigationRoot, onState }) {
if (Array.isArray(session)) {
getSession = session[0];
loadMore = () => session[1].loadMore();
onNavigate = session[2];
navigationRoot = document;
}
let generation = 0;
let current = null;
let mutationRequest = null;
let shareRequest = null;
let moveRequest = null;
let replyRequest = null;
let watchRequest = null;
let conversation = null;
let review = null;
let openedRevision = null;
function sameItem(left, right) {
return left && right && left.kind === right.kind && left.repository === right.repository &&
Number(left.number) === Number(right.number);
}
async function notifyOpened(item, requestGeneration) {
const revision = [item?.kind, item?.repository, item?.number, item?.updated_at].join(':');
if (requestGeneration !== generation || openedRevision === revision) return false;
await onOpened?.({...item});
if (requestGeneration === generation) openedRevision = revision;
return true;
}
function replyKey(item, suffix) {
return 'stackchain.search-reply.' + [item?.kind, item?.repository, item?.number]
.map(value => encodeURIComponent(String(value || ''))).join('.') + '.' + suffix;
}
function stored(key) {
try { return storage?.getItem(key) || ''; }
catch (_) { return ''; }
}
function save(key, value) {
try {
if (value) storage?.setItem(key, value);
else storage?.removeItem(key);
} catch (_) { /* Volatile drafts still remain in the textarea. */ }
}
function navigation(item) {
const session = typeof getSession === 'function' ? getSession() : null;
const items = Array.isArray(session?.items) ? session.items : [];
const index = items.findIndex(candidate => sameItem(candidate, item));
if (index < 0) return null;
return {
position:index + 1,
total:items.length,
hasPrevious:index > 0,
hasNext:index < items.length - 1 || session.more === true,
};
}
function publish(state) {
if (conversation && !Object.prototype.hasOwnProperty.call(state, 'conversation')) {
state = {...state, conversation};
}
if (review && !Object.prototype.hasOwnProperty.call(state, 'review')) state = {...state, review};
const position = navigation(state.item || current);
if (navigationRoot) {
const bar = navigationRoot.querySelector('.search-preview-navigation');
bar.hidden = !position;
if (position) {
navigationRoot.querySelector('#search-preview-position').textContent = position.position + ' of ' + position.total;
navigationRoot.querySelector('#previous-search-result').disabled = !position.hasPrevious;
navigationRoot.querySelector('#next-search-result').disabled = !position.hasNext;
}
}
onState(position ? { ...state, navigation:position } : state);
if (state.complete && navigationRoot) {
navigationRoot.querySelector('#search-preview-status').textContent = 'Search pass complete.';
}
}
function run(action, pending, success, detail) {
if (mutationRequest) return mutationRequest;
publish({ status: pending, item: current, detail });
mutationRequest = mutate(detail, action).then(result => {
publish({ status: success, item: current, detail, result });
return result;
}).catch(error => {
publish({ status: 'ready', item: current, detail, error });
throw error;
}).finally(() => { mutationRequest = null; });
return mutationRequest;
}
function loadConversation(detail, requestGeneration, page) {
if (typeof fetchConversation !== 'function') return Promise.resolve(null);
const previousComments = page && Array.isArray(conversation?.comments)
? conversation.comments : [];
conversation = {
status:'loading', comments:previousComments, olderPage:page ?? null,
reviewedAt:detail?.following === true ? detail.reviewed_at : null,
};
publish({ status:'ready', item:current, detail, conversation });
return fetchConversation(detail, page).then(result => {
if (requestGeneration !== generation) return result;
const incoming = Array.isArray(result?.comments) ? result.comments : [];
const liveComments = Array.isArray(conversation?.comments) ? conversation.comments : previousComments;
const comments = [...incoming, ...liveComments].filter((comment, index, all) =>
all.findIndex(candidate => candidate?.id === comment?.id) === index);
conversation = {
status:'ready',
comments,
olderPage:result?.older_page ?? null,
reviewedAt:detail?.following === true ? detail.reviewed_at : null,
};
publish({ status:'ready', item:current, detail:current, conversation });
return result;
}).catch(error => {
if (requestGeneration === generation) {
conversation = {
status:'error', comments:previousComments,
olderPage:page ?? conversation?.olderPage ?? null, error,
reviewedAt:detail?.following === true ? detail.reviewed_at : null,
};
publish({ status:'ready', item:current, detail:current, conversation });
}
return null;
});
}
function loadReview(detail, requestGeneration) {
if (!(detail?.following === true && detail?.kind === 'pull' && typeof fetchReview === 'function')) {
review = null;
return Promise.resolve(null);
}
review = {status:'loading', data:null};
publish({status:'ready', item:current, detail:current, conversation, review});
return fetchReview(detail).then(data => {
if (requestGeneration === generation) {
review = {status:'ready', data};
publish({status:'ready', item:current, detail:current, conversation, review});
}
return data;
}).catch(error => {
if (requestGeneration === generation) {
review = {status:'error', data:null, error};
publish({status:'ready', item:current, detail:current, conversation, review});
}
return null;
});
}
async function acknowledgeWhenContextReady(requestGeneration) {
const conversationReady = typeof fetchConversation !== 'function' || conversation?.status === 'ready';
const reviewRequired = current?.following === true && current?.kind === 'pull' &&
typeof fetchReview === 'function';
if (conversationReady && (!reviewRequired || review?.status === 'ready')) {
await notifyOpened(current, requestGeneration);
}
}
const conversationPager = {
snapshot() {
return {...(conversation || {}), comments:(conversation?.comments || []).map(comment => ({...comment}))};
},
replace(comment) {
if (!comment || !Number.isInteger(comment.id) || !conversation) return conversationPager.snapshot();
conversation = {...conversation, comments:(conversation.comments || []).map(existing =>
existing.id === comment.id ? {...comment} : existing)};
publish({status:'ready', item:current, detail:current, conversation});
return conversationPager.snapshot();
},
remove(commentId) {
if (!conversation) return conversationPager.snapshot();
conversation = {...conversation, comments:(conversation.comments || []).filter(comment => comment.id !== commentId)};
publish({status:'ready', item:current, detail:current, conversation});
return conversationPager.snapshot();
},
};
const api = {
commentPager() { return conversationPager; },
hasReplyAttachments() {
return Boolean(hasAttachments?.());
},
open(item) {
if (current && !sameItem(current, item) && hasAttachments?.()) clearAttachments?.();
generation += 1;
const requestGeneration = generation;
current = { ...item };
conversation = null;
review = null;
openedRevision = null;
publish({ status: 'loading', item: current });
return fetchJson(current).then(async detail => {
if (requestGeneration === generation) {
current = { ...current, ...detail };
if (typeof fetchConversation === 'function') {
const context = loadConversation(current, requestGeneration);
if (current.following === true) {
const reviewContext = loadReview(current, requestGeneration);
await Promise.all([context, reviewContext]);
await acknowledgeWhenContextReady(requestGeneration);
} else {
await notifyOpened(current, requestGeneration);
}
} else {
publish({ status: 'ready', item: current, detail });
await notifyOpened(current, requestGeneration);
}
}
return detail;
}).catch(error => {
if (requestGeneration === generation) {
publish({ status: 'error', item: current, error });
}
throw error;
});
},
close() {
clearAttachments?.();
generation += 1;
current = null;
onState({ status: 'closed' });
},
previous() {
const session = typeof getSession === 'function' ? getSession() : null;
const items = Array.isArray(session?.items) ? session.items : [];
const index = items.findIndex(item => sameItem(item, current));
if (index <= 0) return Promise.resolve(current);
const item = items[index - 1];
if (typeof onNavigate === 'function') onNavigate(item);
return api.open(item);
},
next() {
if (moveRequest) return moveRequest;
moveRequest = (async () => {
let session = typeof getSession === 'function' ? getSession() : null;
let items = Array.isArray(session?.items) ? session.items : [];
let index = items.findIndex(item => sameItem(item, current));
if (index < 0) return current;
if (index === items.length - 1 && session?.more === true && typeof loadMore === 'function') {
await loadMore();
session = getSession();
items = Array.isArray(session?.items) ? session.items : [];
index = items.findIndex(item => sameItem(item, current));
}
if (index >= 0 && index < items.length - 1) {
const item = items[index + 1];
if (typeof onNavigate === 'function') onNavigate(item);
return api.open(item);
}
publish({ status:'ready', item:current, detail:current, complete:true });
return current;
})().finally(() => { moveRequest = null; });
return moveRequest;
},
async retryConversation() {
if (!current) return null;
const requestGeneration = generation;
const result = await loadConversation(current, requestGeneration);
if (current?.following === true) await acknowledgeWhenContextReady(requestGeneration);
return result;
},
async retryReview() {
if (!current) return null;
const requestGeneration = generation;
const result = await loadReview(current, requestGeneration);
await acknowledgeWhenContextReady(requestGeneration);
return result;
},
loadOlderConversation() {
if (!current || !conversation?.olderPage) return Promise.resolve(null);
return loadConversation(current, generation, conversation.olderPage);
},
saveReplyDraft(body) {
if (!current) return '';
const key = replyKey(current, 'draft');
const next = String(body || '');
if (stored(key) !== next) save(replyKey(current, 'operation'), '');
save(key, next);
return next;
},
replyDraft() {
return current ? stored(replyKey(current, 'draft')) : '';
},
reply({ advance = false } = {}) {
if (replyRequest) return replyRequest;
if (!current || (typeof postReply !== 'function' && typeof queueReply !== 'function')) {
return Promise.reject(new Error('Replying is unavailable.'));
}
const body = api.replyDraft().trim();
if (!body && !hasAttachments?.()) return Promise.reject(new Error('Write a reply or add a photo first.'));
const item = { ...current };
const operationKey = replyKey(item, 'operation');
let operationId = stored(operationKey);
if (!operationId) {
operationId = String(createOperationId?.() || Date.now()).slice(0, 128);
save(operationKey, operationId);
}
publish({ status:'replying', item:current, detail:current, conversation });
replyRequest = (typeof queueReply === 'function' ?
Promise.resolve(queueReply(item, body, operationId)) :
Promise.resolve(typeof prepareReply === 'function' ? prepareReply(item, body) : body)
.then(preparedBody => {
if (!String(preparedBody || '').trim()) throw new Error('Write a reply or add a photo first.');
return postReply(item, preparedBody, operationId);
})
).then(async comment => {
await afterReply?.(item);
if (!comment?.queued) {
const comments = [...(conversation?.comments || [])];
if (!comments.some(candidate => candidate?.id === comment?.id)) comments.push(comment);
conversation = { status:'ready', comments, olderPage:conversation?.olderPage ?? null };
}
save(replyKey(item, 'draft'), '');
save(operationKey, '');
clearAttachments?.();
publish({ status:comment?.queued ? 'queued' : 'replied', item:current, detail:current, conversation, result:comment });
return advance ? api.next().then(() => comment) : comment;
}).catch(error => {
publish({ status:'reply-error', item:current, detail:current, conversation, error });
throw error;
}).finally(() => { replyRequest = null; });
return replyRequest;
},
share(url) {
if (shareRequest) return shareRequest;
if (!current || typeof share !== 'function') return Promise.reject(new Error('Sharing is unavailable.'));
const detail = current;
publish({ status:'sharing', item:current, detail });
shareRequest = share(url).then(result => {
publish({ status:result, item:current, detail });
return result;
}).catch(error => {
publish({ status:error?.name === 'AbortError' ? 'share-canceled' : 'share-error', item:current, detail, error });
throw error;
}).finally(() => { shareRequest = null; });
return shareRequest;
},
claim(detail) {
return run('claim', 'claiming', 'claimed', detail);
},
reopen(detail) {
return run('reopen', 'reopening', 'reopened', detail);
},
reopenPull(detail) {
return run('reopen-pull', 'reopening', 'reopened', detail);
},
setWatching(watching) {
if (watchRequest) return watchRequest;
if (!current || typeof watch !== 'function') {
return Promise.reject(new Error('Watching is unavailable.'));
}
const detail = current;
publish({ status:watching ? 'watching' : 'unwatching', item:current, detail });
watchRequest = watch(detail, watching).then(async result => {
current = { ...current, watching:result?.watching === true };
const status = result?.following_synced === false
? 'watch-partial' : (watching ? 'watched' : 'unwatched');
publish({ status, item:current, detail:current, result });
if (!watching && result?.watching === false && typeof afterUnwatch === 'function') {
const next = await afterUnwatch({...current});
if (next) {
onNavigate?.(next);
await api.open(next);
}
}
return result;
}).catch(error => {
publish({ status:'watch-error', item:current, detail, error });
throw error;
}).finally(() => { watchRequest = null; });
return watchRequest;
},
};
if (navigationRoot) {
navigationRoot.querySelector('#previous-search-result').addEventListener('click', () => api.previous().catch(() => {}));
navigationRoot.querySelector('#next-search-result').addEventListener('click', () => api.next().catch(() => {}));
navigationRoot.querySelector('#retry-search-preview-conversation').addEventListener('click', () => api.retryConversation());
navigationRoot.querySelector('#retry-search-preview-review').addEventListener('click', () => api.retryReview());
navigationRoot.querySelector('#load-older-search-preview-comments').addEventListener('click', () => api.loadOlderConversation());
const reply = navigationRoot.querySelector('#search-preview-reply');
reply?.addEventListener('input', event => {
api.saveReplyDraft(event.target.value);
const disabled = !event.target.value.trim();
navigationRoot.querySelector('#send-search-preview-reply').disabled = disabled;
navigationRoot.querySelector('#send-search-preview-reply-next').disabled = disabled;
});
reply?.addEventListener('focus', event => event.target.scrollIntoView({ block:'center', behavior:'smooth' }));
navigationRoot.querySelector('#send-search-preview-reply')?.addEventListener('click', () => api.reply().catch(() => {}));
navigationRoot.querySelector('#send-search-preview-reply-next')?.addEventListener('click', () =>
api.reply({ advance:true }).catch(() => {}));
}
return api;
};
});