From d1bf2ecde5302ebbf9cb7d6533ec9bbc81ec8ccd Mon Sep 17 00:00:00 2001 From: dawn <90008@klbr.net> Date: Fri, 2 Oct 2026 15:44:43 +0300 Subject: [PATCH] tui: after /cd, offer the /link add that links the folder the session left a notice with the folder as stored, ready to paste; a later move replaces it, and a move away from a missing folder offers nothing. Signed-off-by: dawn <90008@klbr.net> --- cli/internal/tui/chat.go | 20 +++++++++++++++++++- cli/test/e2e/tui_folders_test.go | 14 ++++++++++++++ robot-docs/workspaces.md | 16 ++++++++++++---- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/cli/internal/tui/chat.go b/cli/internal/tui/chat.go index a2a1eaac..2c163d6a 100644 --- a/cli/internal/tui/chat.go +++ b/cli/internal/tui/chat.go @@ -1647,7 +1647,12 @@ func (m ChatModel) update(msg tea.Msg) (ChatModel, tea.Cmd) { // Moved takes the session's new folder, then sends again the turn a // missing folder refused. func (m *ChatModel) Moved(moved daemon.Session, retry *WorkspaceRetry) tea.Cmd { - workspace := moved.Workspace + workspace, from := moved.Workspace, m.Workspace + m.dropSignIn() // the offer was for the old folder's host + // a hint from an earlier move names a folder this one did not leave + m.Notices = slices.DeleteFunc(m.Notices, func(n Notice) bool { return strings.HasPrefix(n.Message, linkHintText) }) + m.Notices.AddNotice(linkHint(from, workspace, retry)) + m.syncViewportHeight() m.Workspace, m.Host, m.hostHome = workspace, sessionHost(moved), "" m.Renderer.Workspace = workspace m.rebuildSettledLines() // settled rows name paths from the old workspace @@ -1655,6 +1660,19 @@ func (m *ChatModel) Moved(moved daemon.Session, retry *WorkspaceRetry) tea.Cmd { return m.resend(retry) } +const linkHintText = "to share memory and work with the folder you left: /link add " + +// linkHint offers linking the folder a session just left with the one it +// moved to, as a command to paste. The folder is named as stored, so the +// link lands on the key the left folder's memory and work items use. A +// session that moved because its folder went missing has nothing to link. +func linkHint(from, to string, retry *WorkspaceRetry) string { + if from == "" || from == to || retry != nil { + return "" + } + return linkHintText + from +} + // resend sends again a turn the daemon refused, once what refused it is fixed. func (m *ChatModel) resend(retry *WorkspaceRetry) tea.Cmd { if retry == nil { diff --git a/cli/test/e2e/tui_folders_test.go b/cli/test/e2e/tui_folders_test.go index 3ec70442..95cb47cf 100644 --- a/cli/test/e2e/tui_folders_test.go +++ b/cli/test/e2e/tui_folders_test.go @@ -62,6 +62,20 @@ func TestTUICdMovesTheSessionThroughThePicker(t *testing.T) { if got := daemonSession(t, id).Workspace; !same(got, filepath.Join(root, "other")) || !same(d.App.Chat.Workspace, got) { t.Fatalf("/cd ../other left the daemon at %q and the chat at %q", got, d.App.Chat.Workspace) } + // The chat offers linking the folder it left, as a command to paste. + view := d.View() + _, offered, ok := strings.Cut(view, "/link add ") + if !ok { + t.Fatalf("moving did not offer /link add:\n%s", view) + } + offered = strings.Fields(offered)[0] + if !same(offered, filepath.Join(root, "picked")) { + t.Fatalf("the hint offers %q, not the folder the session left", offered) + } + d.Dispatch(tui.ChatExecuteCommandMsg{Name: "/link", Args: "add " + offered}) + if view := d.View(); !strings.Contains(view, "linked with "+offered) { + t.Fatalf("pasting the hint did not link the folders:\n%s", view) + } } func TestTUIMissingWorkspaceSendsTheTurnFromThePickedFolder(t *testing.T) { diff --git a/robot-docs/workspaces.md b/robot-docs/workspaces.md index d2a08830..6ed6e54a 100644 --- a/robot-docs/workspaces.md +++ b/robot-docs/workspaces.md @@ -122,10 +122,11 @@ host in every layout that shows a place, and colors the host with a stable hue derived from its label, lifted to the theme's contrast. the host goes faint while the session's kernel boots or reattaches (the status line says `connecting to chernobog…` with an animated face, the same one the picker -shows beside a host still warming, picked by the host's label) and takes the error color once it is lost (the -port owner gave up and forgot the kernel, so the status line says the next -turn starts a fresh one), from -the session status's `kernel.link` (kernel.md). recent folders and the +shows beside a host still warming, picked by the host's label, or `copying +the kernel to chernobog…` while albedo is staged there) and takes the error +color once it is lost (the port owner gave up and forgot the kernel, so the +status line says the next turn starts a fresh one), from the session +status's `kernel.link` (kernel.md). recent folders and the sessions view lead remote entries with their host. ## moving a session @@ -144,6 +145,13 @@ once; a running one moves when its current turn ends. descendants that were working somewhere else keep their own folder. if the session itself cannot move, nobody moves. +after a move the tui offers linking the folder the session left, as a notice +to paste: `/link add ` (absolute, `user@` kept, so the link +lands on the key that folder's memory and work items use). a later move +replaces it, and a move away from a folder that went missing offers nothing. +it does not check whether the two are already linked; `/link add` refuses +that. + ## browsing all three routes take `path`: absolute, or starting with `~`, which expands -- knotmirror 0.0.1