@@ -18,8 +18,9 @@ import (
1818// agent-side completion notice never leaves the LLM payload. bodek therefore
1919// watches GET /api/jobs itself: a 10s cadence whenever a session is live —
2020// the canonical case is a job finishing while the operator reads output —
21- // stepping up to 3s while the tab is visible. Diffs surface as transient
22- // notes; the tab is the full surface (rows, output detail, stop).
21+ // stepping up to 3s while the tab is visible. Terminal transitions surface
22+ // as alert-tier notes naming the command and fire the attention layer; the
23+ // tab is the full surface (rows, output detail, stop).
2324
2425const (
2526 jobsPollEvery = 3 * time .Second // tab visible: live view
@@ -91,10 +92,12 @@ func (m *Model) fetchJobs() tea.Cmd {
9192 }
9293}
9394
94- // applyJobs stores a snapshot; watcher diffs become transient notes. The
95- // first successful snapshot baselines silently — jobs that predate the
96- // attach belong to the tab, not the transcript.
97- func (m * Model ) applyJobs (jobs []client.Job , err error ) {
95+ // applyJobs stores a snapshot; watcher diffs become alert-tier notes — a
96+ // finished job is actionable, not housekeeping — and terminal transitions
97+ // fire the attention layer (bell / OSC 9). The first successful snapshot
98+ // baselines silently — jobs that predate the attach belong to the tab, not
99+ // the transcript. Returns the attention cmd (nil without a transition).
100+ func (m * Model ) applyJobs (jobs []client.Job , err error ) tea.Cmd {
98101 if err != nil {
99102 if errors .Is (err , client .ErrJobsUnavailable ) {
100103 m .jobsOff = true
@@ -103,17 +106,18 @@ func (m *Model) applyJobs(jobs []client.Job, err error) {
103106 m .panelMsg = "background commands need odek ≥ v1.38.0"
104107 m .refresh ()
105108 }
106- return
109+ return nil
107110 }
108111 if m .panel == panelJobs {
109112 // Transient network blips surface here only when the operator
110113 // is looking; the silent watcher keeps ticking.
111114 m .panelMsg = "error: " + err .Error ()
112115 m .refresh ()
113116 }
114- return
117+ return nil
115118 }
116119 m .jobs = jobs
120+ var attn tea.Cmd
117121 if m .jobsPrev == nil {
118122 m .jobsPrev = make (map [string ]string , len (jobs ))
119123 for _ , j := range jobs {
@@ -133,7 +137,10 @@ func (m *Model) applyJobs(jobs []client.Job, err error) {
133137 case ! seen :
134138 m .addTransientNote ("bg · " + jobStatusGlyph (j .Status ) + " " + sanitize (j .ID ) + " · " + sanitize (j .Command ))
135139 case old == "running" && j .Status != "running" :
136- m .addTransientNote (jobExitNote (j ))
140+ // Alert tier + attention: a finished job must survive a
141+ // glance-away, and the operator opted into knowing.
142+ m .addNote (jobExitNote (j ))
143+ attn = m .attentionCmd (m .attentionFor (jobAttentionKind (j )))
137144 }
138145 m .jobsPrev [j .ID ] = j .Status
139146 }
@@ -147,6 +154,7 @@ func (m *Model) applyJobs(jobs []client.Job, err error) {
147154 if m .panelSel >= m .panelLen () {
148155 m .panelSel = max (m .panelLen ()- 1 , 0 )
149156 }
157+ return attn
150158}
151159
152160// rearmJobs schedules the next fetch: the fast chain while the tab is
@@ -339,16 +347,29 @@ func jobStatusGlyph(status string) string {
339347 return "·"
340348}
341349
342- // jobExitNote is the watcher's terminal-transition note: glyph, id, terminal
343- // status, exit code when the server reported one, humanized runtime.
350+ // jobExitNote is the watcher's terminal-transition note: glyph, id, command
351+ // head, terminal status, exit code when the server reported one, humanized
352+ // runtime.
344353func jobExitNote (j client.Job ) string {
345- s := jobStatusGlyph (j .Status ) + " " + sanitize (j .ID ) + " " + j . Status
354+ s := jobStatusGlyph (j .Status ) + " " + sanitize (j .ID ) + " · " + truncate ( sanitize ( j . Command ), 48 )
346355 if j .ExitCode != nil {
347- s += fmt .Sprintf (" %d" , * j .ExitCode )
356+ s += " — " + j .Status + fmt .Sprintf (" %d" , * j .ExitCode )
357+ } else {
358+ s += " — " + j .Status
348359 }
349360 return s + " · " + fmtRuntime (j .RuntimeS )
350361}
351362
363+ // jobAttentionKind maps a terminal job status to its attention kind.
364+ func jobAttentionKind (j client.Job ) attentionKind {
365+ switch j .Status {
366+ case "failed" , "timeout" , "killed" :
367+ return attentionJobFailed
368+ default :
369+ return attentionJobDone
370+ }
371+ }
372+
352373// fmtRuntime humanizes seconds the way the transcript does durations.
353374func fmtRuntime (s float64 ) string {
354375 if s < 0 {
0 commit comments