Skip to content

Commit f28e7b3

Browse files
authored
Merge pull request #40 from bertheto/feat/ui-13-reply-jump
feat(ui): reply-quote jump navigation + thread creation fix (UI-13, UI-14)
2 parents 9de7156 + dc29e1a commit f28e7b3

7 files changed

Lines changed: 131 additions & 7 deletions

File tree

src/static/css/main.css

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,6 +1042,30 @@ body[data-theme="light"] .msg-reply-quote {
10421042
color: #555;
10431043
}
10441044

1045+
/* UI-13: reply-quote clickable button reset */
1046+
button.msg-reply-quote {
1047+
cursor: pointer;
1048+
background: none;
1049+
border: none;
1050+
padding: 2px 8px;
1051+
font: inherit;
1052+
color: inherit;
1053+
text-align: left;
1054+
display: inline-block;
1055+
}
1056+
button.msg-reply-quote:hover {
1057+
text-decoration: underline dotted;
1058+
opacity: 0.85;
1059+
}
1060+
1061+
/* UI-13: jump highlight on target message */
1062+
.msg-jump-highlight {
1063+
outline: 2px solid var(--accent, #7c6af7);
1064+
outline-offset: 3px;
1065+
border-radius: 6px;
1066+
transition: outline 0.3s ease-out;
1067+
}
1068+
10451069
body[data-theme="light"] .stop-tag {
10461070
background: rgba(100,100,100,0.08);
10471071
color: #555;
@@ -2090,6 +2114,14 @@ body[data-theme="light"] .skills-badge {
20902114
background: #2563eb;
20912115
}
20922116

2117+
.btn-primary:disabled {
2118+
opacity: 0.4;
2119+
cursor: not-allowed;
2120+
}
2121+
.btn-primary:disabled:hover {
2122+
background: var(--accent);
2123+
}
2124+
20932125
/* Thread context menu */
20942126
#thread-context-menu {
20952127
position: fixed;

src/static/index.html

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!DOCTYPE html>
1+
<!DOCTYPE html>
22
<html lang="en">
33

44
<head>
@@ -154,6 +154,7 @@
154154
<acb-modal-shell></acb-modal-shell>
155155

156156
<script src="/static/js/shared-api.js?v=2"></script>
157+
<script src="/static/js/shared-ui-agent.js?v=1"></script>
157158
<script src="/static/js/shared-theme.js?v=2"></script>
158159
<script src="/static/js/shared-utils.js?v=3"></script>
159160
<script src="/static/js/shared-message-renderer.js?v=2"></script>
@@ -984,6 +985,28 @@
984985
}).catch(() => {});
985986
};
986987

988+
// UI-13: scroll to the message referenced by a reply-quote button
989+
window.AcbScrollToMsg = (msgId) => {
990+
const target = document.querySelector(`[data-msg-id="${CSS.escape(msgId)}"]`);
991+
if (!target) return;
992+
const scrollEl = document.getElementById('messages-scroll');
993+
if (scrollEl) {
994+
scrollEl.scrollTo({ top: target.offsetTop - 12, behavior: 'smooth' });
995+
} else {
996+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
997+
}
998+
target.classList.add('msg-jump-highlight');
999+
setTimeout(() => target.classList.remove('msg-jump-highlight'), 1500);
1000+
};
1001+
1002+
// UI-13: event delegation -- intercepts clicks on any .msg-reply-quote in #messages
1003+
document.getElementById('messages')?.addEventListener('click', (e) => {
1004+
const btn = e.target.closest('button.msg-reply-quote');
1005+
if (!btn) return;
1006+
const targetId = btn.dataset.replyTarget;
1007+
if (targetId) window.AcbScrollToMsg(targetId);
1008+
});
1009+
9871010
function appendBubble(m) {
9881011
const box = document.getElementById('messages');
9891012

@@ -1183,9 +1206,9 @@
11831206
priorityBadgeHtml = '<span class="msg-priority-badge msg-priority-system" title="System message">SYSTEM</span>';
11841207
}
11851208

1186-
// Reply-to quote (UP-14)
1209+
// Reply-to quote (UP-14) -- UI-13: clickable button with event delegation
11871210
const replyQuoteHtml = m.reply_to_msg_id
1188-
? `<div class="msg-reply-quote" title="In reply to message ${esc(m.reply_to_msg_id)}">&#8617; In reply to: <em>${esc(m.reply_to_msg_id.slice(0, 8))}</em></div>`
1211+
? `<button class="msg-reply-quote" data-reply-target="${esc(m.reply_to_msg_id)}" title="Jump to message ${esc(m.reply_to_msg_id)}">&#8617; In reply to: <em>${esc(m.reply_to_msg_id.slice(0, 8))}&#8230;</em></button>`
11891212
: '';
11901213

11911214
// Edit indicator (UP-21)

