| 1 | package main |
| 2 | |
| 3 | import "reasonix/internal/session" |
| 4 | |
| 5 | // Resolve the durable identity before publishing a readable topic surface. |
| 6 | // Runtime ownership and controller startup belong to the existing tab build, |
| 7 | // not the synchronous phase of sidebar navigation. |
| 8 | func (a *App) canonicalTopicOpen(path string) (SessionTarget, *canonicalTabBinding, error) { |
| 9 | id, ok := parseSessionRoute(path) |
| 10 | if !ok { |
| 11 | return SessionTarget{}, nil, nil |
| 12 | } |
| 13 | ref := session.SessionRef{HostID: localDesktopHostID, SessionID: id} |
| 14 | target, err := a.resolveCanonicalSessionTarget(ref, "") |
| 15 | if err != nil { |
| 16 | return SessionTarget{}, nil, err |
| 17 | } |
| 18 | workspace, err := a.canonicalSessionWorkspace(a.bootContext(), ref) |
| 19 | if err != nil { |
| 20 | return SessionTarget{}, nil, err |
| 21 | } |
| 22 | state, err := a.workspaceRegistry().Load(a.bootContext()) |
| 23 | if err != nil { |
| 24 | return SessionTarget{}, nil, err |
| 25 | } |
| 26 | target.Scope, target.WorkspaceRoot = canonicalWorkspaceScope(workspace), workspace.Root |
| 27 | if target.Scope == "global" { |
| 28 | target.WorkspaceRoot = "" |
| 29 | } |
| 30 | title, source := a.canonicalTabTitle(a.bootContext(), state, ref) |
| 31 | return target, &canonicalTabBinding{ref: ref, workspaceID: workspace.ID, title: title, titleSource: source}, nil |
| 32 | } |
| 33 | |
| 34 | // A cancelled startup may still be unwinding when its session is selected |
| 35 | // again. Reusing that shell would wait on a build that can no longer publish. |
| 36 | // Leave it to its own completion and let the normal open path create a fresh |
| 37 | // surface for the same durable session. Callers hold App.mu. |
| 38 | func topicTabReusableLocked(tab *WorkspaceTab) bool { |
| 39 | return tab != nil && !tab.removed && (tab.Ctrl != nil || tab.buildDoneGen == 0 || tab.buildDoneGen == tab.buildGeneration) |
| 40 | } |
| 41 | |
| 42 | // newTopicTabLocked constructs the identity and profile before any runtime is |
| 43 | // started, so history readers can address the selected session immediately. |
| 44 | func (a *App) newTopicTabLocked(scope, workspaceRoot, actualRoot, topicID, sessionPath string, canonical *canonicalTabBinding) *WorkspaceTab { |
| 45 | topicTitle, topicSource := "", "" |
| 46 | if canonical != nil { |
| 47 | topicTitle, topicSource = canonical.title, canonical.titleSource |
| 48 | } else { |
| 49 | topicTitle = topicTitleForTab(scope, workspaceRoot, topicID) |
| 50 | if title, source, ok := topicTitleFallbackForOpen(workspaceRoot, topicID, sessionPath); ok { |
| 51 | topicTitle = title |
| 52 | _ = setTopicTitleWithSource(workspaceRoot, topicID, title, source) |
| 53 | } |
| 54 | topicSource = loadTopicTitleSource(topicTitleRoot(scope, workspaceRoot), topicID) |
| 55 | } |
| 56 | profile := defaultTabSessionProfile() |
| 57 | if sessionPath != "" && canonical == nil { |
| 58 | profile = loadTabSessionProfile(sessionPath) |
| 59 | } |
| 60 | tab := &WorkspaceTab{ |
| 61 | ID: a.newUniqueTabIDLocked(), |
| 62 | Scope: scope, |
| 63 | WorkspaceRoot: actualRoot, |
| 64 | TopicID: topicID, |
| 65 | TopicTitle: topicTitle, |
| 66 | topicTitleSource: topicSource, |
| 67 | SessionPath: sessionPath, |
| 68 | disabledMCP: map[string]ServerView{}, |
| 69 | } |
| 70 | // Existing canonical sessions publish their identity now. New topics are |
| 71 | // bound by the controller build without creating an empty legacy log. |
| 72 | if canonical != nil { |
| 73 | canonical.applyLocked(tab) |
| 74 | } |
| 75 | applyTabSessionProfile(tab, profile) |
| 76 | tab.sink = &tabEventSink{tabID: tab.ID, app: a} |
| 77 | return tab |
| 78 | } |
| 79 |