Skip to content

Commit 00338b2

Browse files
authored
feat(actions): restore task preamble log and fix clipboard keybindings (#235)
* feat(actions): restore task preamble log and fix clipboard fallback order Re-add the dir/cmd/env preamble that was printed at the top of each task log in the old implementation. emitTaskPreamble emits the working directory, command, and any gitte-injected env vars (project env, env_when, feature gates) before the command starts, making logs self-contained for debugging. Fix copyToClipboardOrFile to try native clipboard tools before OSC 52. Native tools provide a real exit-code signal; OSC 52 has no feedback mechanism and silently succeeds even in terminals that do not support it. OSC 52 is now used as a best-effort fallback for SSH sessions and reports a distinct "sent via OSC 52 (terminal clipboard)" toast message. * refactor(actions): split clipboard and save-to-file into separate keybindings c: copies to clipboard (native tools → OSC 52, errors if nothing available). w: always writes a temp file and shows the path. Removes the silent fallback where clipboard failure would silently produce a file instead, making the two operations explicit and independently reachable.
1 parent ad8c000 commit 00338b2

2 files changed

Lines changed: 87 additions & 22 deletions

File tree

actions/runner.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"path/filepath"
99
"regexp"
1010
"runtime"
11+
"sort"
1112
"strconv"
1213
"strings"
1314
"sync"
@@ -274,6 +275,8 @@ func runGroupTask(
274275

275276
env := buildEnv(cfg, st, projName, proj)
276277

278+
emitTaskPreamble(ctx, handler, taskName, taskDir, cmds, proj, cfg, st, projName)
279+
277280
wrappedHandler := &searchForHandler{
278281
inner: handler,
279282
searchFors: searchFors,
@@ -295,6 +298,56 @@ func runGroupTask(
295298
return nil
296299
}
297300

301+
// emitTaskPreamble writes a short header to the task log showing the working
302+
// directory, command, and any env vars injected by gitte (project env, env_when,
303+
// feature gates). It is emitted before the command starts so the log is
304+
// self-contained for debugging.
305+
func emitTaskPreamble(
306+
ctx context.Context,
307+
handler executor.OutputHandler,
308+
taskName, taskDir string,
309+
cmds []string,
310+
proj config.ProjectConfig,
311+
cfg *config.GitteConfig,
312+
st *state.GitteState,
313+
projName string,
314+
) {
315+
emit := func(line string) {
316+
_ = handler.HandleOutput(ctx, executor.Output{
317+
Output: []byte(line),
318+
CmdName: taskName,
319+
Stream: executor.StdoutStream,
320+
})
321+
}
322+
323+
emit(" dir: " + taskDir)
324+
emit(" cmd: " + strings.Join(cmds, " "))
325+
326+
// Collect only the vars gitte injects (not all of os.Environ).
327+
injected := make(map[string]string)
328+
for k, v := range proj.Env {
329+
injected[k] = v
330+
}
331+
for k, v := range config.ResolveEnvWhen(proj.EnvWhen, runtime.GOARCH) {
332+
injected[k] = v
333+
}
334+
for k, v := range extraEnvForProject(cfg, st, projName, proj) {
335+
injected[k] = v
336+
}
337+
if len(injected) > 0 {
338+
keys := make([]string, 0, len(injected))
339+
for k := range injected {
340+
keys = append(keys, k)
341+
}
342+
sort.Strings(keys)
343+
parts := make([]string, 0, len(keys))
344+
for _, k := range keys {
345+
parts = append(parts, k+"="+injected[k])
346+
}
347+
emit(" env: " + strings.Join(parts, " "))
348+
}
349+
}
350+
298351
// extraEnvForProject returns the env vars injected by feature gates for a project.
299352
func extraEnvForProject(cfg *config.GitteConfig, st *state.GitteState, projName string, proj config.ProjectConfig) map[string]string {
300353
if st == nil || cfg.FeatureGates == nil {

actions/view.go

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -813,19 +813,32 @@ 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
820+
} else if method == "osc52" {
821+
m.clipboardMsg = "sent via OSC 52 (terminal clipboard)"
822822
m.clipboardOK = true
823823
} else {
824824
m.clipboardMsg = "copied to clipboard"
825825
m.clipboardOK = true
826826
}
827827
m.clipboardMsgExpiry = time.Now().Add(4 * time.Second)
828828
}
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+
}
829842
case "r":
830843
if t := m.cursorTask; t != "" {
831844
if e, ok := m.taskState[t]; ok && e.state == actionFailed {
@@ -1146,6 +1159,7 @@ func (m *actionsModel) View() tea.View {
11461159
if m.focusTask != "" {
11471160
parts = append(parts, "Enter/f: all logs")
11481161
parts = append(parts, "c: copy logs")
1162+
parts = append(parts, "w: save to file")
11491163
} else {
11501164
parts = append(parts, "Enter/f: focus logs")
11511165
}
@@ -1529,27 +1543,25 @@ func stripActANSI(s string) string {
15291543
return b.String()
15301544
}
15311545

1532-
// copyResult holds the outcome of a copy operation.
1546+
// copyResult holds the outcome of a file-save operation.
15331547
type copyResult struct {
1534-
method string // "clipboard", "file"
1535-
path string // only set when method == "file"
1536-
err error
1548+
path string
1549+
err error
15371550
}
15381551

1539-
// copyToClipboardOrFile tries: OSC 52 → native clipboard tools → temp file.
1540-
func copyToClipboardOrFile(ctx context.Context, text, taskName string) copyResult {
1541-
// 1. OSC 52: works over SSH in modern terminals (iTerm2, kitty, WezTerm, etc.).
1542-
if tryOSC52(text) {
1543-
return copyResult{method: "clipboard"}
1544-
}
1545-
1546-
// 2. Native clipboard tools.
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) {
15471556
if tryNativeClipboard(ctx, text) {
1548-
return copyResult{method: "clipboard"}
1557+
return "clipboard", nil
15491558
}
1550-
1551-
// 3. Fallback: write to temp file.
1552-
return writeToTempFile(text, taskName)
1559+
// OSC 52 has no feedback mechanism — terminals silently ignore unsupported
1560+
// sequences — but it is useful over SSH where native tools are absent.
1561+
if tryOSC52(text) {
1562+
return "osc52", nil
1563+
}
1564+
return "", fmt.Errorf("no clipboard tool available")
15531565
}
15541566

15551567
// tryOSC52 writes the OSC 52 escape sequence to stdout.
@@ -1616,7 +1628,7 @@ func writeToTempFile(text, taskName string) copyResult {
16161628
if _, err := f.WriteString(text); err != nil {
16171629
return copyResult{err: fmt.Errorf("failed to write temp file: %w", err)}
16181630
}
1619-
return copyResult{method: "file", path: f.Name()}
1631+
return copyResult{path: f.Name()}
16201632
}
16211633

16221634
func fmtActionDuration(d time.Duration) string {

0 commit comments

Comments
 (0)