web → iOS parity inventory #
Compiled 2026-09-29 from source. Citations are file:line relative to this directory. This is the acceptance list for docs/ios-goal.md. When the web app changes, update this file in the same commit.
0. shell #
- Header title follows the view (Sessions / Your day / Timeline / Find). Page title is "{Title} · Agents" (attention.mjs:431-439, index.html:18-26).
- The session count and connection status show only in Sessions (attention.mjs:440-441).
- ⋯ opens settings (index.html:30-40, attention.mjs:559).
- Refresh appears on every view except Sessions. It forces a refresh and is disabled while the digest loads (index.html:205, attention.mjs:442,527,547,550).
- The notice banner is hidden in Sessions or when empty (attention.mjs:42-45,443).
- Tab bar has four tabs with icons; the active one is marked (index.html:207-232, attention.mjs:444-450).
- Switching tabs keeps
day, dropsthreadand the hash, and refreshes. The Find tab focuses search (attention.mjs:53-66,585-589). - Scroll position is kept per view (attention.mjs:481-486). Search and provider choice stay in memory across tabs (tests/ui-regression.md:72).
- Content inset 20px; sticky header; fixed tab bar with safe area (attention.css:79-84,285-295).
1. theme #
- Appearance: System / Light / Dark, persisted to
observatory-theme(index.html:306-312, attention.mjs:593-605). - Light tokens: bg #f2f2f7, surface #fff, text #1c1c1e, secondary #6c6c70, line #e5e5ea, blue #006ee6, bluewash #eaf2ff (attention.css:8-15).
- Dark tokens: bg #000, surface #1c1c1e, text #f2f2f7, secondary #aaaab1, line #38383a, blue #69aaff, bluewash #14263e (attention.css:593-617).
2. shared row components #
-
Provider logo: Codex uses the text colour, Claude #c4704f, Pi #8461cf; icons are in
icons/(provider-icons.css:1-24, attention-shared.mjs:100-108). -
Harness badge: 32×32 tiles (radius 9), one per distinct provider, overlapping by -10px. Label is "A and B" (attention-shared.mjs:114-126).
-
Repository rail: a horizontal scroll row, de-duplicated, never cut off. Each link reads "{label} ↗" and opens externally with tooltip "host/label · {repo_source||'Recorded remote'}". Hidden when empty. With more than one link it is a labelled scroll region (attention-shared.mjs:127-153).
-
Repo reference rule: must match
host/path, the host must contain a dot, and there must be no credentials, query or hash. Links go tohttps://{repo}. For DID paths the label is the project (attention-shared.mjs:76-99). -
repositories(thread)= the primary repo plusthread.repositories[](attention-shared.mjs:154-156). -
Breadcrumbs: provider, then host, then repo link, or "Workspace: {project}" / "Workspace not recorded" (attention-shared.mjs:157-171).
-
Context details:
- "Archived workspace", or "Workspace {status}" (only when a host is present)
- Worktree and Branch
- GitHub-only "PR #n ↗" and "Issue #n ↗"
- an extra repo rail
(attention-shared.mjs:172-202)
-
Conversation row: goes to session
{provider}:{id}. Shows badge,display_title||title, "host · worktree||project" and ›, with context details and the repo rail below (attention-shared.mjs:203-223).
3. Sessions (app.js) #
Toolbar and filters
- Search matches title, display_title, worktree, host, cwd, model, branch, repo and repositories[].repo, case-insensitive (app.js:70-85).
- Provider segments: "All" plus the providers present, alphabetically. Falls back to All if the chosen provider disappears (app.js:280-304).
- Filter sheet (index.html:122-154, app.js:255-279,315-318):
- activity: Any / 24h (
now−updated<86400) / 2min (status==="recent") - project, from a sorted unique list; the choice survives refreshes
- archived (default off)
- subagents (default off: they are child processes, not threads of work)
- Reset and Done
- activity: Any / 24h (
- Active filter count = (scope≠all) + project + archived + subagents (shared/sessions.mjs).
- The "N filter(s) applied" bar has Clear, which clears filters only (app.js:176-178,319).
- An errors banner joins
snapshot.errorswith " · " (app.js:425-426). - Any change resets the page size to 60 (app.js:302,305-313).
List
- Groups: Today / Yesterday / "Month Year" (app.js:62-69; see the goal's timezone decision).
- Each row (app.js:27-32,49-61,137):
- badge
- title (
display_title||title||"Untitled session") worktree||project||"No project"· host- status: Recent activity / Turn completed / Quiet, plus · Subagent and · Archived
- relative time: Now / Nm / Nh / "Mon D"
- repo rail
- Status tint for
recent: blue foreground on bluewash with a blue border (style.css:215-217,543-547). - Pagination: "Show more (N remaining)" adds 60 (app.js:166-168,337-340).
- Footer: "Recent activity reflects log updates, not process liveness." (index.html:96-98)
- Empty states (app.js:156-158,320-331,433-435):
- loading: "Finding your sessions / Loading session history."
- no data: "No sessions yet / …"
- filtered to nothing: "No matching sessions / …", with a "Clear search & filters" button that also resets search and provider
- failed with nothing loaded: "Unable to load sessions / Reconnecting automatically."
Reader
- Header (app.js:212): provider logo, name and status; title; project and "Updated Mon D, h:mm"; breadcrumbs; context details.
- Session details (app.js:191-192,212,359-370):
- First request (only when a display_title exists)
- Project
- Model
- Branch
- Session ID, with a copy button that shows "Copied"
- Latest activity, newest first (app.js:205-224, style.css:434-443):
- consecutive duplicates removed
- user text in bubbles; assistant plain; system muted
- empty text: "No readable activity in this session's current log tail."
- a tail note follows
- If you have scrolled more than 180px or have text selected, new data shows "New activity ↑" rather than re-rendering. Tapping it re-renders and jumps to the top (app.js:193-202,332-336).
- Unavailable: "Session unavailable / This session may have moved or been removed…" (app.js:181-186).
- "Read only" tag (index.html:106-113).
Connection
- Connection label: Connecting → Up to date ("Last updated h:mm") → Reconnecting (data already loaded) or Offline (nothing loaded) (app.js:427-432).
- Response validation: provider is a non-empty string, id is a string,
updatedis finite,eventsis an array (app.js:407-418). - Web polls every 2s with a 15s timeout, pausing while hidden (app.js:391-441). Native replaces this per the goal.
4. Your day (attention.mjs) #
- Date header (index.html:157-175, attention.mjs:67-73,408-430):
- Today / Yesterday / weekday, plus "Month D"
- calendar button
- 7-day strip: dots on days with a digest count or timeline threads; future days disabled; the strip ends at today if the selected day is within the last 6 days, otherwise at selected+3
- Calendar sheet (attention.mjs:560-584):
- 30 days, each "{DayName}, {Month D}" with "N thread(s)" or "No records"
- Day states (attention.mjs:274-295):
- quiet: "A quiet page." with "No recorded conversations / …" and "Choose another day to look back."
- pending: "Your conversations are here. The recap is still being prepared." with conversation rows from
day.eventsand "You can open the conversations while the summary is prepared." - normal:
day.recapas the headline
- Work rows show the badge for every provider across the group, title, summary, "N conversation(s) · {time(last)}" and ›. Repo rails put repos named in the title first (attention.mjs:297-329).
- The first 5 areas show; the rest are behind "{N} more areas", whose open state is remembered per day (attention.mjs:27,330-345).
- Day note (attention.mjs:347-349):
- "N new interaction(s) since this recap. An update is on its way."
- or "From N recorded interaction(s). AI summaries, with sources inside."
5. Timeline (attention-timeline.mjs) #
- Conversation filter banner "Conversation: {titles}", falling back to "Not in the recorded history" (:93-96).
- Coverage note "Only records since YYYY-MM-DD are available in this window." (:105-108).
- Filters summary "{period} · {project}[ · Search active]" (:97-98).
- Period: Selected day / 7 days ending here / 30 days ending here (1|7|30); it lives in the URL (attention.mjs:91-95, :437).
- Project filter: key
repo||project_key, label is the repo label or "{project} (workspace)", sorted, kept across refreshes (:404-417). - Thread search over title, project and repo, debounced 120ms (:56-67,440-444).
- Clear filters resets project, search and thread (:429-436).
- Stats: interactions, conversations, estimated spans, transitions. Durations format as Ns, Nm, or Hh Mm (:39-46,125-138).
- Day track (:162-273), showing the selected day only:
- "Weekday, Mon D"
- legend: ● Message ◆ Voice
- 5 axis labels between hour-rounded bounds
- one row per thread: badge, title, repo label or project
- spans in blue (0.4 opacity, width 5); messages as ellipses; voice as diamonds (#8b66c1)
- each mark labelled "time · kind"
- tapping a row inspects it; the repo rail sits below
- empty text: "No recorded interactions for this day and these filters…"
- Across days (:139-161,420):
- 30 bars ending at
data.at's day, heights relative to the busiest day - counts respect the filters but not the period
- each bar labelled "{day}: N signals"
- selected day pressed; tapping navigates
- scrolled to the end on first load
- 30 bars ending at
- Breakdown (:274-327):
- group by Project / Repository / Thread
- gap slider 1–15 min (default 5), shown as "N minute(s)"
- rows show label, duration, "N signals · N thread(s)" and a bar
- tapping a row: Thread grouping inspects it; other groupings set the project filter (except
unknown-repo) and inspect the first thread - empty text: "No signals in the selected period."
- Transitions (:328-356):
- the latest 12, newest first, as "{day} · {time} · {gap} between signals" and "From → To"
- tapping inspects "to"
- empty text: "No nearby thread transitions at this threshold."
- overflow note: "Showing the latest 12 of N transitions."
- Coverage and limits:
data.coverageplus fixed caveats (:357). - Model: spans and switches are computed over all events before filtering, cached per gap, then clipped to the period. A switch is kept if either side matches. Isolated signals add nothing (attention-model.mjs:1-41, :99-124).
- Event cleaning:
atis finite, the thread exists, kind is voice|message.countsByDayfeeds the week dots and calendar (:359-428).
6. Find #
- Live search over group title, summary, source texts, and each thread's project and repo, across all days, newest first (attention.mjs:46-52,351-364).
- Heading "Recent work" / "Matching work". Rows show title and "Month D · N conversation(s)". Capped at 30 with "Showing 30 results. Search to narrow them down." (attention.mjs:365-384)
- Empty: "No matching work / Try a project name or a phrase from a conversation." (attention.mjs:385-390)
- With a query, a "Conversations" section of up to 20 matching threads (attention.mjs:391-406).
7. sheets #
Work sheet (attention.mjs:183-258)
- date, title, summary, meta "N conversation(s) · N interaction(s) · AI summary"
- conversation rows that drill into the reader
- "View conversations on timeline" (
thread=ids,period=1) - "Other days in these conversations", up to 4
- "Your words": over 420 characters is cut with "…" plus "Read full excerpt"; footer "Spoken|Message at {time} · {thread}"
- "How this was put together", visits: "{start}[ – {end} if >60s] · N interaction(s) · {thread}"
Thread sheet (attention.mjs:124-167)
- date, or "Mon D – Mon D" when period > 1
- "Recorded interactions in this conversation."
- "N interaction(s) in the selected period"
- conversation row
- "See {Mon D} recap"
- "Other conversations in this repository"
- recorded interactions, newest first: "{Mon D}, {time} · Spoken turn|Message"
- method text
Settings (index.html:279-322, attention.mjs:462-464)
- about text (3 paragraphs)
- appearance
- sign out
- "Last checked {time}. Summaries refresh in the background."
- note: 30 days, the viewer's time zone, OpenAI
8. routes → native deep links #
/and#session={provider}:{id}→ Sessions / reader (attention.mjs:475-480, app.js:228-254)./attention?day&view=day|timeline|find&period&thread=a,b, plus#work=/#thread=. An invalid day falls back to today in the viewer's zone (attention.mjs)./attention/timeline→view=timeline(attention.mjs:468-472).#work=resolves after the digest loads;#thread=resolves after the timeline loads (attention.mjs:488-511).- Verified flow: recap → source session → Back returns to the sheet (tests/ui-regression.md:45,72).
9. refresh and error states #
- The shell refreshes every 60s, but not while hidden or while a sheet is open. The digest is throttled to 60s unless forced (attention.mjs:524,611-619).
- On a digest failure: "Couldn't refresh your day. Try again in a moment." and the old data is kept (attention.mjs:514-549).
- On an attention failure: "Could not refresh interaction history. Your previous view is preserved. Try Refresh."
- Notice =
digest.errorsplus the day'serror(attention.mjs:456-460). - 401 clears everything (attention.mjs:168-182):
- Sessions, digest and timeline data
- the work list and Find results
- the open sheet
- recap becomes "Your conversations stay private."
- notice becomes "Sign in to see your history."
- 503 or another failure keeps content (tests/ui-regression.md:37,49,73).
- Server stale messages:
| Condition | Message |
|---|---|
| Sessions snapshot older than 30s | "Session updates are delayed." |
| Sessions snapshot unreadable | "Session history is temporarily unavailable." |
| Some session sources unreadable | "Some local session sources could not be read." |
| Attention data older than 120s | "Interaction collection is delayed." |
| Attention data missing | "Interaction history is not available yet." |
| Digest older than 900s | "Updates are delayed. Showing the last available recaps." |
| Digest missing | "Daily recaps are being prepared." |
| One day failed | "This recap could not be updated yet." |
Sources: public_snapshot.py:52,68-72; attention.py:266-270; attention_digest.py:212,221,226-227.
10. auth #
- Error codes (web_dashboard.py:26-32):
- state: "That sign-in expired…"
- denied: "Sign-in was cancelled."
- account: "This account cannot access the dashboard."
- server: "Your account server could not complete sign-in…"
- grant: "That sign-in could not be verified…"
- Sign out ends the session server-side (web_dashboard.py:166-172).
- Today the REST routes accept only the
__Host-observatorycookie plus the DID allowlist (web_dashboard.py:49-55,174-177). - HTTPBoundary (http_boundary.py:29-86):
- 600 requests/min
- 30/min on login paths
- more than 8 concurrent → 503 "Server busy"
- handler timeout 45s
- responses are no-store
11. response shapes #
-
/api/sessions→{sessions[], errors[], at}(public_snapshot.py:29-52). Each session:id(sha256 ofprovider:rawid, first 24 hex characters)provider, title, display_title, project, cwd, model, branchrepo, repo_source, host, worktree, workspace_statuslinked_pr?, linked_issue?archived, subagentupdated(epoch seconds)status(recent|completed|quiet)events[≤16]of{at,label,text}repositories[]of{repo,project,repo_source}
-
/api/attention→{at, since, threads{id:{title, project, project_key, repo, provider, display_title, worktree, host, branch, linked_pr, linked_issue, workspace_status, worktree_archived, repo_source, repositories[]}}, events[{thread,at,kind}], errors[], coverage}(attention.py:246-260). -
/api/attention/digest→{at, timezone, days{YYYY-MM-DD:DayEntry}, threads, errors[], model}(attention_digest.py:186,220-228). A DayEntry is either:- generated:
{recap, groups[], count, threads, generated_at, updating?, new_count?, error?} - pending:
{pending:true, groups:[], events[]}
A group is
{id, title, summary, threads[], event_ids[], count, first, last, sources[{id,thread,at,kind,text}], visits[{start,end,thread,count}]}. "Brief check-ins" sorts last, the rest by count. - generated: