Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- In the native window, "Unfold long pastes" now unfolds Claude Code and Kiro CLI pastes: the clipboard, a batch of review comments, and the prompt a pull request or issue opens a session with.
- In the native window, "Unfold long pastes" now unfolds Claude Code and Kiro CLI pastes: the clipboard, a batch of review comments, the prompt a pull request or issue opens a session with, and dropped or attached file paths.

## [0.71.1] - 2026-10-18

Expand Down
8 changes: 5 additions & 3 deletions docs/ceilings.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,11 @@ work when nobody knows it and that the call site never shows. The mechanism and
replacing it; opencode's own "Disable paste summary" command is stored in its state and wins over any config, so
once used there the switch changes nothing. Codex, Antigravity, Crush and Cursor CLI fold with a constant, no
setting and no key that unfolds, so they have no switch; oh-my-pi was not measured. The native window also
unfolds what it writes as a paste itself (a batch of review comments, a pull request's or an issue's prompt),
measured against the session's own terminal; one written into a session whose terminal the window never opened
has no height to measure and lands folded. Dropped and attached file paths are never unfolded.
unfolds what it writes as a paste itself (a batch of review comments, a pull request's or an issue's prompt,
dropped and attached file paths), measured against the session's own terminal; one written into a session whose
terminal the window never opened has no height to measure and lands folded. No caller does that today: each
writes into the session on screen, or waits for a session it just opened to reach its prompt, which only
happens after the window started its terminal.
- **`LICH_WORKTREE_PORT` is reserved, never held** (`internal/terminal/worktreeport.go`): the number is a name the
checkout owns, nothing binds it, and anything on the machine can take the port before the dev server starts.
- **A folder is a name on a session, so it has no life of its own** (`internal/store/mutations.go`,
Expand Down
4 changes: 3 additions & 1 deletion native/cmd/lich-native/footer.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ type footer struct {
quota *planQuota
handsOn *handsOn
cost *costGate
// writePaste is terminals.writePaste, which unfolds a long paste.
writePaste func(ctx context.Context, id, block string) error

mu sync.Mutex
layout footerLayout
Expand Down Expand Up @@ -230,7 +232,7 @@ func (f *footer) act(gtx C, in footerInput) {
// A failure is toasted in the backend's own words, which name the ceiling a
// file was refused for.
func (f *footer) attach(in footerInput) {
if err := attachFile(f.ctx, f.client, in.sessionID, in.checkout, in.sandboxed, runtime.GOOS == "windows"); err != nil {
if err := attachFile(f.ctx, f.client, f.writePaste, in.sessionID, in.checkout, in.sandboxed, runtime.GOOS == "windows"); err != nil {
f.toaster.Show(ui.Toast{Variant: ui.ToastError, Title: lichclient.ErrorText(err)})
}
}
Expand Down
7 changes: 4 additions & 3 deletions native/cmd/lich-native/footer_attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ import (
// its checkout, so the call that chooses the file also copies it where the
// session can read it. checkout, not the shell's cwd, because the sandbox is
// built around the spawn directory. A cancelled dialog is no error; a failure
// carries the backend's sentence, which names why a file was refused.
func attachFile(ctx context.Context, c *lichclient.Client, sessionID, checkout string, sandboxed, windows bool) error {
// carries the backend's sentence, which names why a file was refused. write
// puts the paste at the prompt.
func attachFile(ctx context.Context, c *lichclient.Client, write func(ctx context.Context, id, block string) error, sessionID, checkout string, sandboxed, windows bool) error {
a, err := c.Attach(ctx, sessionID, checkout, sandboxed)
if err != nil || a.Path == "" {
return err
}
return c.Write(ctx, sessionID, composeDroppedPaths([]string{a.Path}, windows, []string{a.Notice}))
return write(ctx, sessionID, composeDroppedPaths([]string{a.Path}, windows, []string{a.Notice}))
}

// composeDroppedPaths is drop-files.ts: the paths quoted and space-separated
Expand Down
4 changes: 2 additions & 2 deletions native/cmd/lich-native/footer_readout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ func attachBackend(t *testing.T, picked lichclient.Attachment, writes *[][]strin
func TestAttachFilePastesThePickedPathUnsent(t *testing.T) {
var writes [][]string
c := attachBackend(t, lichclient.Attachment{Path: "/cfg/dropped/b.png", Notice: "copy of b"}, &writes)
if err := attachFile(context.Background(), c, "s1", "/repo", true, false); err != nil {
if err := attachFile(context.Background(), c, c.Write, "s1", "/repo", true, false); err != nil {
t.Fatal(err)
}
want := [][]string{{"s1", "\x1b[200~/cfg/dropped/b.png\ncopy of b\n\x1b[201~"}}
Expand All @@ -363,7 +363,7 @@ func TestAttachFilePastesThePickedPathUnsent(t *testing.T) {
func TestAttachFileCancelledWritesNothing(t *testing.T) {
var writes [][]string
c := attachBackend(t, lichclient.Attachment{}, &writes)
if err := attachFile(context.Background(), c, "s1", "/repo", true, false); err != nil || len(writes) != 0 {
if err := attachFile(context.Background(), c, c.Write, "s1", "/repo", true, false); err != nil || len(writes) != 0 {
t.Errorf("cancelled dialog: err %v, writes %q", err, writes)
}
}
1 change: 1 addition & 0 deletions native/cmd/lich-native/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ func run(w *app.Window, rt lichclient.Runtime) error {
u.dock.tab = map[string]dockTab{"code": dockCode, "review": dockReview}[*flagTab]
u.footer.dock = u.dock
u.footer.openPullRequest = u.showPulls
u.footer.writePaste = u.terms.writePaste
u.footer.showDelegates = func(id string) { u.layoutStage.delegates = id }
u.settings.footer = u.footer
u.footer.customize = u.customizeFooter
Expand Down
25 changes: 25 additions & 0 deletions native/cmd/lich-native/paste_unfold_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package main

import (
"encoding/json"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -145,3 +148,25 @@ func TestBatchOfCommentsStaysFoldedWithTheSwitchOff(t *testing.T) {
t.Errorf("wrote %q, want the batch once", got)
}
}

// Dropping enough files writes a paste Claude Code would fold, and it lands
// unfolded the same as any other.
func TestDropOfManyFilesUnfoldsInClaudeCode(t *testing.T) {
u, b := newGateWindow(t)
b.answer["store.GetSetting"] = "true"
u.terms = claudeTerminals(t, b)
dir := t.TempDir()
var paths []string
for i := range 12 {
path := filepath.Join(dir, strings.Repeat("a-file-with-a-long-name-", 3)+strconv.Itoa(i))
if err := os.WriteFile(path, nil, 0o600); err != nil {
t.Fatal(err)
}
paths = append(paths, path)
}
u.dropFiles(dropTarget{sessionID: "s", checkout: dir}, paths)
paste := composeDroppedPaths(paths, false, nil)
if got := injectedWrite(t, b); got != paste+paste {
t.Errorf("wrote %q, want the drop twice", got)
}
}
2 changes: 1 addition & 1 deletion native/cmd/lich-native/terminal_drop_hint.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,5 @@ func (u *window) dropFiles(target dropTarget, paths []string) {
if paste == "" {
return
}
u.logErr("drop files", u.client.Write(u.ctx, target.sessionID, paste))
u.logErr("drop files", u.terms.writePaste(u.ctx, target.sessionID, paste))
}
Loading