|
| 1 | +# 098 — slack keeper defers to pinned workspace tab instead of forcing reopen |
| 2 | + |
| 3 | +- **Status:** in-progress |
| 4 | +- **Mode:** HITL |
| 5 | +- **Estimate:** 0.5d |
| 6 | +- **Depends on:** none |
| 7 | +- **Blocks:** none |
| 8 | + |
| 9 | +## Goal |
| 10 | + |
| 11 | +The Slack parked-tab keeper (t070, ADR-0011) reopens an anonymous background tab |
| 12 | +for every registered workspace whose tab is not currently live — **even when the |
| 13 | +user has that workspace pinned**. Closing the workspace's tab immediately spawns a |
| 14 | +stray duplicate in the Tabs list, which is annoying. After this task, the keeper |
| 15 | +**defers to a pin**: a workspace that has a pin is considered covered by its pin and |
| 16 | +the keeper never spawns a duplicate for it. Capture is unaffected because **one live |
| 17 | +Slack tab refreshes creds for all workspaces** (shared `d` cookie + `localConfig_v2` |
| 18 | +holds every team's token) and the sweep polls every workspace over the web API |
| 19 | +regardless of which tab is live. A cred lifeline keeps exactly one Slack tab alive |
| 20 | +(preferring a pinned URL) only when no Slack tab is live and nothing else would open |
| 21 | +one. |
| 22 | + |
| 23 | +## Why now |
| 24 | + |
| 25 | +Daily-driver annoyance on the priority web/PWA surface: the user pins their Slack |
| 26 | +workspaces and the keeper keeps resurrecting closed tabs against their intent. The |
| 27 | +fix is small and server-only, and it tightens guaranteed delivery (ADR-0011) rather |
| 28 | +than weakening it — capture no longer depends on a per-workspace tab. |
| 29 | + |
| 30 | +## Acceptance criteria |
| 31 | + |
| 32 | +- [ ] A registered workspace that has a pin gets **no** anonymous parked tab from |
| 33 | + the keeper (it is not re-created on close). |
| 34 | +- [ ] A registered workspace with **no** pin keeps today's anonymous parked-tab |
| 35 | + behavior (unchanged). |
| 36 | +- [ ] Cred lifeline: when **no** Slack tab is live and no unpinned workspace would |
| 37 | + open one, the keeper opens exactly **one** tab, preferring a pinned URL, so |
| 38 | + creds keep refreshing. |
| 39 | +- [ ] The cred lifeline respects the existing create-cooldown (no spam). |
| 40 | +- [ ] Omitting the pin map preserves byte-identical prior behavior (back-compat). |
| 41 | + |
| 42 | +## Test plan |
| 43 | + |
| 44 | +### Layer 1 — Pure logic (TDD) |
| 45 | + |
| 46 | +- [ ] `core/slack-workspaces.js` `planParkedTabs(registry, live, createdAt, now, pinUrlByTeam)` |
| 47 | + — skips a pinned registered workspace; still plans an unpinned one alongside it. |
| 48 | +- [ ] cred lifeline opens one pinned workspace when `live` is empty and no unpinned |
| 49 | + plan exists; uses the pin URL; respects cooldown; does nothing when a tab is |
| 50 | + live or an unpinned plan already keeps one alive. |
| 51 | +- [ ] back-compat: omitting `pinUrlByTeam` equals the prior result. |
| 52 | + |
| 53 | +### Layer 2 — Manual smoke (CDP/IPC) |
| 54 | + |
| 55 | +Web build (`pnpm web`) against a live Remote Browser with ≥1 Slack workspace pinned: |
| 56 | + |
| 57 | +- [ ] Pin a Slack workspace, open it, then close its tab → keeper does **not** reopen |
| 58 | + a stray; the workspace still receives notifications (sweep continues). |
| 59 | +- [ ] Close **every** Slack tab → keeper opens exactly one (a pinned workspace's URL); |
| 60 | + creds recover and sweeps resume. |
| 61 | +- [ ] An unpinned workspace closed → still auto-reopens (unchanged). |
| 62 | + |
| 63 | +### Layer 3 — Visual review |
| 64 | + |
| 65 | +n/a — no renderer UI is touched (server-side keeper only). |
| 66 | + |
| 67 | +## Design notes |
| 68 | + |
| 69 | +- **Contracts changed:** `planParkedTabs` gains an optional 5th arg |
| 70 | + `pinUrlByTeam: { [teamId]: url }`. A workspace whose `teamId` is a key is skipped |
| 71 | + (pin owns it). When `live` is empty and the normal plan list is empty, the planner |
| 72 | + appends **one** lifeline plan from `pinUrlByTeam` (cooldown-gated) so a single |
| 73 | + Slack tab stays alive for shared-cred refresh. Absent/empty map → prior behavior. |
| 74 | +- The server (`web/server.mjs` `keepSlackTabsAlive`) derives `pinUrlByTeam` from |
| 75 | + `settings.getPins()` via `teamIdOf(pin.url)` and passes it in. No renderer change. |
| 76 | +- **New modules:** none. |
| 77 | +- **New ADR needed?** No — this is a tuning of the t070 keeper within ADR-0011. |
| 78 | + Document in CLAUDE.md (keeper bullet + `slack-workspaces.js` note) and the code. |
| 79 | + |
| 80 | +## Out of scope |
| 81 | + |
| 82 | +- Renderer pin-adoption of a keeper-opened tab mid-session (ADR-0004 keeps |
| 83 | + URL-adoption startup-only; the lifeline tab adopts on next reload). Not needed |
| 84 | + for the chosen "don't reopen — pin owns it" behavior. |
| 85 | +- Reducing the unpinned per-workspace keeper to one-tab-total (kept as-is per the |
| 86 | + decision). |
| 87 | + |
| 88 | +## Definition of Done |
| 89 | + |
| 90 | +- [ ] Layer 1 tests written and green |
| 91 | +- [ ] Layer 2 smoke checklist completed with a live Remote Browser (web build) |
| 92 | +- [ ] `pnpm check:changed` clean |
| 93 | +- [ ] `pnpm typecheck` clean |
| 94 | +- [ ] `pnpm test` green |
| 95 | +- [ ] CLAUDE.md updated (keeper bullet + `slack-workspaces.js` note) |
| 96 | +- [ ] No commented-out code, no `console.log` debris, no AI attribution |
| 97 | +- [ ] Task closed: status → done, file moved to `docs/tasks/done/`, t098 in commit |
| 98 | + |
| 99 | +## Notes |
| 100 | + |
| 101 | +Decided with the user via two grills: (1) "reuse the pin" over "hands-off"; then, |
| 102 | +after surfacing the code fact that one live Slack tab refreshes all workspaces' |
| 103 | +creds, (2) "don't reopen — pin owns it" with a single-tab cred lifeline. The keeper |
| 104 | +was over-aggressive: per-workspace tabs are not needed for the sweep — only one live |
| 105 | +Slack tab is. |
| 106 | + |
| 107 | +--- |
| 108 | + |
| 109 | +_When task status flips to `done`, move this file to `done/`._ |
0 commit comments