Repository navigation
Keep one workspace view per window across session switches #1500
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| using Avalonia.Controls; | ||
| using Avalonia.Controls.Templates; | ||
| using Avalonia.Markup.Xaml.Templates; | ||
|
|
||
| namespace Capacitor.App.Views; | ||
|
|
||
| /// A data template that builds its view once and hands the same instance back on every later | ||
| /// build, including after the presenter has dropped it for null or another template's content. | ||
| /// On macOS a detached control stays reachable from any accessibility peer a client created for | ||
| /// it, so a template that builds afresh leaks the whole previous view on each rebuild. | ||
| public sealed class RetainedViewTemplate : DataTemplate, IRecyclingDataTemplate { | ||
| Control? _view; | ||
|
|
||
| Control? ITemplate<object?, Control?>.Build(object? param) => Build(param, null); | ||
|
|
||
| Control? IRecyclingDataTemplate.Build(object? data, Control? existing) => Build(data, existing); | ||
|
|
||
| new Control? Build(object? data, Control? existing) { | ||
| // Another presenter matching this template must not steal the view from the one showing it. | ||
| if (_view is { Parent: not null } && !ReferenceEquals(_view, existing)) return base.Build(data, null); | ||
| return _view ??= base.Build(data, null); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1. Closed sessions remain in memory until reopened RetainedViewTemplate keeps the detached workspace control in _view, but neither the template nor WorkspaceView clears its session DataContext when the control leaves the presenter. After CloseWorkspace() sets CurrentWorkspace to null, the cached control can keep the closed workspace model, its subscription, and lazily created panes reachable until another session replaces its context. Agent Prompt
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not reproduced: the view's DataContext is inherited from the presenter, so removing it from the presenter on close clears it, and |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| using Capacitor.App.Services; | ||
| using Capacitor.App.ViewModels; | ||
| using Capacitor.Cli.Core.PullRequests; | ||
| using Capacitor.Remote.Models; | ||
| using DynamicData; | ||
| using Microsoft.Extensions.Time.Testing; | ||
|
|
||
| namespace Capacitor.App.Tests.Unit; | ||
|
|
||
| /// The remote host's dependencies, held together so a test disposes them once. The lane is | ||
| /// never connected, so even a row that carries a session id dials nothing and the routing | ||
| /// assertions stay deterministic. | ||
| sealed class RemoteHost : IDisposable { | ||
| readonly FakeServerLane _lane = new(); | ||
| readonly SessionAccessService _access; | ||
| readonly FakePermissionService _permissions = new(); | ||
|
|
||
| public RemoteHost() => _access = new SessionAccessService(_lane, new FakeTimeProvider()); | ||
|
|
||
| public FakeAgentDirectory Directory { get; } = new(); | ||
| public FakeWorkContextSource WorkSource { get; } = new(); | ||
|
|
||
| // A separate overload rather than an optional parameter: the factory parameter takes this | ||
| // as a method group, and that conversion needs an exact one-argument signature. | ||
| public RemoteSessionViewModel New(string agentId) => New(agentId, null); | ||
|
|
||
| /// A session id only where a test needs one: it is what a local row is compared against | ||
| /// before the removal counts as an origin change rather than an ended session. | ||
| public RemoteSessionViewModel New(string agentId, string? sessionId, IPullRequestSource? pullRequests = null, ComposerDrafts? drafts = null) { | ||
| var row = AgentRow.FromRemote(new AgentInstanceDto { | ||
| AgentId = agentId, SessionId = sessionId, Status = "Running", DaemonName = "work-mac", OwnerUserId = "u1", | ||
| Vendor = "claude", RegisteredAt = DateTime.UtcNow, | ||
| }); | ||
| Directory.Rows.AddOrUpdate(row); | ||
| return new RemoteSessionViewModel(row, Directory, _access, _permissions, WorkspaceFixtures.NewActions(), _lane, | ||
| (_, _) => Task.FromResult(new SessionDetailFetch(null)), new RecordingOpener(), new FakeTimeProvider(), WorkSource, | ||
| () => new FakeTerminalSurface(), pullRequests: pullRequests, drafts: drafts); | ||
| } | ||
|
|
||
| public void Dispose() { | ||
| _access.Dispose(); | ||
| _permissions.Dispose(); | ||
| Directory.Dispose(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a user scrolls away from the bottom in session A, closes it, and opens session B, this template now returns the same entire
WorkspaceView, including itsChatTabView. That control keeps session-specific fields such as_followTailand the existing scroll viewer offset, and has noDataContextChangedreset; consequently session B can open at an inherited scroll position and remain opted out of following new messages. Reset transient child-view state when the data context changes before reusing this control.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Valid —
ChatTabViewnow resets follow-tail, the toggle anchor, thumb and smooth-scroll state inOnDataContextChanged, so the next session opens at the tail.A_new_session_in_the_same_view_opens_at_the_bottom_and_followsfailed before the change. (A direct local-to-local switch already reused this view before this PR, so it fixes that path too.) 094a975