Add allowed-apps field and active-view drift interrupt UI

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 17:21:12 -04:00
parent 6ca796dee3
commit f73bb21c80
+43
View File
@@ -28,6 +28,12 @@
.buckets { list-style: none; padding: 0; margin: 10px 0 0; font-size: 13px; color: #9aa0b4; } .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; } .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; } .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; }
</style> </style>
</head> </head>
<body> <body>
@@ -71,6 +77,29 @@ function evidenceBlock(ev) {
</div>`; </div>`;
} }
function updateActiveDrift(drift) {
const el = document.getElementById('drift');
if (!el) return;
const status = drift && drift.status;
if (status === 'drifting') {
el.innerHTML = `<div class="drift">
<div class="drift-title">⚠ Possible drift</div>
<div class="drift-reason">${drift.reason || ''}</div>
<div class="drift-actions">
<button id="refocus" type="button">Back to task</button>
<button id="ontask" type="button" class="secondary">This is on task</button>
<button id="enddrift" type="button" class="secondary">End session</button>
</div></div>`;
document.getElementById('refocus').onclick = () => post('/refocus');
document.getElementById('ontask').onclick = () => post('/ontask');
document.getElementById('enddrift').onclick = () => post('/complete');
} else if (status === 'pending') {
el.innerHTML = `<div class="drift-hint">checking focus…</div>`;
} else {
el.innerHTML = '';
}
}
function updatePlanningCoach(coach) { function updatePlanningCoach(coach) {
const statusEl = document.getElementById('coachStatus'); const statusEl = document.getElementById('coachStatus');
if (!statusEl) return; if (!statusEl) return;
@@ -90,6 +119,10 @@ function updatePlanningCoach(coach) {
if (sc && !sc.value.trim()) sc.value = coach.proposal.success_condition || ''; 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 (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); 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 updatePlanningCoach(state.coach); // partial update only
return; return;
} }
if (rs === 'active' && renderedState === 'active') {
updateActiveDrift(state.drift);
return;
}
if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null; } if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null; }
renderedState = rs; renderedState = rs;
if (rs === 'locked') { if (rs === 'locked') {
@@ -115,6 +152,8 @@ function render(state) {
<label>Next action</label><input id="na" placeholder="What exactly will you do?"> <label>Next action</label><input id="na" placeholder="What exactly will you do?">
<label>Success condition</label><input id="sc" placeholder="How do you know it's done?"> <label>Success condition</label><input id="sc" placeholder="How do you know it's done?">
<label>Minutes</label><input id="mins" type="number" min="1" value="25"> <label>Minutes</label><input id="mins" type="number" min="1" value="25">
<label>Allowed apps (comma-separated window classes)</label>
<input id="apps" placeholder="e.g. code, firefox">
<button id="start" disabled>Start commitment</button>`; <button id="start" disabled>Start commitment</button>`;
const na = document.getElementById('na'), sc = document.getElementById('sc'), const na = document.getElementById('na'), sc = document.getElementById('sc'),
mins = document.getElementById('mins'), start = document.getElementById('start'); mins = document.getElementById('mins'), start = document.getElementById('start');
@@ -124,6 +163,8 @@ function render(state) {
next_action: na.value.trim(), next_action: na.value.trim(),
success_condition: sc.value.trim(), success_condition: sc.value.trim(),
timebox_secs: Math.round(+mins.value * 60), 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 = () => { document.getElementById('sharpen').onclick = () => {
const intent = document.getElementById('intent').value.trim(); const intent = document.getElementById('intent').value.trim();
@@ -133,6 +174,7 @@ function render(state) {
} else if (rs === 'active') { } else if (rs === 'active') {
const c = state.commitment || {}; const c = state.commitment || {};
view.innerHTML = `<span class="pill">Active</span> view.innerHTML = `<span class="pill">Active</span>
<div id="drift"></div>
<div class="timer" id="t">--:--</div> <div class="timer" id="t">--:--</div>
<div class="action">${c.next_action || ''}</div> <div class="action">${c.next_action || ''}</div>
<p class="meta">Done when: ${c.success_condition || ''}</p> <p class="meta">Done when: ${c.success_condition || ''}</p>
@@ -143,6 +185,7 @@ function render(state) {
const tick = () => { t.textContent = fmt((c.deadline_unix_secs || 0) - Date.now() / 1000); }; const tick = () => { t.textContent = fmt((c.deadline_unix_secs || 0) - Date.now() / 1000); };
tick(); tick();
countdownTimer = setInterval(tick, 250); countdownTimer = setInterval(tick, 250);
updateActiveDrift(state.drift);
} else if (rs === 'review') { } else if (rs === 'review') {
const c = state.commitment || {}; const c = state.commitment || {};
view.innerHTML = `<span class="pill">Review</span> view.innerHTML = `<span class="pill">Review</span>