Skip to content

Commit fee2e2e

Browse files
committed
fix(dictation): initialize non-zero session ID and filter stale session 0 messages
Ensure the first dictation session in a process gets stale-message protection by initializing sessionID to 1 in newDictationController and removing msg.sessionID != 0 exemptions in handleDictationTranscribed and handleDictationPartial. Clean up unreachable redaction guards in providerio. Refs #710
1 parent 9500b57 commit fee2e2e

4 files changed

Lines changed: 56 additions & 4 deletions

File tree

‎internal/providers/providerio/providerio.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ func looksLikeToken(w string) bool {
422422
// Redact removes known API-key and bearer-token forms from provider messages.
423423
func Redact(message string, secrets ...string) string {
424424
for _, secret := range secrets {
425-
if secret != "" && !strings.HasPrefix(secret, "[REDACTED") {
425+
if secret != "" {
426426
message = strings.ReplaceAll(message, secret, "[REDACTED]")
427427
}
428428
}
@@ -432,7 +432,7 @@ func Redact(message string, secrets ...string) string {
432432
strings.EqualFold(strings.TrimRight(words[index], ":"), "Token") ||
433433
strings.EqualFold(strings.TrimRight(words[index], ":"), "X-Api-Key") ||
434434
strings.EqualFold(strings.TrimRight(words[index], ":"), "api-key")
435-
if hdr && !strings.HasPrefix(words[index+1], "[REDACTED") && looksLikeToken(words[index+1]) {
435+
if hdr && looksLikeToken(words[index+1]) {
436436
words[index+1] = "[REDACTED]"
437437
}
438438
}

‎internal/tui/dictation.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ func newDictationController(opts Options) dictationController {
127127
platform: dictation.DetectPlatform(),
128128
downloadRoot: opts.STTDownloadRoot,
129129
userConfigPath: opts.UserConfigPath,
130+
sessionID: 1,
130131
}
131132
}
132133

@@ -378,7 +379,7 @@ func (m model) handleDictationStarted(msg dictationStartedMsg) (model, tea.Cmd)
378379
// handleDictationTranscribed inserts the final transcript into the composer (or
379380
// submits it when stt.autoSubmit is on), then returns to idle.
380381
func (m model) handleDictationTranscribed(msg dictationTranscribedMsg) (tea.Model, tea.Cmd) {
381-
if msg.sessionID != 0 && msg.sessionID != m.dictation.sessionID {
382+
if msg.sessionID != m.dictation.sessionID {
382383
return m, nil
383384
}
384385
streaming := m.dictation.streaming || msg.streaming

‎internal/tui/dictation_stream.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func (m model) startStreamingDictation() (model, tea.Cmd) {
7979
// composer, replacing the previously-rendered live region wholesale so the text
8080
// builds up in place as the user keeps talking.
8181
func (m model) handleDictationPartial(msg sttPartialMsg) model {
82-
if msg.sessionID != 0 && msg.sessionID != m.dictation.sessionID {
82+
if msg.sessionID != m.dictation.sessionID {
8383
return m
8484
}
8585
// Ignore stragglers that arrive after the session ended (cancel/final).

‎internal/tui/dictation_test.go‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,16 @@ func TestStaleDictationCompletionIgnoredAfterCancel(t *testing.T) {
353353
t.Fatalf("composer text = %q, want 'original composer text'", m.composer.text)
354354
}
355355

356+
// Stale partial message from session 1 must be ignored
357+
stalePartial := sttPartialMsg{
358+
sessionID: 1,
359+
text: "stale partial that should be ignored",
360+
}
361+
mPartial := m.handleDictationPartial(stalePartial)
362+
if mPartial.composer.text != "original composer text" {
363+
t.Fatalf("composer text changed to %q after stale partial", mPartial.composer.text)
364+
}
365+
356366
staleMsg := dictationTranscribedMsg{
357367
sessionID: 1,
358368
text: "stale text that should be ignored",
@@ -369,3 +379,44 @@ func TestStaleDictationCompletionIgnoredAfterCancel(t *testing.T) {
369379
t.Fatalf("expected no command for stale completion, got %v", cmd)
370380
}
371381
}
382+
383+
func TestStaleDictationSessionZeroIgnoredAfterCancel(t *testing.T) {
384+
m := model{}
385+
m.setComposerState(composerState{text: "initial composer text", cursor: 21})
386+
m.dictation.sessionID = 0
387+
m.dictation.phase = dictRecording
388+
389+
m, _ = m.cancelDictation()
390+
391+
// Session 0 was reset to session 1
392+
if m.dictation.sessionID != 1 {
393+
t.Fatalf("dictation.sessionID = %d after cancel, want 1", m.dictation.sessionID)
394+
}
395+
396+
// Stale partial from session 0 must be ignored
397+
stalePartial := sttPartialMsg{
398+
sessionID: 0,
399+
text: "stale partial from session 0",
400+
}
401+
mPartial := m.handleDictationPartial(stalePartial)
402+
if mPartial.composer.text != "initial composer text" {
403+
t.Fatalf("composer text changed to %q after stale session 0 partial", mPartial.composer.text)
404+
}
405+
406+
// Stale completion from session 0 must be ignored
407+
staleMsg := dictationTranscribedMsg{
408+
sessionID: 0,
409+
text: "stale completion from session 0",
410+
submit: true,
411+
streaming: true,
412+
}
413+
afterStale, cmd := m.handleDictationTranscribed(staleMsg)
414+
got := afterStale.(model)
415+
416+
if got.composer.text != "initial composer text" {
417+
t.Fatalf("composer text changed to %q after stale session 0 completion", got.composer.text)
418+
}
419+
if cmd != nil {
420+
t.Fatalf("expected no command for stale session 0 completion, got %v", cmd)
421+
}
422+
}

0 commit comments

Comments
 (0)