Documentation
¶
Overview ¶
Package flow implements the flows/*.yaml config schema for the desktop pipeline's Node-RED-style graph: a strict decoder, a per-node-type registry, cross-file reference validation, and directory loaders.
A flow is a directed graph of Node values connected by Wire edges. The flow's id is not stored in the file — it is the filename stem (e.g. "triage.yaml" -> id "triage") so the file and its id can never disagree.
This package is deliberately self-contained: it does not know about Wails, the desktop pipeline database, or cmd/desktop/internal/app/sources/github/feed. Cross-file action lookups are supplied by the caller through the Refs interface, so actions can stay owned by their own package without this package depending on them.
Index ¶
- Constants
- Variables
- func MigrateDir(dir, backupDir string, log *zerolog.Logger) error
- func NodeDoc(nodeType string) (string, error)
- func NodeTypes() []string
- func ResurfacePolicyNames() []string
- func SaveFlow(path string, f Flow) error
- func SaveSidebar(path string, layout SidebarLayout) error
- func SaveUI(path string, layout Layout) error
- type ActionConfig
- type Duration
- type FeedConfig
- type Flow
- type FlowStatus
- type FlowStore
- func (s *FlowStore) Create(name string, seed Seed) (Flow, error)
- func (s *FlowStore) Delete(id string) error
- func (s *FlowStore) Exists(id string) bool
- func (s *FlowStore) Get(id string) (Flow, bool)
- func (s *FlowStore) GetLayout(id string) Layout
- func (s *FlowStore) GetSidebar(id string) SidebarLayout
- func (s *FlowStore) List() []Flow
- func (s *FlowStore) ParseDocument(id string, data []byte) (Flow, []string, error)
- func (s *FlowStore) Reload() error
- func (s *FlowStore) Rename(id, name string) (Flow, error)
- func (s *FlowStore) Save(f Flow) error
- func (s *FlowStore) SaveLayout(id string, layout Layout) error
- func (s *FlowStore) SaveSidebar(id string, layout SidebarLayout) error
- func (s *FlowStore) SetEnabled(id string, enabled bool) (Flow, error)
- func (s *FlowStore) SetImage(id, hash string) (Flow, error)
- func (s *FlowStore) SetOrder(ids []string)
- func (s *FlowStore) SetSourceImage(flowID, nodeID, hash string) (Flow, error)
- func (s *FlowStore) SourceImageRef(flowID, nodeID string) (string, error)
- func (s *FlowStore) Statuses() []FlowStatus
- type FlowsWatcher
- type FunctionConfig
- type GithubFilterConfig
- type HeadlessActionRefs
- type LaunchChatConfig
- type LaunchSessionConfig
- type Layout
- type Node
- type NodeCategory
- type NodeConfig
- type NodePosition
- type NotifyConfig
- func (c *NotifyConfig) CooldownOrDefault() time.Duration
- func (c *NotifyConfig) Inputs() int
- func (c *NotifyConfig) NotifyDeclaration() (cfg *NotifyConfig, ok bool)
- func (c *NotifyConfig) Outputs() int
- func (c *NotifyConfig) SeverityOrDefault() string
- func (c *NotifyConfig) SoundOrDefault() bool
- func (c *NotifyConfig) Validate(Refs) error
- type Refs
- type ResurfacePolicy
- type Seed
- type SidebarFolder
- type SidebarItem
- type SidebarLayout
- type SourceConfig
- func (c *SourceConfig) Connector() connector.Config
- func (c *SourceConfig) ConnectorType() string
- func (c *SourceConfig) Inputs() int
- func (c *SourceConfig) MarshalJSON() ([]byte, error)
- func (c *SourceConfig) MarshalYAML() (any, error)
- func (c *SourceConfig) Outputs() int
- func (c *SourceConfig) UnmarshalJSON(data []byte) error
- func (c *SourceConfig) UnmarshalYAML(value *yaml.Node) error
- func (c *SourceConfig) Validate(Refs) error
- type Wire
Constants ¶
const ( // DefaultFunctionTimeout is used when timeout is omitted. DefaultFunctionTimeout = 5 * time.Second )
const DefaultResurfacePolicy = ResurfacePolicyStateChanges
const NodeTypeFeed = "feed"
NodeTypeFeed is the feed terminal's type discriminator. It is named because a feed node is the only node type addressed from outside the graph: its id, qualified by the flow's, is the feed id inbox membership is keyed on.
const NotifyCooldownDefault = 5 * time.Minute
NotifyCooldownDefault is the per-item delivery floor a notify node uses when it declares no cooldown of its own: once a node interrupts about an item, it stays quiet about that same item for this long.
const NotifySeverityDefault = "info"
NotifySeverityDefault is the severity a notify node uses when it declares none: an ordinary, non-interrupting banner.
Variables ¶
var ( ErrFlowNotFound = errors.New("flow: flow not found") ErrNodeNotFound = errors.New("flow: node not found") ErrNodeNotImageMarkable = errors.New("flow: node does not carry an image mark") )
var CategoryOrder = []NodeCategory{CategorySources, CategoryProcess, CategoryDestinations}
CategoryOrder is the presentation order for the three categories: a flow reads source to destination, and so does its documentation.
var ErrInvalidResurfacePolicy = fmt.Errorf("not a valid ResurfacePolicy, try [%s]", strings.Join(_ResurfacePolicyNames, ", "))
var WorkedExampleYAML string
WorkedExampleYAML is the canonical sample flow: a sources.github feeding a github-filter, a two-output function node, and both terminal kinds (sources.github -> github-filter -> function -> {feed, action}).
It has exactly one home because it has two jobs that must never disagree. TestLoadFlow_WorkedExample loads these bytes and asserts they parse and validate, and cmd/desktop/internal/app/prompts embeds them in the flows authoring prompt as the concrete example an agent works from. A prompt example that no longer parses is a broken prompt, so the loader test is what keeps it honest.
Functions ¶
func MigrateDir ¶
MigrateDir migrates every flow definition file under dir forward to current, writing a backup under backupDir for each rewrite. Skips .ui.yaml/.sidebar.yaml siblings via isFlowDefinition (the same filter LoadFlows uses). Per-file errors are logged, not fatal — matching flow's per-file-isolation semantics.
func NodeTypes ¶
func NodeTypes() []string
NodeTypes returns every registered node type in sorted order. Sorting keeps prompt output byte-stable across runs — Go map iteration is randomized.
func ResurfacePolicyNames ¶
func ResurfacePolicyNames() []string
ResurfacePolicyNames returns a list of possible string values of ResurfacePolicy.
func SaveFlow ¶
SaveFlow writes f to path (flows/<id>.yaml). The frontend never serializes YAML itself: it sends a Flow value and this is the only place that turns it into a document, matching feed's one-way "Go owns the on-disk format" convention.
If path doesn't exist (or is empty), a clean document is marshaled with a short header comment. If it exists, its yaml.Node tree is edited in place: version/name/enabled are set as scalars on the existing root mapping, and the nodes/wires sequences are replaced wholesale. This preserves the document's leading header comment and any comments on unrelated top-level keys, but — since there's no way to tell, from a Flow value alone, which individual node or wire actually changed — comments attached to specific node/wire entries do not survive an edit. If per-entry comments become important, SaveFlow will need a richer edit model; round-trip structural fidelity (LoadFlow -> SaveFlow -> LoadFlow yields an equal Flow) does not depend on it.
func SaveSidebar ¶
func SaveSidebar(path string, layout SidebarLayout) error
SaveSidebar writes a flow's sidebar layout atomically. Like SaveUI it always marshals a clean document: sidebar-layout files are machine-written only and carry no hand-authored comments worth preserving.
Types ¶
type ActionConfig ¶
type ActionConfig struct {
Action string `json:"action" yaml:"action"`
}
ActionConfig is an action node: 1 input, 0 outputs (terminal). It enqueues an output_command against the referenced desktop actions.yml action id.
func (*ActionConfig) Inputs ¶
func (c *ActionConfig) Inputs() int
func (*ActionConfig) Outputs ¶
func (c *ActionConfig) Outputs() int
func (*ActionConfig) Validate ¶
func (c *ActionConfig) Validate(refs Refs) error
type Duration ¶
Duration decodes a Go duration string ("5s", "250ms") from YAML. Unlike a bare time.Duration, it rejects unquoted integer scalars — "timeout: 5" is a hard error, not "5 nanoseconds" — because a bare number in this schema is virtually always an author's mistaken assumption that it means seconds.
func (Duration) MarshalText ¶
MarshalText formats the duration as a Go duration string ("5s") — the same format UnmarshalYAML and UnmarshalText require on read. Implementing encoding.TextMarshaler (rather than yaml.Marshaler directly) covers both wire formats with one method: yaml.v3 falls back to TextMarshaler when a type isn't a yaml.Marshaler, and encoding/json does the same for encoding.TextMarshaler/TextUnmarshaler — so SaveFlow's YAML output and the Wails JSON bridge both get "5s", never a bare nanosecond count.
func (Duration) String ¶
String satisfies fmt.Stringer so Duration prints like a duration in error messages.
func (*Duration) UnmarshalText ¶
UnmarshalText parses a Go duration string, for JSON decode (the frontend graph editor's wire format via Wails). YAML decode goes through UnmarshalYAML instead, which additionally rejects bare integers — a distinction that matters for hand-authored YAML but not for a value the frontend already round-tripped through this same Duration type.
type FeedConfig ¶
type FeedConfig struct {
Icon string `json:"icon,omitempty" yaml:"icon,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
}
FeedConfig is a feed node: 1 input, 0 outputs (terminal). The node *is* the feed — its identity is the node id (flow-qualified as "<flowId>/<nodeId>" for membership claims). Icon and Description are purely cosmetic sidebar presentation: the icon shown in the tree and the tooltip surfaced on hover (handy context for LLM-generated feeds). Both are optional.
func (*FeedConfig) Inputs ¶
func (c *FeedConfig) Inputs() int
func (*FeedConfig) Outputs ¶
func (c *FeedConfig) Outputs() int
func (*FeedConfig) Validate ¶
func (c *FeedConfig) Validate(Refs) error
type Flow ¶
type Flow struct {
// ID is the filename stem (no extension), never a value read from the
// file itself.
ID string `json:"id"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
Resurface ResurfacePolicy `json:"resurface,omitempty"`
// Image is the content hash of the profile's avatar, whose normalized PNG
// lives in the app data dir (see cmd/desktop/internal/app/profileimg). It is owned by
// SetImage, not the graph editor: FlowStore.Save preserves whatever is on
// disk so a graph save never drops it. Empty means "no avatar" — the rail
// falls back to the letter chip.
Image string `json:"image,omitempty"`
Nodes []Node `json:"nodes"`
Wires []Wire `json:"wires"`
}
func LoadFlow ¶
LoadFlow parses and validates a single flows/*.yaml file. The flow's id is the filename stem (extension stripped), never a value read from the file. On success it returns the Flow plus any soft warnings; a hard validation or parse failure returns a non-nil error and a zero Flow.
func LoadFlows ¶
func LoadFlows(dir string, refs Refs) (flows []Flow, perFileErrors map[string]error, warnings map[string][]string)
LoadFlows loads every *.yaml/*.yml file directly inside dir, except a flow's sibling <id>.ui.yaml layout file (see SaveUI/LoadUI) — layouts live in the same directory but are never flow definitions. Each file is isolated: a broken flow is recorded in perFileErrors (keyed by filename) and skipped, while the remaining files still load. warnings is keyed by flow id and only holds entries for flows that produced soft warnings.
func ParseDocument ¶
ParseDocument parses and validates a flow document that is not on disk, under the id the caller gives it — a dry run against an unsaved edit, say. It is the same migrate-then-decode path LoadFlow uses, so a document that parses here is one that would load from a file, and one that does not fails with the message the file would have produced.
JSON is accepted too: the decoder is YAML, of which JSON is a subset, so an API caller can send the document as a JSON object rather than as text.
type FlowStatus ¶
FlowStatus is one flows/*.yaml file's load outcome, keyed by the flow id (the filename stem — see LoadFlow). Valid flows carry Flow and any soft Warnings; invalid files carry Err instead and a zero Flow, so a listing UI can show a broken flow file (and why) rather than have it silently vanish from the list.
type FlowStore ¶
type FlowStore struct {
// contains filtered or unexported fields
}
FlowStore holds the flows loaded from a flows/*.yaml directory, reloading on demand (Reload) or from a fsnotify FlowsWatcher. It is the backend half of Deploy: a flows-dir change reaches here via Reload, and the app emits "flows:updated" so the frontend knows to re-fetch and reconcile its running graph snapshots. This store only ever swaps its own in-memory snapshot; it does not touch a running graph.
Thread-safe: every method takes the same mutex.
func NewFlowStore ¶
NewFlowStore returns a store over dir (typically desktop.FlowsDir()), resolving action-node references through refs. Nothing is read from disk until the first List/Get/Save/Statuses call.
func (*FlowStore) Create ¶
Create writes a new flow (a new "profile") named name: it slugifies name to a flow id unique among existing flows, writes seed's graph plus its layout, reloads, and returns the loaded flow. A profile is a flow, so this is how the app's "New profile" affordance is backed.
An empty seed writes an empty graph, which is what a workspace created before any account is connected gets: a source node carries a credential ref, so there is no unconfigured source node to stand in for one.
func (*FlowStore) Delete ¶
Delete removes a flow (a "profile") and its sibling layout file, then reloads. A missing flow file is not an error — the end state (gone) is the same either way.
func (*FlowStore) Exists ¶
Exists reports whether a flow file backs id, parsing or not. It is a weaker question than Get's: a file that fails to validate has no Flow to return but is very much there, and deleting or renaming it is how that gets fixed.
func (*FlowStore) GetLayout ¶
GetLayout returns id's node layout — see LoadUI for missing/broken-file semantics (an empty Layout, never an error).
func (*FlowStore) GetSidebar ¶
func (s *FlowStore) GetSidebar(id string) SidebarLayout
GetSidebar returns id's sidebar layout (feed folders + ordering) — see LoadSidebar for missing/broken-file semantics (an empty layout, never an error).
func (*FlowStore) List ¶
List returns every successfully loaded flow in rail order — see SetOrder. Flows whose file failed to load are omitted — see Statuses for the full picture, including broken files.
func (*FlowStore) ParseDocument ¶
ParseDocument parses a flow document that is not on disk, resolving its references the same way a loaded file's are. Nothing is written and the store's own snapshot is untouched — this is how an unsaved edit is checked, or executed by a dry run, without being deployed first.
func (*FlowStore) Reload ¶
Reload re-reads every flow file in the directory. It only returns an error when the directory itself can't be created/read (rare — the flows dir is created on demand); a broken individual flow file is never a Reload error, it just shows up in Statuses/Errors.
func (*FlowStore) Rename ¶
Rename updates a flow's display name without changing its stable id or any graph content.
func (*FlowStore) Save ¶
Save re-validates f (the same checks LoadFlow runs, via the store's Refs) and, only if that passes, writes it with SaveFlow and reloads. An invalid flow is rejected before anything is written, so the file on disk — and the store's in-memory state — is untouched: the last-good flow keeps serving.
func (*FlowStore) SaveLayout ¶
SaveLayout persists id's node layout.
func (*FlowStore) SaveSidebar ¶
func (s *FlowStore) SaveSidebar(id string, layout SidebarLayout) error
SaveSidebar persists id's sidebar layout.
func (*FlowStore) SetEnabled ¶
SetEnabled updates whether a flow participates in polling and runtime execution without changing its graph or display name.
func (*FlowStore) SetImage ¶
SetImage sets a flow's avatar reference — the content hash of its stored image, or "" to clear it — without touching its graph or other fields. Like SetEnabled it reads the file directly rather than the possibly-stale cached snapshot, so a concurrent external graph edit is not reverted.
func (*FlowStore) SetOrder ¶
SetOrder sets the rail order: the ids given sort first, in the order given, and every other flow sorts alphabetically after them. An id naming no flow is ignored rather than rejected, so deleting a profile never invalidates the setting that named it, and an id the caller repeats keeps its first position.
Order is process state, not flow state: it comes from settings.yaml, which this package does not read, and a reload of the flows directory leaves it alone.
func (*FlowStore) SetSourceImage ¶
SetSourceImage sets (hash != "") or clears (hash == "") source node nodeID's feed-mark image in flow flowID, saves the flow, and returns the reloaded flow. It reads from disk rather than the cached snapshot, so a concurrent external edit is not reverted.
func (*FlowStore) SourceImageRef ¶
SourceImageRef returns source node nodeID's feed-mark image hash, or "" when it has none.
func (*FlowStore) Statuses ¶
func (s *FlowStore) Statuses() []FlowStatus
Statuses returns one FlowStatus per flow file in the directory — valid and invalid alike — in rail order, for a listing UI that must surface broken flows too, not just the ones that loaded. A broken file sorts by the same rule a working one does: ordering the rail by one comparator and its error entries by another would interleave them unpredictably.
type FlowsWatcher ¶
type FlowsWatcher struct {
// contains filtered or unexported fields
}
FlowsWatcher invokes onChange when a *.yaml/*.yml file in the flows directory changes on disk, so edits made outside the app — or the app's own writes via SaveFlow/SaveLayout — apply live. It watches the directory rather than individual files, since editors and atomic writers replace a file by rename (which silently drops a watch registered on the file itself), and so the watch works before the directory has any flow files in it yet.
func NewFlowsWatcher ¶
func NewFlowsWatcher(dir string, onChange func(), logger zerolog.Logger) (*FlowsWatcher, error)
func (*FlowsWatcher) Close ¶
func (w *FlowsWatcher) Close()
func (*FlowsWatcher) Start ¶
func (w *FlowsWatcher) Start()
Start runs the watch loop in a goroutine until Close.
type FunctionConfig ¶
type FunctionConfig struct {
OnMessage string `json:"on_message" yaml:"on_message"`
OutputsN int `json:"outputs,omitempty" yaml:"outputs,omitempty"`
Timeout Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
}
FunctionConfig is a function node: 1 input, N outputs (default 1). It carries the JavaScript the engine evaluates for each arriving message; this package only parses and validates the config.
on_message is the node's whole lifecycle. There are deliberately no start or stop hooks: the node does no I/O, so a stop hook could only mutate state that is about to be discarded, and a start hook is better written as lazy initialisation inside on_message (`state.counts ??= {}`). Having exactly one entry point also removes the pretence that `msg` may be undefined.
func (*FunctionConfig) EffectiveTimeout ¶
func (c *FunctionConfig) EffectiveTimeout() time.Duration
EffectiveTimeout returns the configured timeout, defaulting to DefaultFunctionTimeout when unset.
func (*FunctionConfig) Inputs ¶
func (c *FunctionConfig) Inputs() int
func (*FunctionConfig) Outputs ¶
func (c *FunctionConfig) Outputs() int
Outputs returns the configured output count, defaulting to 1 when unset.
func (*FunctionConfig) Validate ¶
func (c *FunctionConfig) Validate(Refs) error
type GithubFilterConfig ¶
type GithubFilterConfig struct {
Repos []string `json:"repos,omitempty" yaml:"repos,omitempty"`
ExcludeRepos []string `json:"exclude_repos,omitempty" yaml:"exclude_repos,omitempty"`
Authors []string `json:"authors,omitempty" yaml:"authors,omitempty"`
ExcludeAuthors []string `json:"exclude_authors,omitempty" yaml:"exclude_authors,omitempty"`
Labels []string `json:"labels,omitempty" yaml:"labels,omitempty"`
ExcludeLabels []string `json:"exclude_labels,omitempty" yaml:"exclude_labels,omitempty"`
Types []string `json:"types,omitempty" yaml:"types,omitempty"`
Reasons []string `json:"reasons,omitempty" yaml:"reasons,omitempty"`
CI []string `json:"ci,omitempty" yaml:"ci,omitempty"`
Review []string `json:"review,omitempty" yaml:"review,omitempty"`
}
GithubFilterConfig is a github-filter node: 1 input, 2 outputs (port 0 = pass, port 1 = fail). Its fields mirror the profiles feed FilterDef: groups AND together, values within a group OR, and exclude groups win over includes.
func (*GithubFilterConfig) Inputs ¶
func (c *GithubFilterConfig) Inputs() int
func (*GithubFilterConfig) Outputs ¶
func (c *GithubFilterConfig) Outputs() int
func (*GithubFilterConfig) Validate ¶
func (c *GithubFilterConfig) Validate(Refs) error
type HeadlessActionRefs ¶
HeadlessActionRefs is optionally implemented by action resolvers. Older resolvers still validate existence; desktop's resolver additionally blocks interactive launch actions from background flow nodes.
type LaunchChatConfig ¶
type LaunchChatConfig struct {
// Workspace is a directory name under the workspace root.
Workspace string `json:"workspace" yaml:"workspace"`
Prompt string `json:"prompt" yaml:"prompt"`
}
LaunchChatConfig is a terminal node that opens an agent workspace chat per item, with the rendered prompt as the first message.
func (*LaunchChatConfig) Inputs ¶
func (c *LaunchChatConfig) Inputs() int
func (*LaunchChatConfig) Outputs ¶
func (c *LaunchChatConfig) Outputs() int
func (*LaunchChatConfig) Validate ¶
func (c *LaunchChatConfig) Validate(Refs) error
type LaunchSessionConfig ¶
type LaunchSessionConfig struct {
Repo string `json:"repo" yaml:"repo"`
Agent string `json:"agent,omitempty" yaml:"agent,omitempty"`
// Not `name`: that key is the node's own label.
SessionName string `json:"sessionName,omitempty" yaml:"sessionName,omitempty"`
Prompt string `json:"prompt" yaml:"prompt"`
}
LaunchSessionConfig is a terminal node that starts a hive session per item, rendering the same templates as an actions.yml launch-session action.
func (*LaunchSessionConfig) Inputs ¶
func (c *LaunchSessionConfig) Inputs() int
func (*LaunchSessionConfig) Outputs ¶
func (c *LaunchSessionConfig) Outputs() int
func (*LaunchSessionConfig) Validate ¶
func (c *LaunchSessionConfig) Validate(Refs) error
type Layout ¶
type Layout struct {
Nodes map[string]NodePosition `json:"nodes" yaml:"nodes"`
}
Layout is the machine-written, non-authoritative sibling of a flow — and the on-disk shape of a <id>.ui.yaml file: node canvas positions only, keyed by node id. It is never consulted by LoadFlow/validation and never blocks a flow from loading — a missing or broken .ui.yaml just means the editor lays out nodes fresh (see LoadUI).
type Node ¶
type Node struct {
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name,omitempty"`
Disabled bool `json:"disabled,omitempty"`
Config NodeConfig `json:"-"`
}
Node is one node in a flow's graph: the common envelope fields (id, type, name, disabled) plus a per-type Config decoded via the registry. The json tags document the wire shape produced by MarshalJSON/UnmarshalJSON (see node_json.go) — Node's own (Un)MarshalJSON/(Un)MarshalYAML methods bypass normal struct-tag reflection, so these tags are documentation, not decode wiring.
func (Node) MarshalJSON ¶
MarshalJSON flattens the envelope fields and the per-type config's own fields into one JSON object — the wire shape the desktop frontend's graph editor consumes over Wails (FlowsService.GetFlow/SaveFlow) — mirroring the flattened *.yaml on-disk shape produced by MarshalYAML.
func (Node) MarshalYAML ¶
MarshalYAML implements yaml.Marshaler, inverting UnmarshalYAML's two-pass decode: the envelope fields (id, type, name, disabled) and the per-type config's own fields are flattened into one mapping, matching the on-disk shape UnmarshalYAML reads back (see the worked example in loader_test.go). Returning a *yaml.Node here (rather than a plain value) is a documented yaml.v3 pattern for building the node by hand: the encoder uses it directly instead of re-marshaling it through reflection.
func (*Node) UnmarshalJSON ¶
UnmarshalJSON implements the same two-pass strict decode as UnmarshalYAML: (1) decode the envelope to read the `type` discriminator, (2) look up the type in the registry (unknown type is a hard error), (3) strict-decode the remaining fields — with the reserved envelope keys stripped out first — into a fresh per-type config, so an unrecognized per-type field is also a hard error (matching UnmarshalYAML's KnownFields(true) behavior).
func (*Node) UnmarshalYAML ¶
UnmarshalYAML implements the two-pass strict decode: (1) decode a lax Header to read the `type:` discriminator, (2) look up the type in the registry (unknown type is a hard error), (3) strict-decode the node's remaining fields — with the reserved envelope keys stripped out first — into a fresh per-type config so an unknown per-type field is also a hard error.
type NodeCategory ¶
type NodeCategory string
NodeCategory groups node types the way the palette and the flows prompt present them. It is derived from a type's port counts rather than declared, so it cannot disagree with what the graph validator enforces.
const ( // CategorySources covers node types with no inputs — where a flow starts. CategorySources NodeCategory = "Sources" // CategoryProcess covers node types with both inputs and outputs. CategoryProcess NodeCategory = "Process" // CategoryDestinations covers terminal node types with no outputs. CategoryDestinations NodeCategory = "Destinations" )
func CategoryOf ¶
func CategoryOf(nodeType string) (NodeCategory, error)
CategoryOf classifies a registered node type from the port counts its config reports: no inputs makes it a source, no outputs a destination, anything else a processor.
type NodeConfig ¶
NodeConfig is the per-type union every registered node type implements. Inputs/Outputs report the node's port counts (used by wire validation); Validate checks the node's own fields, including any cross-file references, against the supplied Refs resolver.
type NodePosition ¶
NodePosition is one node's canvas coordinates in a flow's sibling <id>.ui.yaml layout file.
type NotifyConfig ¶
type NotifyConfig struct {
// Title renders the notification's headline. Required — a native
// notification without a title is rejected by the OS layer.
Title string `json:"title" yaml:"title"`
// Body renders the notification's message. Optional.
Body string `json:"body,omitempty" yaml:"body,omitempty"`
// Severity selects the native interruption level (see notifySeverities).
// Empty means NotifySeverityDefault.
Severity string `json:"severity,omitempty" yaml:"severity,omitempty"`
// Sound is a pointer so an absent key ("use the default", which is sound
// on) is distinguishable from an explicit `sound: false`. It can only
// silence a notification — the global notification-sound setting still
// wins when it is off. Resolve through SoundOrDefault.
Sound *bool `json:"sound,omitempty" yaml:"sound,omitempty"`
// CooldownSeconds is a per-item delivery floor: once this node interrupts
// about an item, it stays quiet about that same item for this long. A
// pointer so an absent key ("use NotifyCooldownDefault") is distinct from
// an explicit 0 ("no cooldown; every accepted message may interrupt").
// This is a delivery floor, not dedup — deciding *whether* an item is
// worth notifying is upstream's job. Resolve through CooldownOrDefault.
CooldownSeconds *int `json:"cooldownSeconds,omitempty" yaml:"cooldownSeconds,omitempty"`
}
NotifyConfig is a notify node: 1 input, 0 outputs (terminal). Every arriving message enqueues an output_command that the notify executor delivers as a native OS notification, with Title/Body rendered as Go text/templates over the message (the same templating story as an action's prompt_template — see cmd/desktop/internal/app/actions/launch_session.go).
The node cannot override the app's notification settings: the executor checks the global kill switch before every delivery, so "notifications off" always wins over any flow.
func (*NotifyConfig) CooldownOrDefault ¶
func (c *NotifyConfig) CooldownOrDefault() time.Duration
CooldownOrDefault resolves CooldownSeconds, defaulting to NotifyCooldownDefault when the key is absent. An explicit 0 disables the cooldown entirely.
func (*NotifyConfig) Inputs ¶
func (c *NotifyConfig) Inputs() int
func (*NotifyConfig) NotifyDeclaration ¶
func (c *NotifyConfig) NotifyDeclaration() (cfg *NotifyConfig, ok bool)
NotifyDeclaration satisfies dispatch's notify-raiser capability: a notify node always has content to raise — it notifies for whatever is routed to it, which is the author's explicit choice in authoring the node at all.
func (*NotifyConfig) Outputs ¶
func (c *NotifyConfig) Outputs() int
func (*NotifyConfig) SeverityOrDefault ¶
func (c *NotifyConfig) SeverityOrDefault() string
SeverityOrDefault resolves Severity, defaulting to NotifySeverityDefault when the node declares none.
func (*NotifyConfig) SoundOrDefault ¶
func (c *NotifyConfig) SoundOrDefault() bool
SoundOrDefault resolves Sound, defaulting to true (play the notification sound) when the key is absent.
func (*NotifyConfig) Validate ¶
func (c *NotifyConfig) Validate(Refs) error
type Refs ¶
type Refs interface {
// ResolveAction reports whether id names a known action.
ResolveAction(id string) bool
}
Refs resolves the one remaining cross-file reference a flow's nodes point at: the action node's desktop actions.yml action id. (Source and feed nodes are now self-contained — a source embeds its fetch config, a feed's identity is its node id — so they no longer resolve anything.) It is injected rather than imported so this package never depends on the actions loader.
type ResurfacePolicy ¶
type ResurfacePolicy string
Flow is one parsed and validated flows/*.yaml document. Besides being the decode target for LoadFlow, it is the wire shape GetFlow/SaveFlow expose to the desktop frontend's graph editor over Wails — hence the json tags alongside the (unused-by-Flow-itself, since flowFile is the YAML decode target) documentation of the on-disk names. ENUM(all, state-changes, never)
const ( // ResurfacePolicyAll is a ResurfacePolicy of type all. ResurfacePolicyAll ResurfacePolicy = "all" // ResurfacePolicyStateChanges is a ResurfacePolicy of type state-changes. ResurfacePolicyStateChanges ResurfacePolicy = "state-changes" // ResurfacePolicyNever is a ResurfacePolicy of type never. ResurfacePolicyNever ResurfacePolicy = "never" )
func ParseResurfacePolicy ¶
func ParseResurfacePolicy(name string) (ResurfacePolicy, error)
ParseResurfacePolicy attempts to convert a string to a ResurfacePolicy.
func (*ResurfacePolicy) AppendText ¶
func (x *ResurfacePolicy) AppendText(b []byte) ([]byte, error)
AppendText appends the textual representation of itself to the end of b (allocating a larger slice if necessary) and returns the updated slice.
Implementations must not retain b, nor mutate any bytes within b[:len(b)].
func (ResurfacePolicy) IsValid ¶
func (x ResurfacePolicy) IsValid() bool
IsValid provides a quick way to determine if the typed value is part of the allowed enumerated values
func (ResurfacePolicy) MarshalText ¶
func (x ResurfacePolicy) MarshalText() ([]byte, error)
MarshalText implements the text marshaller method.
func (ResurfacePolicy) String ¶
func (x ResurfacePolicy) String() string
String implements the Stringer interface.
func (*ResurfacePolicy) UnmarshalText ¶
func (x *ResurfacePolicy) UnmarshalText(text []byte) error
UnmarshalText implements the text unmarshaller method.
type Seed ¶
Seed is the graph a newly created flow starts with, plus the canvas positions for it. Create takes one from its caller rather than building it: a starter graph names source connectors, and this package is connector-neutral by design.
The zero Seed is an empty graph, which is a valid flow — a workspace is the thing that exists before any credential does.
type SidebarFolder ¶
type SidebarFolder struct {
ID string `json:"id" yaml:"id"`
Name string `json:"name" yaml:"name"`
Feeds []string `json:"feeds" yaml:"feeds"`
}
SidebarFolder is one named group of feeds in a profile's sidebar. Feeds lists the member feed node ids in display order. Note there is no collapsed field: whether a folder is expanded is transient view state the frontend keeps in localStorage, not configuration — this file records structure and order only.
type SidebarItem ¶
type SidebarItem struct {
Feed string `json:"feed,omitempty" yaml:"feed,omitempty"`
Folder *SidebarFolder `json:"folder,omitempty" yaml:"folder,omitempty"`
}
SidebarItem is one entry in the sidebar's ordered top level: either a bare feed (Feed set to a feed node id) or a folder (Folder set). Exactly one is meant to be non-empty; a malformed item with neither is ignored by the frontend when it reconciles the layout against the flow's actual feed nodes.
type SidebarLayout ¶
type SidebarLayout struct {
Items []SidebarItem `json:"items" yaml:"items"`
}
SidebarLayout is the machine-written, non-authoritative sibling of a flow — the on-disk shape of a <id>.sidebar.yaml file. It records how a profile's feed nodes are grouped into folders and ordered in the sidebar's FEEDS section. Like Layout (.ui.yaml) it is purely cosmetic and keyed by node id: a missing or broken file just means the sidebar falls back to listing feeds in flow-node order, feed nodes absent from the layout are appended (so a newly added feed is never hidden), and entries for feeds that no longer exist are dropped. It is never consulted by LoadFlow/validation and never blocks a flow from loading.
func LoadSidebar ¶
func LoadSidebar(path string) SidebarLayout
LoadSidebar reads a flow's sibling sidebar-layout file. A missing file, or one that fails to parse, is not an error — the layout is purely cosmetic — it returns an empty SidebarLayout instead so the caller always has an (empty) slice to range over.
type SourceConfig ¶
type SourceConfig struct {
// contains filtered or unexported fields
}
SourceConfig is the node-graph half of a source connector: it adapts a connector.Config — which knows only its own fields and how to validate them — to the NodeConfig every node type implements.
The port counts live here rather than on the connector because they are graph vocabulary: a source is 0-in/1-out by definition, and a connector that had to declare it could get it wrong. Validate forwards, because a source node's config is self-contained; Refs resolves the action node's cross-file reference and a source has none.
func NewSourceConfig ¶
func NewSourceConfig(connectorType string, cfg connector.Config) *SourceConfig
NewSourceConfig wraps a connector's parsed config as a node config.
func (*SourceConfig) Connector ¶
func (c *SourceConfig) Connector() connector.Config
Connector returns the parsed connector config, which is what a factory needs to construct a running instance.
func (*SourceConfig) ConnectorType ¶
func (c *SourceConfig) ConnectorType() string
ConnectorType is the registered connector this node configures — the same string as the node's `type:`.
func (*SourceConfig) Inputs ¶
func (c *SourceConfig) Inputs() int
func (*SourceConfig) MarshalJSON ¶
func (c *SourceConfig) MarshalJSON() ([]byte, error)
func (*SourceConfig) MarshalYAML ¶
func (c *SourceConfig) MarshalYAML() (any, error)
MarshalYAML and MarshalJSON emit the connector's own fields, so a source node round-trips to exactly the flattened shape every other node type does — the wrapper is invisible on disk and on the wire.
func (*SourceConfig) Outputs ¶
func (c *SourceConfig) Outputs() int
func (*SourceConfig) UnmarshalJSON ¶
func (c *SourceConfig) UnmarshalJSON(data []byte) error
UnmarshalJSON mirrors UnmarshalYAML: json.Unmarshal on a type with a custom unmarshaler drops the outer decoder's DisallowUnknownFields, so it is re-applied here.
func (*SourceConfig) UnmarshalYAML ¶
func (c *SourceConfig) UnmarshalYAML(value *yaml.Node) error
UnmarshalYAML re-creates the strict decode rather than delegating to the caller's decoder. A custom unmarshaler bypasses the outer decoder's KnownFields(true), so without this an unknown key in a source node's config would decode to nothing instead of failing the load — the silent-failure mode every other node type is protected from.
func (*SourceConfig) Validate ¶
func (c *SourceConfig) Validate(Refs) error