-
+
⚠
-
Delete session?
+
Delete session?
This action cannot be undone.
-
+
+
+
+
diff --git a/cmd/odek/ui/js/approvals.js b/cmd/odek/ui/js/approvals.js
index 62d0808..b65d007 100644
--- a/cmd/odek/ui/js/approvals.js
+++ b/cmd/odek/ui/js/approvals.js
@@ -2,7 +2,7 @@
// inline decision card pinned at the bottom of the message stream.
import { S } from './state.js';
import { messagesEl } from './dom.js';
-import { forceScrollBottom } from './utils.js';
+import { forceScrollBottom, announce } from './utils.js';
import { hideEmptyState } from './render.js';
// Approval requests are queued and rendered one at a time as an inline
@@ -193,6 +193,7 @@ function renderApprovalCard(event) {
messagesEl.appendChild(card);
forceScrollBottom();
card.focus({ preventScroll: true });
+ announce('Approval required: ' + (event.risk || 'unknown') + ' risk operation');
}
export function sendApproval(action) {
@@ -211,6 +212,8 @@ export function sendApproval(action) {
S.approvalQueue.shift();
removeActiveApprovalCard();
showNextApproval();
+ announce(action === 'trust' ? 'Risk class trusted for this session' :
+ action === 'approve' ? 'Approved' : 'Denied');
}
// Keyboard operation while an approval card is active. Ignored when the
diff --git a/cmd/odek/ui/js/input.js b/cmd/odek/ui/js/input.js
index 97dac8b..d2cc4f1 100644
--- a/cmd/odek/ui/js/input.js
+++ b/cmd/odek/ui/js/input.js
@@ -107,7 +107,13 @@ function renderFileChips() {
const remove = document.createElement('span');
remove.className = 'chip-remove';
remove.textContent = '✕';
+ remove.setAttribute('role', 'button');
+ remove.setAttribute('tabindex', '0');
+ remove.setAttribute('aria-label', 'Remove ' + f.name);
remove.addEventListener('click', () => removeAttachedFile(i));
+ remove.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); removeAttachedFile(i); }
+ });
chip.append(icon, name, size, remove);
fileChips.appendChild(chip);
@@ -139,10 +145,16 @@ function addErrorChip(name, reason) {
const remove = document.createElement('span');
remove.className = 'chip-remove';
remove.textContent = '✕';
+ remove.setAttribute('role', 'button');
+ remove.setAttribute('tabindex', '0');
+ remove.setAttribute('aria-label', 'Dismiss');
chip.append(icon, label, remove);
fileChips.appendChild(chip);
const dismiss = () => chip.remove();
remove.addEventListener('click', dismiss);
+ remove.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); dismiss(); }
+ });
setTimeout(dismiss, 6000);
}
diff --git a/cmd/odek/ui/js/main.js b/cmd/odek/ui/js/main.js
index 5ddc939..6e24eab 100644
--- a/cmd/odek/ui/js/main.js
+++ b/cmd/odek/ui/js/main.js
@@ -3,7 +3,7 @@
import { S, getSessionToken } from './state.js';
import { apiHeaders } from './net.js';
import { promptEl, skeletonEl, thinkBtn } from './dom.js';
-import { escapeHtml, escapeAttr, showToast, toggleShortcuts, hideCancel } from './utils.js';
+import { escapeHtml, escapeAttr, showToast, toggleShortcuts, hideCancel, closeDialog } from './utils.js';
import { addSystemMessage } from './render.js';
import { loadSessions } from './sessions.js';
import { connect } from './ws.js';
@@ -16,11 +16,18 @@ S.savedEmptyStateNode = document.getElementById('empty-state');
S.savedScrollBtnNode = document.getElementById('scroll-bottom-btn');
// Empty-state hint actions (the saved node is re-appended on session
-// switches, so direct listeners persist).
+// switches, so direct listeners persist). The hints are role="button"
+// spans, so Enter/Space must activate them like a click.
+function activateOnKey(el, fn) {
+ el.addEventListener('click', fn);
+ el.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); fn(); }
+ });
+}
if (S.savedEmptyStateNode) {
const hints = S.savedEmptyStateNode.querySelectorAll('.es-hints span');
- if (hints[0]) hints[0].addEventListener('click', toggleShortcuts);
- if (hints[1]) hints[1].addEventListener('click', loadSessions);
+ if (hints[0]) activateOnKey(hints[0], toggleShortcuts);
+ if (hints[1]) activateOnKey(hints[1], loadSessions);
}
// ── Theme Toggle ──
@@ -189,8 +196,7 @@ document.addEventListener('keydown', (e) => {
// Escape closes overlays. Approval cards are deliberately NOT dismissed —
// a decision must be made explicitly.
if (e.key === 'Escape') {
- document.getElementById('shortcuts-overlay').classList.remove('active');
- document.getElementById('confirm-overlay').classList.remove('active');
+ closeDialog(); // whichever overlay dialog is open (shortcuts / confirm)
S.pendingDeleteId = null;
}
// Ctrl+R refreshes sessions
diff --git a/cmd/odek/ui/js/render.js b/cmd/odek/ui/js/render.js
index 6282ad5..6b5430b 100644
--- a/cmd/odek/ui/js/render.js
+++ b/cmd/odek/ui/js/render.js
@@ -6,7 +6,7 @@ import { messagesEl, promptEl, sendBtn, emptyState } from './dom.js';
import {
escapeHtml, escapeAttr, truncateStr, copyTextToClipboard,
pruneMessages, scrollBottom, forceScrollBottom, stripAttachmentBodies,
- showCancel, hideCancel,
+ showCancel, hideCancel, announce,
} from './utils.js';
import { markdownToHtml } from './markdown.js';
@@ -88,7 +88,7 @@ export function streamThinking(content) {
const block = document.createElement('div');
block.className = 'thinking-block';
block.innerHTML =
- '
' +
+ '
' +
'▶ reasoning' +
'
' +
'
' + escapeHtml(content) + '
';
@@ -110,6 +110,7 @@ function toggleThinking(el) {
if (content) {
content.classList.toggle('open');
arrow.classList.toggle('open');
+ el.setAttribute('aria-expanded', content.classList.contains('open'));
// Auto-open on first click
if (content.classList.contains('open')) {
scrollBottom();
@@ -248,6 +249,7 @@ export function addSystemMessage(content) {
el.className = 'msg system';
el.innerHTML = '
' + escapeHtml(content) + '
';
messagesEl.appendChild(el);
+ announce(content);
pruneMessages();
scrollBottom();
}
@@ -346,7 +348,7 @@ export function addToolCall(name, data) {
const el = document.createElement('div');
el.className = 'tool-block';
el.innerHTML =
- '