Skip to content

Commit f706bf7

Browse files
amtiYoclaude
andcommitted
Put everything that shows or configures behind /start
Voicy published five commands in a direct chat, and four of them — /stats, /language, /help, /about — opened a tab that was already a button on the /start panel. Listing the same door twice is the one place Voicy broke the rule the rest of the family keeps: a command earns its own slot only when it acts on a message somebody points at, which is what /v and /vp do. The four handlers still answer for anyone who types them from memory; they are simply no longer advertised, and the group menu is untouched. The panel itself had one shape stretched over five screens. Because "title, subtitle, quoted body" required a subtitle, subtitles were invented for screens with nothing to put there — About was captioned "Voicy" under the title "About". Form now follows content: the home card keeps the full frame, help is a list and is no longer boxed in a quote, the language screen is a heading over its picker, settings is a heading over the one note the toggles cannot show, and About is the family credits card — product and running build, one line of what it is, then label · value rows for the engine, the repository under Apache-2.0, and the admin. The repository is a link inside the text and deliberately not a button, because two ways to open one address are not two actions. The version is the same string /healthz reports, so a screenshot identifies the binary. Close is now a group affordance. In a direct chat the conversation is the panel and deleting the message the reader is looking at leaves them with their own history; in a group the panel is Voicy's message in somebody else's feed and clearing it is the polite exit. For the same reason the group screen no longer offers settings or the interface language: both are personal and shared with the sibling bots through Core, so a group visitor was being shown switches that do not mean in a group what they appear to mean. It now shows the About card and a line explaining /v and /vp. Button styles mark one thing per screen instead of four scattered accents: the language tab on the direct-chat home, since nothing else there is readable until the language is right; the language in use, in a grid of sixteen; the open statistics tab; and Close, which is destructive. The settings grid stays unstyled — colouring seven switches highlights none of them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FxNv1rk7jMQtb9tMBREc5B
1 parent 072358a commit f706bf7

9 files changed

Lines changed: 478 additions & 258 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,43 @@ See [`docs/versioning.md`](docs/versioning.md) for what the numbers mean and
1313

1414
### Changed
1515

16+
- **The direct-chat command menu is one entry: `/start`.** `/stats`,
17+
`/language`, `/help` and `/about` were published as commands while already
18+
being buttons on the `/start` panel, so every one of them listed the same door
19+
twice. Everything that shows or configures now lives behind `/start`; a
20+
command is published only when it acts on a message somebody points at, which
21+
is what `/v` and `/vp` do and why the group menu is unchanged. All four still
22+
answer for anyone who types them from memory — they are simply no longer
23+
advertised.
24+
- **The panel is two screens instead of one.** In a direct chat it keeps every
25+
tab and no longer offers `Close`: there the whole conversation is the panel,
26+
and closing it deleted the message the reader was looking at. In a group it
27+
offers `Close`, because there the panel is Voicy's message sitting in somebody
28+
else's feed, and it now shows only the about card and a line explaining `/v`
29+
and `/vp`. Settings and the interface language are personal and shared with the
30+
sibling bots through Core, so a group visitor is no longer offered switches
31+
that do not mean in a group what they appear to mean.
32+
- **Each panel now carries the shape its content asks for.** One "title,
33+
subtitle, quoted body" frame was stretched over five screens of different
34+
kinds, and because the frame required a subtitle, subtitles were invented for
35+
screens that had nothing to put there — "About" was captioned "Voicy". Help is
36+
a list and is no longer boxed in a quote; the language screen is a heading and
37+
one sentence over the picker; settings is a heading and the note the toggles
38+
cannot show; the home card keeps the full frame, which is the one place it was
39+
always right.
40+
- **`About` is the family-standard card.** It leads with the product and the
41+
build it is actually running — the same version string `/healthz` reports — one
42+
line of what Voicy does, then `label · value` rows: the recognition engine, the
43+
repository as a link in the text under Apache-2.0, and the admin to write to.
44+
The repository is deliberately a link and not a button: two ways to open one
45+
address are not two actions. Localized in all sixteen languages.
46+
- **Button styles mark one thing per screen** rather than four scattered
47+
accents. The language tab is highlighted on the direct-chat home, since nothing
48+
else there is readable until the language is right; the language currently in
49+
use is marked in the grid of sixteen; the open statistics tab is marked; and
50+
`Close` is destructive. The seven-switch settings grid stays unstyled — its
51+
state is already on the glyphs, and colouring all of it would highlight none of
52+
it.
1653
- One versioning and release document for the whole family. `docs/versioning.md`
1754
and `docs/releases.md` are now byte-identical across every Asterfield
1855
repository apart from two clearly marked sections: this repository's own
@@ -27,6 +64,14 @@ See [`docs/versioning.md`](docs/versioning.md) for what the numbers mean and
2764
Earlier pre-releases in this repository were tagged on `main` under the
2865
previous rule; they are left as they are.
2966

