home-order follow-ups: fresh stamps for the blocked group, sort/display agreement, live Alt+N projection

Three follow-ups from the 1.19.0 review of the activity-ordered home screens:

- Hook events now ride the same debounced session state broadcast the
  working/idle handlers use. The blocked group ranks on lastActivityAt, and
  without this a permission prompt raised after page load kept ranking by
  whatever stamp the browser loaded with.

- A working row with no submit stamp now shows the lastActivityAt fallback
  its sort anchor already uses: a row must never be ranked by a number it
  does not display.

- Alt+digit resolves through the live-session projection the render paints
  (sessionOrder minus dead ids), so a stale id cannot shift every painted
  number off its target, web tabs included. New tests pin both surfaces to
  one shared order and the numbering to the live projection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-08-16 20:21:54 +02:00
parent f07905b193
commit cdbde9f36f
5 changed files with 109 additions and 12 deletions
+9 -4
View File
@@ -1116,12 +1116,17 @@ class CodemanApp {
if (digitMatch) {
const idx = parseInt(digitMatch[1], 10) - 1;
// Sessions occupy 1..N and web tabs continue from N+1, matching the
// numbers actually painted on the tabs.
if (idx < this.sessionOrder.length) {
// numbers actually painted on the tabs. Resolve through the same
// live-session projection the render paints: sessionOrder can
// transiently hold a dead id (delete raced against the order sync),
// and raw indexing then names the wrong tab for every key to its
// right, web tabs included.
const live = this.sessionOrder.filter((id) => this.sessions.has(id));
if (idx < live.length) {
e.preventDefault();
this.selectSession(this.sessionOrder[idx]);
this.selectSession(live[idx]);
} else {
const webIdx = idx - this.sessionOrder.length;
const webIdx = idx - live.length;
const webId = (this.webviewOrder || [])[webIdx];
if (webId) {
e.preventDefault();
+6 -4
View File
@@ -118,14 +118,16 @@ Object.assign(CodemanApp.prototype, {
* A WORKING pane is the opposite: it repaints about once a second, so its
* last-activity stamp is always "now" and would report every running turn as
* 0m. The turn's own start is the pane's last Enter (`lastSubmitAt`), which is
* persisted server-side and therefore survives a Codeman restart. A session
* that has never submitted has no anchor at all, and gets no stamp rather than
* a made-up one.
* persisted server-side and therefore survives a Codeman restart. A working
* session with NO submit stamp falls back to `lastActivityAt`, because that is
* exactly what `sessionActivityAnchor` (constants.js) sorts it by: a row must
* never be ranked by a number it does not show.
*
* @returns {{key: string, at: number}|null}
*/
_mobileOverviewSince(state, session) {
const at = state === 'working' ? Number(session.lastSubmitAt) || 0 : Number(session.lastActivityAt) || 0;
const activeAt = Number(session.lastActivityAt) || 0;
const at = state === 'working' ? Number(session.lastSubmitAt) || activeAt : activeAt;
if (!at) return null;
return { key: MOBILE_OVERVIEW_SINCE_LABEL[state] || state, at };
},
+5
View File
@@ -148,6 +148,11 @@ export function registerHookEventRoutes(
...safeData,
...(approvalId && { approvalId }),
});
// Full state ride-along, same shape as the working/idle handlers: the home
// screens rank the blocked group on lastActivityAt, and without this a
// permission prompt raised after page load kept ranking by whatever stamp
// the browser loaded with. Debounced, so a hook burst costs one broadcast.
ctx.broadcastSessionStateDebounced(sessionId);
// Send push notifications for hook events
ctx.sendPushNotifications(`hook:${event}`, {