@@ -813,14 +813,11 @@ func (m *actionsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
813813 case "c" :
814814 if m .focusTask != "" {
815815 text := m .buildCopyText (m .focusTask )
816- res := copyToClipboardOrFile (context .Background (), text , m . focusTask )
817- if res . err != nil {
818- m .clipboardMsg = res . err . Error ()
816+ method , err := copyToClipboard (context .Background (), text )
817+ if err != nil {
818+ m .clipboardMsg = "no clipboard tool available — use w to save to file"
819819 m .clipboardOK = false
820- } else if res .method == "file" {
821- m .clipboardMsg = "saved to " + res .path
822- m .clipboardOK = true
823- } else if res .method == "osc52" {
820+ } else if method == "osc52" {
824821 m .clipboardMsg = "sent via OSC 52 (terminal clipboard)"
825822 m .clipboardOK = true
826823 } else {
@@ -829,6 +826,19 @@ func (m *actionsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
829826 }
830827 m .clipboardMsgExpiry = time .Now ().Add (4 * time .Second )
831828 }
829+ case "w" :
830+ if m .focusTask != "" {
831+ text := m .buildCopyText (m .focusTask )
832+ res := writeToTempFile (text , m .focusTask )
833+ if res .err != nil {
834+ m .clipboardMsg = res .err .Error ()
835+ m .clipboardOK = false
836+ } else {
837+ m .clipboardMsg = "saved to " + res .path
838+ m .clipboardOK = true
839+ }
840+ m .clipboardMsgExpiry = time .Now ().Add (4 * time .Second )
841+ }
832842 case "r" :
833843 if t := m .cursorTask ; t != "" {
834844 if e , ok := m .taskState [t ]; ok && e .state == actionFailed {
@@ -1149,6 +1159,7 @@ func (m *actionsModel) View() tea.View {
11491159 if m .focusTask != "" {
11501160 parts = append (parts , "Enter/f: all logs" )
11511161 parts = append (parts , "c: copy logs" )
1162+ parts = append (parts , "w: save to file" )
11521163 } else {
11531164 parts = append (parts , "Enter/f: focus logs" )
11541165 }
@@ -1532,33 +1543,25 @@ func stripActANSI(s string) string {
15321543 return b .String ()
15331544}
15341545
1535- // copyResult holds the outcome of a copy operation.
1546+ // copyResult holds the outcome of a file-save operation.
15361547type copyResult struct {
1537- method string // "clipboard", "file"
1538- path string // only set when method == "file"
1539- err error
1548+ path string
1549+ err error
15401550}
15411551
1542- // copyToClipboardOrFile tries: native clipboard tools → OSC 52 → temp file.
1543- // Native tools are tried first because they give a real exit-code signal.
1544- // OSC 52 has no feedback mechanism (terminals silently ignore unsupported
1545- // sequences), so it is used as a best-effort fallback for SSH sessions where
1546- // native tools are unavailable.
1547- func copyToClipboardOrFile (ctx context.Context , text , taskName string ) copyResult {
1548- // 1. Native clipboard tools — reliable success/failure via exit code.
1552+ // copyToClipboard tries native clipboard tools first, then OSC 52 as a
1553+ // best-effort fallback for SSH sessions. Returns the method used ("clipboard"
1554+ // or "osc52"), or an error if no tool is available.
1555+ func copyToClipboard (ctx context.Context , text string ) (string , error ) {
15491556 if tryNativeClipboard (ctx , text ) {
1550- return copyResult { method : "clipboard" }
1557+ return "clipboard" , nil
15511558 }
1552-
1553- // 2. OSC 52: best-effort for SSH sessions. The sequence is written to the
1554- // terminal but there is no way to confirm the terminal honored it. We fall
1555- // through to a temp file only if stdout is not a terminal at all.
1559+ // OSC 52 has no feedback mechanism — terminals silently ignore unsupported
1560+ // sequences — but it is useful over SSH where native tools are absent.
15561561 if tryOSC52 (text ) {
1557- return copyResult { method : "osc52" }
1562+ return "osc52" , nil
15581563 }
1559-
1560- // 3. Fallback: write to temp file.
1561- return writeToTempFile (text , taskName )
1564+ return "" , fmt .Errorf ("no clipboard tool available" )
15621565}
15631566
15641567// tryOSC52 writes the OSC 52 escape sequence to stdout.
@@ -1625,7 +1628,7 @@ func writeToTempFile(text, taskName string) copyResult {
16251628 if _ , err := f .WriteString (text ); err != nil {
16261629 return copyResult {err : fmt .Errorf ("failed to write temp file: %w" , err )}
16271630 }
1628- return copyResult {method : "file" , path : f .Name ()}
1631+ return copyResult {path : f .Name ()}
16291632}
16301633
16311634func fmtActionDuration (d time.Duration ) string {
0 commit comments