src/static/js/components/acb-modal-shell.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
</div>
4848
<div class="modal-actions">
4949
<button class="btn-secondary" onclick="closeModal()">Cancel</button>
50-
<button class="btn-primary" onclick="submitModal()">Create</button>
50+
<button id="btn-create-thread" class="btn-primary" onclick="submitModal()" disabled>Create</button>
5151
</div>
5252
</div>
5353
</div>
@@ -101,6 +101,22 @@
101101

102102
// Attach minimap toggle listener after DOM is built
103103
this._attachMinimapToggle();
104+
// UI-14: enable Create button only when topic is non-empty
105+
this._attachTopicGuard();
106+
}
107+
108+
_attachTopicGuard() {
109+
const input = this.querySelector("#modal-topic");
110+
const btn = this.querySelector("#btn-create-thread");
111+
if (!input || !btn) return;
112+
const sync = () => { btn.disabled = input.value.trim().length === 0; };
113+
input.addEventListener("input", sync);
114+
// Re-sync when modal is opened (topic may have been cleared)
115+
const overlay = this.querySelector("#modal-overlay");
116+
if (overlay) {
117+
const observer = new MutationObserver(() => sync());
118+
observer.observe(overlay, { attributes: true, attributeFilter: ["style"] });
119+
}
104120
}
105121

106122
_attachMinimapToggle() {

src/static/js/shared-api.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
(function () {
22
async function api(path, opts) {
33
const options = opts || {};
4+
const { headers: extraHeaders, ...restOptions } = options;
45
try {
56
const response = await fetch(path, {
6-
headers: { "Content-Type": "application/json" },
7-
...options,
7+
headers: { "Content-Type": "application/json", ...extraHeaders },
8+
...restOptions,
89
});
910
if (!response.ok) {
1011
console.warn(`[API] ${options.method || 'GET'} ${path} → HTTP ${response.status}`);

src/static/js/shared-chat.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333

3434
const box = document.getElementById("messages");
3535
box.innerHTML = "";
36+
const sysPromptAreaEl = document.getElementById("sys-prompt-area");
37+
if (sysPromptAreaEl) sysPromptAreaEl.innerHTML = "";
3638
box.classList.add("loading-history");
3739

3840
const msgs =

src/static/js/shared-modals.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@
9999
const templateSel = document.getElementById("modal-template");
100100
const template = templateSel ? templateSel.value || null : null;
101101

102+
// UI-14: get or register a browser-session agent to provide auth for thread creation
103+
const uiAgent = window.AcbUiAgent ? await window.AcbUiAgent.ensureUiAgent() : null;
104+
if (!uiAgent) {
105+
console.error("[Thread Create] Could not obtain UI agent token -- cannot create thread");
106+
return;
107+
}
108+
102109
topicInput.value = "";
103110
if (templateSel) templateSel.value = "";
104111
const descEl = document.getElementById("modal-template-desc");
@@ -107,7 +114,11 @@
107114

108115
const t = await api("/api/threads", {
109116
method: "POST",
110-
body: JSON.stringify({ topic, ...(template ? { template } : {}) }),
117+
headers: {
118+
"Content-Type": "application/json",
119+
"X-Agent-Token": uiAgent.token,
120+
},
121+
body: JSON.stringify({ topic, creator_agent_id: uiAgent.agent_id, ...(template ? { template } : {}) }),
111122
});
112123
if (t) {
113124
const syncContext =

src/static/js/shared-ui-agent.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// UI-14: Auto-register a browser-session agent so the UI can create threads.
2+
// Token is stored in sessionStorage (cleared on tab close).
3+
(function () {
4+
const SESSION_KEY = "acb-ui-agent";
5+
6+
async function ensureUiAgent() {
7+
const cached = sessionStorage.getItem(SESSION_KEY);
8+
if (cached) {
9+
try {
10+
const parsed = JSON.parse(cached);
11+
if (parsed.agent_id && parsed.token) return parsed;
12+
} catch (_) {
13+
// corrupted -- fall through to re-register
14+
}
15+
}
16+
17+
try {
18+
const res = await fetch("/api/agents/register", {
19+
method: "POST",
20+
headers: { "Content-Type": "application/json" },
21+
body: JSON.stringify({
22+
name: "ui-human",
23+
display_name: "Browser User",
24+
ide: "browser",
25+
model: "human",
26+
}),
27+
});
28+
if (!res.ok) return null;
29+
const data = await res.json();
30+
if (!data.agent_id || !data.token) return null;
31+
sessionStorage.setItem(SESSION_KEY, JSON.stringify({ agent_id: data.agent_id, token: data.token }));
32+
return { agent_id: data.agent_id, token: data.token };
33+
} catch (_) {
34+
return null;
35+
}
36+
}
37+
38+
window.AcbUiAgent = { ensureUiAgent };
39+
})();

0 commit comments

Comments
 (0)