Documentation
¶
Overview ¶
Package notify records that a target has news, whether it finished the current request, asks a question, or waits for a permission, and fans that out to the web UI. Events are deliberately not classified further. They are produced by provider-native signals: Claude Code Stop/Notification hooks dropping JSON files into the provider inbox, the copilot terminal bell, and shell prompt marks. State is one small JSON file in the dev-cockpit state directory, read and written through the file on every call so a fresh process picks up the latest entries.
Index ¶
- Constants
- func DockerTarget(project string) string
- func DockerTargetProject(targetID string) string
- func GitPromptTarget(project string) string
- func GitPromptTargetProject(targetID string) string
- func InboxDir(stateDir, coderID string) string
- func IsDockerTarget(targetID string) bool
- func IsGitPromptTarget(targetID string) bool
- func StorePath(stateDir string) string
- type Event
- type Notification
- type Resolver
- type Service
- func (s *Service) Add(targetID string)
- func (s *Service) List(limit int) []Notification
- func (s *Service) MarkAllRead() int
- func (s *Service) MarkRead(id string) int
- func (s *Service) MarkTargetRead(targetID string) int
- func (s *Service) PruneTargets(keep map[string]bool) int
- func (s *Service) RunInbox(dir string, interval time.Duration)
- func (s *Service) SetSignal(listen func(targetID string))
- func (s *Service) SetSilent(quiet func(targetID string) bool)
- func (s *Service) Signal(targetID string)
- func (s *Service) Subscribe() (<-chan Event, func())
- func (s *Service) UnreadCount() int
- func (s *Service) UnreadEvent() Event
- func (s *Service) UnreadTargets() map[string]bool
- type TargetInfo
Constants ¶
const BackupTarget = "backup"
BackupTarget is the well known target id for finished backup jobs. It is no terminal, so the restore prune keeps it alive explicitly and it can never collide with the UUID shaped session ids.
const DockerTargetPrefix = "docker:"
DockerTargetPrefix names the targets of finished docker compose runs, one per project (`docker:<project>`), under the same rules as BackupTarget.
One target per project and not one for all of docker, because a target is what holds at most one unread entry: bringing two projects down at the same moment is two pieces of news and has to read as two, while a down and an up of the same project seconds apart is one and still collapses.
const GitPromptTargetPrefix = "gitprompt:"
GitPromptTargetPrefix names the targets of standing askpass questions, one per project (`gitprompt:<project>`), under the same rules as the docker targets: per project because a target holds at most one unread entry, and two projects asking at the same moment are two pieces of news.
The entry is how a question reaches somebody with no page open at all, the phone in the pocket: the unread entry rides the push channels, and any page it opens shows the app-wide dialog. A question that is answered, cancelled or timed out marks its target read again, so the bell never claims a question that no longer stands.
Variables ¶
This section is empty.
Functions ¶
func DockerTarget ¶
DockerTarget is the target id one project's compose runs report under.
func DockerTargetProject ¶
DockerTargetProject answers the project such an id names.
func GitPromptTarget ¶
GitPromptTarget is the target id one project's standing questions report under.
func GitPromptTargetProject ¶
GitPromptTargetProject answers the project such an id names.
func InboxDir ¶
InboxDir returns the directory event files for one coder are dropped into, the ingestion seam kept next to the store but clearly separate from it. Claude Code hooks (injected via --settings when a coder starts) write there; for other coders it is the generic seam, used by the e2e suite.
func IsDockerTarget ¶
IsDockerTarget reports whether an id is one of them.
func IsGitPromptTarget ¶
IsGitPromptTarget reports whether an id is one of them.
Types ¶
type Event ¶
type Event struct {
Unread int `json:"unread"`
Targets []string `json:"targets"`
Added *Notification `json:"added,omitempty"`
}
Event is one fan-out message to SSE subscribers. Targets carries the ids of every target with an unread entry, so pages can mark target lists live. Added is set only when a new unread notification was ingested (the toast trigger); events without it follow a read-state change or an entry that was written read.
type Notification ¶
type Notification struct {
ID string `json:"id"`
TargetID string `json:"targetId"`
TargetName string `json:"targetName"`
Title string `json:"title,omitempty"`
// Detail is the line below the title, shown where the project would
// stand (list, toast, push body). A title that only says that something
// happened leaves the reader guessing what about, so this names it: the
// coder, shell, job or backup in quotes plus its project, or the first
// words of the assistant's answer.
Detail string `json:"detail,omitempty"`
Project string `json:"project"`
URL string `json:"url"`
CreatedAt time.Time `json:"createdAt"`
Read bool `json:"read"`
// Silent marks an entry that was written read from the start, see
// SetSilent. It is what lets the next silent entry of the same target
// replace this one without touching the lines a person really read.
Silent bool `json:"silent,omitempty"`
}
Notification is one entry in the notification center: this target (a coder or shell) has news. URL is the page the entry links to. Every entry is written as two lines: Title says what happened, Detail says which one it happened in. Title, when set, replaces the generic "Something new in ..." wording everywhere the entry surfaces; an entry without one is a target nobody could resolve or an entry from an older build, and falls back to it.
type Resolver ¶
type Resolver func(targetID string) TargetInfo
Resolver looks up display context for a target.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service owns the persistent notification list and its subscribers. Safe for concurrent use.
func NewService ¶
NewService returns a service persisting to path. The resolver may be nil, then names fall back to the target identifier.
func (*Service) Add ¶
Add ingests one event, collapses older unread entries of the same target, and notifies subscribers. A target therefore holds at most one unread entry, no matter how many signals fired. Entries start unread unless the silent predicate claims the target; the client marks them read when the target's page is visibly open.
func (*Service) List ¶
func (s *Service) List(limit int) []Notification
List returns the stored notifications, newest first, capped at limit (0 means all).
func (*Service) MarkAllRead ¶
MarkAllRead marks every notification read.
func (*Service) MarkTargetRead ¶
MarkTargetRead marks every notification of one target read. Called when the target's attach page is opened, so seen targets clear themselves.
func (*Service) PruneTargets ¶
PruneTargets drops stored notifications whose target id is not in keep. The startup terminal restore calls it with everything that still resolves: the live and resumable terminals, the backup target, and the docker and git prompt targets of every project that still exists. What is not in the set would link nowhere forever, a dead terminal's entry and a deleted project's compose run alike. Returns how many entries were removed.
func (*Service) RunInbox ¶
RunInbox polls the provider's inbox directory and ingests every completed event file. Writers put a .tmp name first and rename to .json, so a .json file is always complete. Blocks; run it in a goroutine.
func (*Service) SetSignal ¶
SetSignal installs a listener that hears every signal this service ingests, before anything is collapsed or deduplicated. It exists because the entries here are for a person, with their read state and their quiet window, while another consumer needs the raw fact that a target reported. This service does not know what the listener does with it and must not: it classifies nothing. Set it before the pollers start.
func (*Service) SetSilent ¶
SetSilent installs the predicate that decides whether a target's news is written read from the start. It exists for the one case where somebody else is already looking at that target: the assistant steers a job on it, it looks into that coder when it reports, and its report is the message that reaches the user. Ringing for the coder as well would say the same thing twice, and the raw one would say it first and with less to say. The entry is still written so the history stays complete, and it surfaces nowhere: it counts as no unread, marks neither coder nor project, and raises no toast, no jingle and no push.
Like SetSignal it is set after construction, because this service classifies nothing and must not learn what a job is. Set it before the pollers start.
func (*Service) Signal ¶
Signal is one ingested signal: the notification for the person, and the raw fact for whoever else listens. Every source of news goes through here, the inbox files a coder's hooks drop and the bell a pane rings.
func (*Service) Subscribe ¶
Subscribe registers a fan-out channel. The returned cancel func must be called when the subscriber goes away.
func (*Service) UnreadCount ¶
UnreadCount returns how many stored notifications are unread.
func (*Service) UnreadEvent ¶
UnreadEvent returns the current unread state as a fan-out event, used as the initial SSE payload.
func (*Service) UnreadTargets ¶
UnreadTargets returns the ids of every target with an unread notification, so lists can mark targets that have news.
type TargetInfo ¶
type TargetInfo struct {
Name string
Title string
Detail string
Project string
URL string
// Urgent marks news the dedupe window must not swallow: a compose run
// that failed right after one that went through says the opposite of the
// fresh unread entry standing there, and that word is owed. The resolver
// decides at ingest, this service still classifies nothing.
Urgent bool
}
TargetInfo carries display context resolved at ingest time.