Something went wrong. Try again.
Where attention appears to go across agent threads: collector, web explorer, and native iOS client
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263// @ts-checkimport { plural, shiftDay } from "./dates.mjs";import { rankRepositories, repositories } from "./repos.mjs";/** @typedef {import("./contract.mjs").Digest} Digest *//** @typedef {import("./contract.mjs").DigestDay} DigestDay *//** @typedef {import("./contract.mjs").WorkGroup} WorkGroup *//** @typedef {import("./contract.mjs").Thread} Thread *//** @typedef {import("./contract.mjs").Visit} Visit *//** @typedef {import("./contract.mjs").Source} Source *//** @typedef {import("./dates.mjs").Calendar} Calendar *//** @typedef {{day: string, group: WorkGroup}} DatedGroup */
export const VISIBLE_AREAS = 5;export const FIND_LIMIT = 30;export const THREAD_LIMIT = 20;export const EXCERPT_LIMIT = 420;
/** @param {Digest | null} digest @returns {DatedGroup[]} */export function allGroups(digest) { return Object.entries(digest?.days || {}) .sort((a, b) => b[0].localeCompare(a[0])) .flatMap(([day, data]) => (data.pending ? [] : data.groups.map((group) => ({ day, group }))));}
/** @param {Digest} digest @param {string} query */export function findWork(digest, query) { const q = query.toLowerCase().trim(); return allGroups(digest).filter(({ group }) => `${group.title} ${group.summary} ${group.sources.map((e) => e.text).join(" ")} ${group.threads .map((id) => { const t = digest.threads[id]; return `${t?.project || ""} ${t?.repo || ""}`; }) .join(" ")}` .toLowerCase() .includes(q), );}
/** @param {Digest} digest @param {string} query @returns {[string, Thread][]} */export function findThreads(digest, query) { const q = query.toLowerCase().trim(); if (!q) return []; return Object.entries(digest.threads) .filter(([, t]) => `${t.title} ${t.project} ${t.repo}`.toLowerCase().includes(q)) .slice(0, THREAD_LIMIT);}
/** @param {Digest} digest @param {string} day @param {WorkGroup} group */export function relatedGroups(digest, day, group) { return allGroups(digest) .filter((item) => item.day !== day && item.group.threads.some((id) => group.threads.includes(id))) .slice(0, 4);}
/** @param {Digest} digest @param {string} day @param {string} id */export function findGroup(digest, day, id) { const entry = digest.days[day]; return entry && !entry.pending ? entry.groups.find((g) => g.id === id) || null : null;}
/** @param {Digest} digest @param {WorkGroup} group */export function groupThreads(digest, group) { return group.threads.flatMap((id) => (digest.threads[id] ? [/** @type {[string, Thread]} */ ([id, digest.threads[id]])] : []));}
/** @param {Digest} digest @param {WorkGroup} group */export function groupProviders(digest, group) { return groupThreads(digest, group).map(([, t]) => t.provider);}
/** @param {Digest} digest @param {WorkGroup} group */export function groupRepositories(digest, group) { return rankRepositories( group.title, groupThreads(digest, group).flatMap(([, t]) => repositories(t)), );}
/** Seven day keys ending today, or at selected+3 when looking further back. @param {string} selected @param {string} today */export function weekDays(selected, today) { const end = selected > shiftDay(today, -6) ? today : shiftDay(selected, 3); return Array.from({ length: 7 }, (_, i) => shiftDay(end, i - 6));}
/** * @param {Digest | null} digest * @param {Map<string, Set<string>> | null} dayCounts * @param {string} day */export const hasRecords = (digest, dayCounts, day) => Boolean(digest?.days[day]?.count || dayCounts?.get(day)?.size);
/** * @param {string} today * @param {Digest | null} digest * @param {Map<string, Set<string>> | null} dayCounts */export function calendarDays(today, digest, dayCounts) { return Array.from({ length: 30 }, (_, ago) => { const day = shiftDay(today, -ago); const entry = digest?.days[day]; const counted = dayCounts?.get(day); return { day, detail: entry ? plural(entry.threads, "thread") : counted ? plural(counted.size, "thread") : "No records", }; });}
/** * @param {DigestDay | undefined} day * @returns {{kind: "quiet"} | {kind: "pending", threads: string[], day: DigestDay & {pending: true}} | {kind: "recap", day: DigestDay & {pending: false}}} */export function dayState(day) { if (!day) return { kind: "quiet" }; if (day.pending) return { kind: "pending", threads: [...new Set(day.events.map((e) => e.thread))], day }; return { kind: "recap", day };}
/** @param {DigestDay & {pending: false}} day */export const dayNote = (day) => day.updating ? `${plural(day.new_count, "new interaction")} since this recap. An update is on its way.` : `From ${plural(day.count, "recorded interaction")}. AI summaries, with sources inside.`;
/** @param {WorkGroup} group @param {Calendar} cal */export const workRowMeta = (group, cal) => `${plural(group.threads.length, "conversation")} · ${cal.time(group.last)}`;
/** @param {WorkGroup} group */export const workSheetMeta = (group) => `${plural(group.threads.length, "conversation")} · ${plural(group.count, "interaction")} · AI summary`;
/** @param {DatedGroup} item @param {Calendar} cal */export const findRowMeta = (item, cal) => `${cal.longDate(item.day)} · ${plural(item.group.threads.length, "conversation")}`;
/** @param {Source} source */export function excerpt(source) { return source.text.length > EXCERPT_LIMIT ? { preview: source.text.slice(0, EXCERPT_LIMIT) + "…", full: source.text } : { preview: source.text, full: null };}
/** @param {Source} source @param {Digest} digest @param {Calendar} cal */export const sourceFooter = (source, digest, cal) => `${source.kind === "voice" ? "Spoken" : "Message"} at ${cal.time(source.at)} · ${digest.threads[source.thread]?.title || "Conversation"}`;
/** @param {WorkGroup} group @param {Digest} digest @param {Calendar} cal */export const visitLines = (group, digest, cal) => [...group.visits] .sort((a, b) => a.start - b.start) .map( (v) => `${cal.time(v.start)}${v.end - v.start > 60 ? " – " + cal.time(v.end) : ""} · ${plural(v.count, "interaction")} · ${digest.threads[v.thread]?.title || "Conversation"}`, );
/** @param {Digest | null} digest @param {string} day */export const digestNotice = (digest, day) => [...(digest?.errors || []), digest?.days[day]?.error].filter(Boolean).join(" ");
export const dayCopy = { initialRecap: "Finding the work behind your conversations…", preparing: "Preparing your day", quietRecap: "A quiet page.", quietTitle: "No recorded conversations", quietText: "There may have been plenty going on. We only see interactions recorded in your local agent logs.", quietNote: "Choose another day to look back.", pendingRecap: "Your conversations are here. The recap is still being prepared.", pendingNote: "You can open the conversations while the summary is prepared.", signedOutRecap: "Your conversations stay private.", signedOutNotice: "Sign in to see your history.", refreshFailed: "Couldn’t refresh your day. Try again in a moment.", /** @param {number} n */ moreAreas: (n) => `${n} more areas`,};
export const findCopy = { placeholder: "Work, topic, or conversation", recent: "Recent work", matching: "Matching work", capped: "Showing 30 results. Search to narrow them down.", emptyTitle: "No matching work", emptyText: "Try a project name or a phrase from a conversation.", conversations: "Conversations",};
export const sheetCopy = { viewOnTimeline: "View conversations on timeline", conversations: "Conversations", otherDays: "Other days in these conversations", yourWords: "Your words", yourWordsNote: "The interactions behind this summary.", readFull: "Read full excerpt", method: "How this was put together", workMethod: "These interactions were grouped by their subject, including across threads. The short recap uses sampled excerpts, not a complete transcript. The periods below show where interactions occurred; their length is not time spent paying attention.", threadSummary: "Recorded interactions in this conversation.", otherInRepo: "Other conversations in this repository", recorded: "Recorded interactions", recordedNote: "Message records and spoken turns. Their spacing is not time spent paying attention.", threadMethod: "Filters affect which records are shown. Estimates are calculated across all threads first, so filtering cannot bridge hidden interactions. Repository links come from recorded remote metadata.",};
/** @param {string} zone */export const aboutCopy = (zone) => ({ paragraphs: [ "A recollection of where you showed up in your conversations with agents.", "AI groups related work and writes short recaps from your messages and spoken turns. Tap any area to see its sources. It can get a connection wrong.", "Interactions suggest attention; they do not measure it. Local Codex, Claude and Pi histories include Orca-managed sessions. Silent reading and activity outside collected logs are missing. Known automated prompts are excluded; other automation may resemble human input.", ], note: "Recaps use redacted excerpts and your dedicated OpenAI key. History covers the last 30 days.", zone,});
/** @param {number | undefined} at @param {Calendar} cal */export const updateNote = (at, cal) => at ? `Last checked ${cal.time(at)}. Summaries refresh in the background.` : "Summaries refresh in the background.";
/** * Work areas, newest day first, findable by summary, your words, conversations and repos. * @param {Digest} digest */export function workFindables(digest) { return allGroups(digest).map((item) => { const threads = groupThreads(digest, item.group).map(([, t]) => t); return { key: `${item.day}:${item.group.id}`, title: item.group.title, item, fields: [ { label: "Summary", text: item.group.summary }, ...item.group.sources.map((s) => ({ label: s.kind === "voice" ? "You said" : "You wrote", text: s.text })), ...threads.map((t) => ({ label: "Conversation", text: t.display_title || t.title })), ...threads.flatMap((t) => [t.repo, ...(t.repositories || []).map((r) => r.repo)].filter(Boolean).map((repo) => ({ label: "Repository", text: repo }))), ...threads.map((t) => ({ label: "Project", text: t.project })), ], }; });}
/** Conversations findable by project, repository, worktree and branch. @param {Digest} digest */export function threadFindables(digest) { return Object.entries(digest.threads).map(([id, t]) => ({ key: id, id, thread: t, title: t.display_title || t.title, fields: [ t.display_title && t.title !== t.display_title ? { label: "First request", text: t.title } : null, t.worktree ? { label: "Worktree", text: t.worktree } : null, t.branch ? { label: "Branch", text: t.branch } : null, t.repo ? { label: "Repository", text: t.repo } : null, ...(t.repositories || []).map((r) => ({ label: "Repository", text: r.repo })), t.project ? { label: "Project", text: t.project } : null, ].filter((f) => f !== null), }));}