From f73bb21c8067ac3632c8abcac24a6cc95f538010 Mon Sep 17 00:00:00 2001 From: Felix Martin Date: Sun, 31 May 2026 17:21:12 -0400 Subject: [PATCH] Add allowed-apps field and active-view drift interrupt UI Co-Authored-By: Claude Opus 4.8 --- internal/web/static/index.html | 43 ++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/internal/web/static/index.html b/internal/web/static/index.html index 83b394f..c4f8f38 100644 --- a/internal/web/static/index.html +++ b/internal/web/static/index.html @@ -28,6 +28,12 @@ .buckets { list-style: none; padding: 0; margin: 10px 0 0; font-size: 13px; color: #9aa0b4; } .buckets li { display: flex; justify-content: space-between; padding: 2px 0; font-variant-numeric: tabular-nums; } .switches { font-size: 13px; color: #7a8095; margin-top: 8px; } + .drift { background: #2a1d22; border: 1px solid #5a2d39; border-radius: 10px; padding: 14px 16px; margin-bottom: 16px; } + .drift-title { font-weight: 700; color: #f0a3b1; } + .drift-reason { color: #e6c0c8; margin: 4px 0 10px; } + .drift-actions button { margin: 0 8px 0 0; padding: 8px 14px; } + .drift-actions button.secondary { background: #2d3242; color: #cdd2e0; } + .drift-hint { font-size: 12px; color: #7a8095; margin-bottom: 10px; } @@ -71,6 +77,29 @@ function evidenceBlock(ev) { `; } +function updateActiveDrift(drift) { + const el = document.getElementById('drift'); + if (!el) return; + const status = drift && drift.status; + if (status === 'drifting') { + el.innerHTML = `
+
⚠ Possible drift
+
${drift.reason || ''}
+
+ + + +
`; + document.getElementById('refocus').onclick = () => post('/refocus'); + document.getElementById('ontask').onclick = () => post('/ontask'); + document.getElementById('enddrift').onclick = () => post('/complete'); + } else if (status === 'pending') { + el.innerHTML = `
checking focus…
`; + } else { + el.innerHTML = ''; + } +} + function updatePlanningCoach(coach) { const statusEl = document.getElementById('coachStatus'); if (!statusEl) return; @@ -90,6 +119,10 @@ function updatePlanningCoach(coach) { if (sc && !sc.value.trim()) sc.value = coach.proposal.success_condition || ''; if (mins && coach.proposal.timebox_secs) mins.value = Math.round(coach.proposal.timebox_secs / 60); if (start) start.disabled = !(na.value.trim() && sc.value.trim() && +mins.value > 0); + const apps = document.getElementById('apps'); + if (apps && !apps.value.trim() && Array.isArray(coach.proposal.allowed_window_classes)) { + apps.value = coach.proposal.allowed_window_classes.join(', '); + } } } @@ -99,6 +132,10 @@ function render(state) { updatePlanningCoach(state.coach); // partial update only return; } + if (rs === 'active' && renderedState === 'active') { + updateActiveDrift(state.drift); + return; + } if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null; } renderedState = rs; if (rs === 'locked') { @@ -115,6 +152,8 @@ function render(state) { + + `; const na = document.getElementById('na'), sc = document.getElementById('sc'), mins = document.getElementById('mins'), start = document.getElementById('start'); @@ -124,6 +163,8 @@ function render(state) { next_action: na.value.trim(), success_condition: sc.value.trim(), timebox_secs: Math.round(+mins.value * 60), + allowed_window_classes: (document.getElementById('apps').value || '') + .split(',').map(s => s.trim()).filter(Boolean), }); document.getElementById('sharpen').onclick = () => { const intent = document.getElementById('intent').value.trim(); @@ -133,6 +174,7 @@ function render(state) { } else if (rs === 'active') { const c = state.commitment || {}; view.innerHTML = `Active +
--:--
${c.next_action || ''}

Done when: ${c.success_condition || ''}

@@ -143,6 +185,7 @@ function render(state) { const tick = () => { t.textContent = fmt((c.deadline_unix_secs || 0) - Date.now() / 1000); }; tick(); countdownTimer = setInterval(tick, 250); + updateActiveDrift(state.drift); } else if (rs === 'review') { const c = state.commitment || {}; view.innerHTML = `Review