-
Notifications
You must be signed in to change notification settings - Fork 1.5k
fix(desktop): recover missing topic indexes #3228
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
46af4a5
574c495
0409d66
fbdcf36
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 |
|---|---|---|
|
|
@@ -586,6 +586,11 @@ func (a *App) buildTabController(tab *WorkspaceTab) { | |
| // Write/update scope/session meta. | ||
| if path != "" { | ||
| a.persistTabSessionPath(tab, path) | ||
| if strings.TrimSpace(tab.TopicID) != "" { | ||
| if err := ensureTopicIndexed(tab.Scope, tab.WorkspaceRoot, tab.TopicID, tab.TopicTitle, loadTopicTitleSource(topicTitleRoot(tab.Scope, tab.WorkspaceRoot), tab.TopicID)); err == nil { | ||
|
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.
When a persisted tab is restored after its topic title index was deleted, Useful? React with 👍 / 👎. |
||
| a.emitProjectTreeChanged() | ||
|
SivanCola marked this conversation as resolved.
|
||
| } | ||
| } | ||
| // Restore existing telemetry if resuming a session. | ||
| telemetryPath := path + ".telemetry.json" | ||
| if records := loadTelemetry(telemetryPath); len(records) > 0 { | ||
|
|
@@ -1282,10 +1287,7 @@ func loadTopicTitleSource(workspaceRoot, topicID string) string { | |
| } | ||
|
|
||
| func topicTitleForTab(scope, workspaceRoot, topicID string) string { | ||
| titleRoot := workspaceRoot | ||
| if scope == "global" { | ||
| titleRoot = "" | ||
| } | ||
| titleRoot := topicTitleRoot(scope, workspaceRoot) | ||
| if title := strings.TrimSpace(loadTopicTitle(titleRoot, topicID)); title != "" { | ||
| return title | ||
| } | ||
|
|
@@ -1295,6 +1297,13 @@ func topicTitleForTab(scope, workspaceRoot, topicID string) string { | |
| return defaultTopicTitle | ||
| } | ||
|
|
||
| func topicTitleRoot(scope, workspaceRoot string) string { | ||
| if scope == "global" { | ||
| return "" | ||
| } | ||
| return workspaceRoot | ||
| } | ||
|
|
||
| func forkTopicTitle(title string) string { | ||
| base := strings.TrimSpace(title) | ||
| if base == "" || base == defaultTopicTitle || base == "Global" { | ||
|
|
@@ -1340,6 +1349,49 @@ func setTopicTitleSource(workspaceRoot, topicID, source string) error { | |
| return saveTopicTitleSources(workspaceRoot, sources) | ||
| } | ||
|
|
||
| // topicIndexMu serializes recovery writes to desktop-projects.json and topic | ||
| // title indexes. Startup builds restored tabs concurrently, and each tab may | ||
| // repair its missing index. | ||
| var topicIndexMu sync.Mutex | ||
|
|
||
| func ensureTopicIndexed(scope, workspaceRoot, topicID, title, source string) error { | ||
| topicID = strings.TrimSpace(topicID) | ||
| if topicID == "" { | ||
| return fmt.Errorf("topicID is required") | ||
| } | ||
| topicIndexMu.Lock() | ||
|
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.
This lock only serializes Useful? React with 👍 / 👎. |
||
| defer topicIndexMu.Unlock() | ||
| if strings.TrimSpace(scope) == "global" { | ||
| workspaceRoot = "" | ||
| } else { | ||
| workspaceRoot = normalizeProjectRoot(workspaceRoot) | ||
| } | ||
| title = strings.TrimSpace(title) | ||
| if title == "" { | ||
| title = defaultTopicTitle | ||
| } | ||
| source = strings.TrimSpace(source) | ||
| if source == "" { | ||
| source = topicTitleSourceManual | ||
| } | ||
| if err := setTopicTitleWithSource(workspaceRoot, topicID, title, source); err != nil { | ||
| return err | ||
| } | ||
| f := loadProjectsFile() | ||
| if workspaceRoot == "" { | ||
| f.GlobalTopics = prependUniqueString(f.GlobalTopics, topicID) | ||
| return saveProjectsFile(f) | ||
| } | ||
| for i, p := range f.Projects { | ||
| if p.Root == workspaceRoot { | ||
| f.Projects[i].Topics = prependUniqueString(p.Topics, topicID) | ||
|
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.
When this recovery path runs for an existing project topic, Useful? React with 👍 / 👎. |
||
| return saveProjectsFile(f) | ||
| } | ||
| } | ||
| f.Projects = append(f.Projects, desktopProject{Root: workspaceRoot, Topics: []string{topicID}}) | ||
| return saveProjectsFile(f) | ||
| } | ||
|
|
||
| // --- telemetry -------------------------------------------------------------- | ||
|
|
||
| func (a *App) tabTelemetryPath(tabID string) string { | ||
|
|
@@ -1734,9 +1786,58 @@ func (a *App) RenameTopic(topicID, title string) error { | |
| a.emitProjectTreeChanged() | ||
| return nil | ||
| } | ||
| if scope, workspaceRoot, ok := a.findTopicLocation(topicID); ok { | ||
| if err := ensureTopicIndexed(scope, workspaceRoot, topicID, trimmed, topicTitleSourceManual); err != nil { | ||
| return err | ||
| } | ||
| a.updateOpenTopicTitle(topicID, trimmed) | ||
| a.updateTopicSessionTitles(topicID, trimmed) | ||
| a.emitProjectTreeChanged() | ||
| return nil | ||
| } | ||
| return fmt.Errorf("topic %q not found", topicID) | ||
| } | ||
|
|
||
| func (a *App) findTopicLocation(topicID string) (string, string, bool) { | ||
| topicID = strings.TrimSpace(topicID) | ||
| if topicID == "" { | ||
| return "", "", false | ||
| } | ||
| a.mu.RLock() | ||
| for _, tab := range a.tabs { | ||
| if tab == nil || tab.TopicID != topicID { | ||
| continue | ||
| } | ||
| scope := tab.Scope | ||
| workspaceRoot := tab.WorkspaceRoot | ||
| a.mu.RUnlock() | ||
| if scope == "global" { | ||
| return "global", "", true | ||
| } | ||
| return "project", normalizeProjectRoot(workspaceRoot), true | ||
| } | ||
| a.mu.RUnlock() | ||
|
|
||
| infos, err := agent.ListSessions(config.SessionDir()) | ||
| if err != nil { | ||
| return "", "", false | ||
| } | ||
| for _, info := range infos { | ||
| if strings.TrimSpace(info.TopicID) != topicID { | ||
| continue | ||
| } | ||
| scope := strings.TrimSpace(info.Scope) | ||
| if scope == "" { | ||
| scope = "global" | ||
| } | ||
| if scope == "global" { | ||
| return "global", "", true | ||
| } | ||
| return "project", normalizeProjectRoot(info.WorkspaceRoot), true | ||
| } | ||
| return "", "", false | ||
| } | ||
|
|
||
| func (a *App) updateOpenTopicTitle(topicID, title string) { | ||
| if strings.TrimSpace(topicID) == "" || strings.TrimSpace(title) == "" { | ||
| return | ||
|
|
||
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.
This catches every
RenameTopicfailure, not just the stale-topic case mentioned in the comment. If the backend fails to write the title or projects file because of permissions, disk space, or a config I/O error, the edit mode is dismissed and the draft is discarded without refreshing or telling the user that the rename did not persist.Useful? React with 👍 / 👎.