67+
### Removed
68+
69+
- Translation keys that nothing renders any more: `cmd.stats`, `cmd.language`,
70+
`cmd.help` and `cmd.about` with the commands they described, `about.title`,
71+
`about.hint` and `about.body` with the old three-part About panel, and
72+
`help.hint`, `lang.hint` and `settings.hint` — the subtitles that only restated
73+
their own title or their own body.
74+
3075
## v0.0.1-beta.4 - 2026-09-08
3176

3277
### Fixed

‎cmd/voicy/main.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ func run(log *slog.Logger) error {
9696
stt := deepgram.New(cfg.DeepgramAPIKey)
9797
store.SetStatsTimezone(cfg.StatsTimezone)
9898
b := bot.New(store, client, stt, log)
99+
b.SetVersion(version)
99100
b.SetMediaLimits(cfg.MaxMediaBytes, cfg.MaxMediaDuration)
100101
b.SetWorkers(cfg.MaxConcurrentJobs)
101102
b.SetStatsTTL(cfg.StatsCacheTTL)

‎docs/telegram.md‎

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,44 @@
22

33
## Commands
44

5-
| Command | Surface | Behavior |
6-
|:--|:--|:--|
7-
| `/start` | DM | Owner-scoped family panel |
8-
| `/start` | group | Ephemeral panel |
9-
| `/v` | reply | Public transcript of any audio or video, in the same topic |
10-
| `/vp` | ephemeral reply in group | Requester-only transcript |
11-
| `/language [ru\|en]` | DM | Open or change shared interface language |
12-
| `/stats` | DM | Personal stats with a global tab |
13-
| `/help`, `/about` | DM | Help and project details |
5+
| Command | Surface | In the menu | Behavior |
6+
|:--|:--|:--|:--|
7+
| `/start` | DM | yes | Owner-scoped family panel |
8+
| `/start` | group | yes, ephemeral | Ephemeral group panel |
9+
| `/v` | reply | yes | Public transcript of any audio or video, in the same topic |
10+
| `/vp` | ephemeral reply in group | yes, ephemeral | Requester-only transcript |
11+
| `/language [ru\|en]` | DM | no | Opens the language tab, or sets the language directly |
12+
| `/stats` | DM | no | Opens the statistics tab |
13+
| `/help` | DM | no | Opens the help tab |
14+
| `/about` | DM | no | Opens the about card |
15+
16+
Everything that shows or configures lives behind `/start`. A separate command is
17+
published only when it acts on a message somebody points at, which is why `/v`
18+
and `/vp` are in the group menu and nothing else is in the direct-chat one:
19+
statistics, language, help and about are tabs of the panel, and publishing a
20+
command that only opens a tab lists the same door twice. The four still answer
21+
for anyone who types them from memory.
1422

1523
Bare group media and non-ephemeral group `/vp` stay silent. Unknown group
1624
commands stay silent. Menu callbacks include the owner ID and foreign taps only
1725
receive a short callback toast.
1826

27+
## Panels
28+
29+
The panel is two screens. In a direct chat it carries every tab — language,
30+
statistics, settings, help, about — and offers no Close, because there the whole
31+
conversation is the panel and deleting the message the reader is looking at
32+
leaves them with their own history. In a group it carries the about card and
33+
Close, and its text explains `/v` and `/vp`: settings and the interface language
34+
are personal and shared with the sibling bots through Core, so offering them from
35+
somebody else's group would promise a local effect Voicy does not have.
36+
37+
Button styles (Bot API 9.4+) mark one thing per screen: the language tab on the
38+
direct-chat home, since nothing else there is readable until the language is
39+
right; the language currently in use, in a grid of sixteen; the open statistics
40+
tab; and Close, which is destructive. The toggle grid stays unstyled — its state
41+
is already on the glyphs, and seven coloured switches highlight nothing.
42+
1943
## Media
2044

2145
Voice messages, video circles and audio files are transcribed in a direct chat

‎internal/bot/bot.go‎

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ type Bot struct {
100100
extractor *media.Extractor
101101
tmpDir string
102102
extractAbove int64
103+
// version is the build the About card names, the same string /healthz
104+
// reports, so a screenshot of the card identifies the running binary.
105+
version string
103106
// pulseEvery is how often the typing indicator is refreshed. It is a field
104107
// so tests can observe several ticks without sleeping for seconds.
105108
pulseEvery time.Duration
@@ -117,6 +120,15 @@ func New(store Store, tg Telegram, stt STT, log *slog.Logger) *Bot {
117120
stats: newStatsCache(defaultStatsTTL),
118121
pulseEvery: chatActionInterval,
119122
extractAbove: defaultExtractAbove,
123+
version: "dev",
124+
}
125+
}
126+
127+
// SetVersion names the build in the About card. It defaults to "dev", which is
128+
// what an unstamped local binary honestly is.
129+
func (b *Bot) SetVersion(v string) {
130+
if v != "" {
131+
b.version = v
120132
}
121133
}
122134

@@ -167,6 +179,13 @@ func (b *Bot) LastPoll() time.Time {
167179
// RegisterCommands publishes the command list once per language Voicy speaks,
168180
// plus a language-less default. Telegram picks the list matching the client's
169181
// language, so an English menu no longer greets a Russian or Ukrainian user.
182+
//
183+
// The direct-chat menu is one entry. Statistics, language, help and about are
184+
// tabs of the /start panel, and a command that only opens a tab already reachable
185+
// by a button is the same door listed twice. /v and /vp stay in the group menu
186+
// because they act on a message somebody points at, which no panel can do.
187+
// The four commands still answer for anyone who types them from memory; they are
188+
// simply not advertised.
170189
func (b *Bot) RegisterCommands(ctx context.Context) error {
171190
for _, lang := range append([]string{""}, i18n.Codes()...) {
172191
text := lang
@@ -175,10 +194,6 @@ func (b *Bot) RegisterCommands(ctx context.Context) error {
175194
}
176195
private := []tg.BotCommand{
177196
{Command: "start", Description: i18n.T(text, "cmd.start")},
178-
{Command: "stats", Description: i18n.T(text, "cmd.stats")},
179-
{Command: "language", Description: i18n.T(text, "cmd.language")},
180-
{Command: "help", Description: i18n.T(text, "cmd.help")},
181-
{Command: "about", Description: i18n.T(text, "cmd.about")},
182197
}
183198
if err := b.tg.SetMyCommandsForScope(ctx, private,
184199
&tg.BotCommandScope{Type: "all_private_chats", LanguageCode: lang}); err != nil {
@@ -467,11 +482,11 @@ func (b *Bot) Handle(ctx context.Context, upd tg.Update) error {
467482
case decide.Start:
468483
return b.handleStart(ctx, act, lang)
469484
case decide.Help:
470-
text, markup := helpPanel(lang, act.UserID)
485+
text, markup := helpPanel(lang, act.UserID, scopeOf(act.Chat))
471486
_, err := b.tg.SendMessage(ctx, act.ChatID, text, markup)
472487
return err
473488
case decide.About:
474-
text, markup := aboutPanel(lang, act.UserID)
489+
text, markup := aboutPanel(lang, act.UserID, b.version, scopeOf(act.Chat))
475490
_, err := b.tg.SendMessage(ctx, act.ChatID, text, markup)
476491
return err
477492
case decide.Stats:
@@ -522,7 +537,7 @@ func (b *Bot) handleLanguage(ctx context.Context, act decide.Action, lang string
522537
lang = code
523538
}
524539
}
525-
text, markup := languagePanel(lang, act.UserID)
540+
text, markup := languagePanel(lang, act.UserID, scopeOf(act.Chat))
526541
_, err := b.tg.SendMessage(ctx, act.ChatID, text, markup)
527542
return err
528543
}
@@ -539,7 +554,7 @@ func normalizeLangChoice(s string) string {
539554
}
540555

541556
func (b *Bot) handleStart(ctx context.Context, act decide.Action, lang string) error {
542-
text, markup := homePanel(lang, act.UserID)
557+
text, markup := homePanel(lang, act.UserID, scopeOf(act.Chat))
543558
if act.Ephemeral {
544559
_, err := b.tg.SendEphemeralMessage(ctx, act.ChatID, act.UserID, act.EphemeralMessageID, text, markup)
545560
return err
@@ -553,7 +568,7 @@ func (b *Bot) handleStats(ctx context.Context, act decide.Action, lang string, g
553568
if err != nil {
554569
return err
555570
}
556-
text, markup := statsPanel(lang, act.UserID, snap, global)
571+
text, markup := statsPanel(lang, act.UserID, snap, global, scopeOf(act.Chat))
557572
_, err = b.tg.SendMessage(ctx, act.ChatID, text, markup)
558573
return err
559574
}
@@ -632,33 +647,34 @@ func (b *Bot) handleCallback(ctx context.Context, act decide.Action, lang string
632647
if act.UserID != owner {
633648
return nil
634649
}
650+
sc := scopeOf(act.Chat)
635651
switch action {
636652
case "home":
637-
text, markup := homePanel(lang, owner)
653+
text, markup := homePanel(lang, owner, sc)
638654
return b.editPanel(ctx, act, text, markup)
639655
case "help":
640-
text, markup := helpPanel(lang, owner)
656+
text, markup := helpPanel(lang, owner, sc)
641657
return b.editPanel(ctx, act, text, markup)
642658
case "stats", "statsp", "statsg":
643659
global := action == "statsg"
644660
snap, err := b.snapshot(ctx, act.UserID, global)
645661
if err != nil {
646662
return err
647663
}
648-
text, markup := statsPanel(lang, owner, snap, global)
664+
text, markup := statsPanel(lang, owner, snap, global, sc)
649665
return b.editPanel(ctx, act, text, markup)
650666
case "about":
651-
text, markup := aboutPanel(lang, owner)
667+
text, markup := aboutPanel(lang, owner, b.version, sc)
652668
return b.editPanel(ctx, act, text, markup)
653669
case "lang":
654-
text, markup := languagePanel(lang, owner)
670+
text, markup := languagePanel(lang, owner, sc)
655671
return b.editPanel(ctx, act, text, markup)
656672
case "set":
657673
s, err := b.store.UserSettings(ctx, owner)
658674
if err != nil {
659675
return err
660676
}
661-
text, markup := settingsPanel(lang, owner, s)
677+
text, markup := settingsPanel(lang, owner, s, sc)
662678
return b.editPanel(ctx, act, text, markup)
663679
case "close":
664680
return b.deletePanel(ctx, act)
@@ -671,7 +687,7 @@ func (b *Bot) handleCallback(ctx context.Context, act decide.Action, lang string
671687
if err := b.store.SetLanguage(ctx, owner, code); err != nil {
672688
return err
673689
}
674-
text, markup := languagePanel(code, owner)
690+
text, markup := languagePanel(code, owner, sc)
675691
return b.editPanel(ctx, act, text, markup)
676692
}
677693
if raw, ok := strings.CutPrefix(action, "set:"); ok {
@@ -686,7 +702,7 @@ func (b *Bot) handleCallback(ctx context.Context, act decide.Action, lang string
686702
if err != nil {
687703
return err
688704
}
689-
text, markup := settingsPanel(lang, owner, s)
705+
text, markup := settingsPanel(lang, owner, s, sc)
690706
return b.editPanel(ctx, act, text, markup)
691707
}
692708
return nil

0 commit comments

Comments
 (0)