notify

package
v1.56.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 26, 2026 License: MIT Imports: 7 Imported by: 0

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

View Source
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.

View Source
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.

View Source
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

func DockerTarget(project string) string

DockerTarget is the target id one project's compose runs report under.

func DockerTargetProject

func DockerTargetProject(targetID string) string

DockerTargetProject answers the project such an id names.

func GitPromptTarget

func GitPromptTarget(project string) string

GitPromptTarget is the target id one project's standing questions report under.

func GitPromptTargetProject

func GitPromptTargetProject(targetID string) string

GitPromptTargetProject answers the project such an id names.

func InboxDir

func InboxDir(stateDir, coderID string) string

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

func IsDockerTarget(targetID string) bool

IsDockerTarget reports whether an id is one of them.

func IsGitPromptTarget

func IsGitPromptTarget(targetID string) bool

IsGitPromptTarget reports whether an id is one of them.

func StorePath

func StorePath(stateDir string) string

StorePath returns the shared notification list file. Like the recent projects store it lives directly in the state dir; separate lists come from separate state dirs.

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

func NewService(path string, resolver Resolver) *Service

NewService returns a service persisting to path. The resolver may be nil, then names fall back to the target identifier.

func (*Service) Add

func (s *Service) Add(targetID string)

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

func (s *Service) MarkAllRead() int

MarkAllRead marks every notification read.

func (*Service) MarkRead

func (s *Service) MarkRead(id string) int

MarkRead marks one notification read and reports the new unread count.

func (*Service) MarkTargetRead

func (s *Service) MarkTargetRead(targetID string) int

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

func (s *Service) PruneTargets(keep map[string]bool) int

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

func (s *Service) RunInbox(dir string, interval time.Duration)

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

func (s *Service) SetSignal(listen func(targetID string))

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

func (s *Service) SetSilent(quiet func(targetID string) bool)

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

func (s *Service) Signal(targetID string)

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

func (s *Service) Subscribe() (<-chan Event, func())

Subscribe registers a fan-out channel. The returned cancel func must be called when the subscriber goes away.

func (*Service) UnreadCount

func (s *Service) UnreadCount() int

UnreadCount returns how many stored notifications are unread.

func (*Service) UnreadEvent

func (s *Service) UnreadEvent() Event

UnreadEvent returns the current unread state as a fan-out event, used as the initial SSE payload.

func (*Service) UnreadTargets

func (s *Service) UnreadTargets() map[string]bool

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL