Where attention appears to go across agent threads: collector, web explorer, and native iOS client
agents docs ios-parity.md
16 kB · 246 lines
Markdown
at main

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, drops thread and 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 to https://{repo}. For DID paths the label is the project (attention-shared.mjs:76-99).

  • repositories(thread) = the primary repo plus thread.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
  • 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.errors with " · " (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, updated is finite, events is 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.events and "You can open the conversations while the summary is prepared."
    • normal: day.recap as 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
  • 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.coverage plus 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: at is finite, the thread exists, kind is voice|message. countsByDay feeds 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
  • / 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.errors plus the day's error (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-observatory cookie 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 of provider:rawid, first 24 hex characters)
    • provider, title, display_title, project, cwd, model, branch
    • repo, repo_source, host, worktree, workspace_status
    • linked_pr?, linked_issue?
    • archived, subagent
    • updated (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.