The operator would not read the Telegram ask for ADR 0343: thirty plain lines, every clause on one, no bold, no summary. A settings proposal was the same wall of text, its values masked so the approval was blind. - messenger: an ask's message is the asker's headline and explanation alone; who asked and when, what each answer does, the expiry, where else it is answered and the asker's Details travel as the message's note (asks.Message.Note). Answered, the message is one line, "You chose Approve at 16:06 on Telegram · <headline>". An explanation that does not read at a glance (asks.Glance) is shown and logged. On a kind that proves who answers, an ask's values are shown whole (asks.Ask.Whole), held to the secret rule alone (from the earlier work on issue 383, #178). - telegram: every message in HTML, never reworded: the title bold, a key line monospace, the note apart in a collapsed blockquote; markup escaped. A tap is compared by its words, whitespace aside, and a message shown before is still compared by the digest it was kept under. - desk-channel: the note follows the body. - mesh-knowledge: a record that needs the operator carries a brief (headline, summary, at most three points), the 30th per-record rule; its ask shows the brief and never the clauses. An ask made before is composed again as it was asked (form), so its warrant still holds. - claude-code: an instructions proposal's explanation is one summary line (the proposer's, or the module's), its text in the Details; a question's context is at most 300 characters and its recommendation one point. Older proposals and questions keep their form. The SDK is pinned at mesh-sdk 9588871 (fix/639-an-ask-reads-at-a-glance), which adds Message.Note and asks.Glance.
245 lines
7.6 KiB
Go
245 lines
7.6 KiB
Go
package main
|
|
|
|
// What a message says (novox/hq ADR 0253): **it reads in one glance.** A title in plain words, one or two
|
|
// plain sentences on what happened, what it means and whether to act, and when it began in the
|
|
// operator's own time. No key, id, commit, verb or markup: the condition's key and summary stay in the
|
|
// controller (`conditions`) and in this holder's history, for whoever looks closer.
|
|
//
|
|
// - The words are the condition's own: its headline, explanation and resolved line, made by the
|
|
// controller where the condition is made, so every channel says the same thing. A condition from a
|
|
// controller older than them, or a module's notice without a headline, is said from its summary,
|
|
// with what the operator cannot read taken out (plainSummary).
|
|
// - A clearance is one short line: "Resolved: openrazer works again on g14, after 26 min".
|
|
// - A reminder, an escalation and a reopening say so in the title, before the same headline.
|
|
// - Times are the operator's (the setting time-zone; this machine's zone when not given), and said as a
|
|
// clock time today, a weekday this week, a date before that.
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func (h *Holder) zone() *time.Location {
|
|
if h.Zone != nil {
|
|
if loc := h.Zone(); loc != nil {
|
|
return loc
|
|
}
|
|
}
|
|
return time.Local
|
|
}
|
|
|
|
// compose is the message for a record. withhold > 0 when the content rule refused what it said: 1 says
|
|
// the headline alone, more says only that something was withheld.
|
|
func compose(r *Record, what string, now time.Time, loc *time.Location, withhold int) Message {
|
|
headline, explanation := wordsOf(r)
|
|
urgent := r.Severity == Urgent
|
|
if withhold >= 2 {
|
|
return withheld(r.Refused, urgent)
|
|
}
|
|
if withhold == 1 {
|
|
explanation = "Part of this message was withheld, because it carried " + r.Refused + "."
|
|
}
|
|
var title string
|
|
switch what {
|
|
case "cleared":
|
|
return Message{Title: resolvedLine(r) + ", after " + roughly(r.Cleared.Sub(r.Raised)), Quiet: true}
|
|
case "reminder":
|
|
title = "Still open: " + headline
|
|
case "quiet-reminder":
|
|
title = "Still open after a day: " + headline
|
|
case "escalated":
|
|
title = "Now urgent: " + headline
|
|
case "silenced":
|
|
// Said in place of the open message: no urgency, so the desk's popup times out, and no answers.
|
|
body := "Nothing more is said of it until then."
|
|
if r.SilencedWhy != "" {
|
|
body = "Because: " + strings.TrimSuffix(strings.TrimSpace(r.SilencedWhy), ".") + ". " + body
|
|
}
|
|
return Message{Title: "Silenced until " + by(r.SilencedTill, now, loc) + ": " + headline, Body: body, Quiet: true}
|
|
case "reopened":
|
|
title = headline + " (again" + times(r.Count) + ")"
|
|
if urgent {
|
|
title = "Urgent: " + title
|
|
}
|
|
default:
|
|
title = headline
|
|
if urgent {
|
|
title = "Urgent: " + headline
|
|
}
|
|
}
|
|
since := "Since " + when(r.Raised, now, loc)
|
|
if what == "reminder" || what == "quiet-reminder" {
|
|
since = fmt.Sprintf("Open since %s, for %s.", when(r.Raised, now, loc), roughly(now.Sub(r.Raised)))
|
|
} else {
|
|
since += "."
|
|
}
|
|
body := since
|
|
if r.Origin == "ask" && what != "reminder" && what != "quiet-reminder" {
|
|
// An ask's own message (issue 639): its body is the asker's summary and points; when it was asked is in
|
|
// its note, with who asked.
|
|
body = ""
|
|
}
|
|
if explanation != "" && body == "" {
|
|
body = explanation
|
|
} else if explanation != "" {
|
|
body = explanation + "\n" + since
|
|
}
|
|
return Message{Title: title, Body: body, Urgent: urgent}
|
|
}
|
|
|
|
// withheld is what is sent in place of a message the content rule refused.
|
|
func withheld(carried string, urgent bool) Message {
|
|
return Message{Title: "A mesh message was withheld",
|
|
Body: "It carried " + carried + ", which may not leave the mesh. An agent can read it among the mesh's " +
|
|
"open conditions.", Urgent: urgent}
|
|
}
|
|
|
|
// times is " 2nd time" for a count of 2, "" for 1.
|
|
func times(n int) string {
|
|
if n < 2 {
|
|
return ""
|
|
}
|
|
suffix := "th"
|
|
switch {
|
|
case n%100 >= 11 && n%100 <= 13:
|
|
case n%10 == 1:
|
|
suffix = "st"
|
|
case n%10 == 2:
|
|
suffix = "nd"
|
|
case n%10 == 3:
|
|
suffix = "rd"
|
|
}
|
|
return fmt.Sprintf(", %d%s time", n, suffix)
|
|
}
|
|
|
|
// wordsOf is a record's headline and explanation: its own, or made from its summary.
|
|
func wordsOf(r *Record) (string, string) {
|
|
if r.Headline != "" {
|
|
return r.Headline, r.Explanation
|
|
}
|
|
summary := plainSummary(r.Summary)
|
|
if r.Origin == "notify" && len(summary) <= 80 {
|
|
return capital(summary), ""
|
|
}
|
|
return fallbackHeadline(r), capital(summary)
|
|
}
|
|
|
|
// resolvedLine is the line a clearance says: the condition's own, or made from its headline.
|
|
func resolvedLine(r *Record) string {
|
|
line := r.Resolved
|
|
if line == "" {
|
|
headline, _ := wordsOf(r)
|
|
line = "Resolved: " + lowerFirst(headline)
|
|
}
|
|
if !strings.HasPrefix(line, "Resolved") {
|
|
line = "Resolved: " + lowerFirst(line)
|
|
}
|
|
return line
|
|
}
|
|
|
|
// fallbackHeadline is a title for something with no headline: what it is about and its kind, in words.
|
|
func fallbackHeadline(r *Record) string {
|
|
what := strings.ReplaceAll(r.Kind, "-", " ")
|
|
if what == "" || what == "notice" {
|
|
what = "a notice"
|
|
}
|
|
about := ""
|
|
if r.Origin == "notify" {
|
|
about = r.Subject
|
|
} else if machine := machineOf(r.Subject); machine != "" {
|
|
about = machine
|
|
} else {
|
|
// A seat, a provider or a module is named by a name; a plan, a delivery or a call by an id, never said.
|
|
for _, scope := range []string{"seat ", "provider ", "module "} {
|
|
if name, ok := strings.CutPrefix(r.Subject, scope); ok {
|
|
about = name
|
|
}
|
|
}
|
|
}
|
|
if about == "" {
|
|
return capital(what)
|
|
}
|
|
return capital(what) + " on " + about
|
|
}
|
|
|
|
// machineOf is the machine a record's subject words name: "machine ace" or "x on ace".
|
|
func machineOf(subject string) string {
|
|
if _, on, ok := strings.Cut(subject, " on "); ok {
|
|
return on
|
|
}
|
|
if m, ok := strings.CutPrefix(subject, "machine "); ok {
|
|
return m
|
|
}
|
|
return ""
|
|
}
|
|
|
|
var (
|
|
codeSpan = regexp.MustCompile("`([^`]*)`")
|
|
noise = regexp.MustCompile(`\s+`)
|
|
)
|
|
|
|
// plainSummary is a summary with what the operator cannot read taken out: markup and the verbs in code
|
|
// spans. Used only where the condition says no plain words of its own.
|
|
func plainSummary(s string) string {
|
|
s = codeSpan.ReplaceAllStringFunc(s, func(m string) string {
|
|
inner := strings.Trim(m, "`")
|
|
if strings.ContainsAny(inner, ". ") {
|
|
return "a command"
|
|
}
|
|
return inner
|
|
})
|
|
s = strings.NewReplacer("**", "", "`", "").Replace(s)
|
|
return strings.TrimSpace(noise.ReplaceAllString(s, " "))
|
|
}
|
|
|
|
// when is a moment as the operator reads it, in their zone: "10:40" today, "Tue 10:40" this week,
|
|
// "1 Oct 10:40" before.
|
|
func when(t, now time.Time, loc *time.Location) string {
|
|
if t.IsZero() {
|
|
return "now"
|
|
}
|
|
lt, ln := t.In(loc), now.In(loc)
|
|
y1, m1, d1 := lt.Date()
|
|
y2, m2, d2 := ln.Date()
|
|
switch {
|
|
case y1 == y2 && m1 == m2 && d1 == d2:
|
|
return lt.Format("15:04")
|
|
case ln.Sub(lt) < 6*24*time.Hour:
|
|
return lt.Format("Mon 15:04")
|
|
}
|
|
return lt.Format("2 Jan 15:04")
|
|
}
|
|
|
|
// by is a moment ahead as the operator reads it, in their zone: "23:33" today, "tomorrow 23:33", "Fri 16 Oct
|
|
// 23:33" later. An ask's expiry was said as a clock time alone, so an ask lasting a week read as one ending
|
|
// tonight (issue 373).
|
|
func by(t, now time.Time, loc *time.Location) string {
|
|
lt, ln := t.In(loc), now.In(loc)
|
|
y1, m1, d1 := lt.Date()
|
|
y2, m2, d2 := ln.Date()
|
|
if y1 == y2 && m1 == m2 && d1 == d2 {
|
|
return lt.Format("15:04")
|
|
}
|
|
ty, tm, td := ln.AddDate(0, 0, 1).Date()
|
|
if y1 == ty && m1 == tm && d1 == td {
|
|
return "tomorrow " + lt.Format("15:04")
|
|
}
|
|
return lt.Format("Mon 2 Jan 15:04")
|
|
}
|
|
|
|
func capital(s string) string {
|
|
if s == "" {
|
|
return s
|
|
}
|
|
return strings.ToUpper(s[:1]) + s[1:]
|
|
}
|
|
|
|
func lowerFirst(s string) string {
|
|
if s == "" || (len(s) > 1 && strings.ToUpper(s[:2]) == s[:2]) {
|
|
return s
|
|
}
|
|
return strings.ToLower(s[:1]) + s[1:]
|
|
}
|