Skip to content

Commit 54e480d

Browse files
authored
Merge pull request #9502 from gitbutlerapp/feed-factory-refactor
refactor: introduce FeedFactory for per-project feeds
2 parents e23290e + 426bdf8 commit 54e480d

File tree

5 files changed

+60
-16
lines changed

5 files changed

+60
-16
lines changed

apps/desktop/src/components/Feed.svelte

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import { invoke } from '$lib/backend/ipc';
99
import { SettingsService } from '$lib/config/appSettingsV2';
1010
import { persistedChatModelName, projectAiGenEnabled } from '$lib/config/config';
11-
import { Feed } from '$lib/feed/feed';
11+
import FeedFactory from '$lib/feed/feed';
1212
import { newProjectSettingsPath } from '$lib/routes/routes.svelte';
1313
import { User } from '$lib/user/user';
1414
import { getContext, getContextStore } from '@gitbutler/shared/context';
@@ -29,15 +29,16 @@
2929
3030
const { projectId, onCloseClick }: Props = $props();
3131
32-
const feed = getContext(Feed);
32+
const feedFactory = getContext(FeedFactory);
33+
const feed = $derived(feedFactory.getFeed(projectId));
3334
const actionService = getContext(ActionService);
3435
const user = getContextStore(User);
3536
const settingsService = getContext(SettingsService);
3637
const settingsStore = $derived(settingsService.appSettings);
3738
3839
const isAdmin = $derived($user.role === 'admin');
39-
const combinedEntries = feed.combined;
40-
const lastAddedId = feed.lastAddedId;
40+
const combinedEntries = $derived(feed.combined);
41+
const lastAddedId = $derived(feed.lastAddedId);
4142
4243
const MODELS = ['gpt-4.1', 'gpt-4.1-mini'] as const;
4344

apps/desktop/src/components/FeedStreamMessage.svelte

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import FeedItemKind from '$components/FeedItemKind.svelte';
3-
import { Feed, type InProgressAssistantMessage } from '$lib/feed/feed';
3+
import FeedFactory, { type InProgressAssistantMessage } from '$lib/feed/feed';
44
import { getContext } from '@gitbutler/shared/context';
55
import Markdown from '@gitbutler/ui/markdown/Markdown.svelte';
66
import type { ToolCall } from '$lib/ai/tool';
@@ -12,7 +12,8 @@
1212
1313
const { projectId, message }: Props = $props();
1414
15-
const feed = getContext(Feed);
15+
const feedFactory = getContext(FeedFactory);
16+
const feed = $derived(feedFactory.getFeed(projectId));
1617
let toolCalls = $state<ToolCall[]>(message.toolCalls);
1718
let messageContent = $state(message.content);
1819
const messageContentLines = $derived(messageContent.split('\n'));

apps/desktop/src/lib/feed/feed.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,36 @@ import type { ToolCall } from '$lib/ai/tool';
99
import type { Tauri } from '$lib/backend/tauri';
1010
import type { StackService } from '$lib/stacks/stackService.svelte';
1111

12+
export default class FeedFactory {
13+
private instance: Feed | null = null;
14+
15+
constructor(
16+
private tauri: Tauri,
17+
private stackService: StackService
18+
) {}
19+
20+
/**
21+
* Gets or creates a Feed instance for the given project ID.
22+
*
23+
* If an instance already exists for the project ID, it returns that instance
24+
*
25+
* If the instance exists but is for a different project ID, it unsubscribes from the previous instance
26+
* and creates a new instance for the new project ID.
27+
*/
28+
getFeed(projectId: string): Feed {
29+
if (!this.instance) {
30+
this.instance = new Feed(this.tauri, projectId, this.stackService);
31+
}
32+
33+
if (!this.instance.isProjectFeed(projectId)) {
34+
this.instance.unlisten();
35+
this.instance = new Feed(this.tauri, projectId, this.stackService);
36+
}
37+
38+
return this.instance;
39+
}
40+
}
41+
1242
type DBEvent = {
1343
kind: 'actions' | 'workflows' | 'hunk-assignments' | 'unknown';
1444
item?: string;
@@ -86,13 +116,13 @@ export type InProgressUpdate = TokenUpdate | ToolCallUpdate;
86116

87117
type InProgressSubscribeCallback = (update: InProgressUpdate) => void;
88118

89-
export class Feed {
90-
private actionsBuffer: ButlerAction[] = [];
91-
private workflowsBuffer: Workflow[] = [];
119+
class Feed {
120+
private actionsBuffer: ButlerAction[];
121+
private workflowsBuffer: Workflow[];
92122
private unlistenDB: () => void;
93123
private unlistenTokens: () => void;
94124
private unlistenToolCalls: () => void;
95-
private initialized = false;
125+
private initialized;
96126
private mutex = new Mutex();
97127
private updateTimeout: ReturnType<typeof setTimeout> | null = null;
98128
private messageSubscribers: Map<InProgressAssistantMessageId, InProgressSubscribeCallback[]>;
@@ -108,7 +138,10 @@ export class Feed {
108138
private projectId: string,
109139
private stackService: StackService
110140
) {
141+
this.actionsBuffer = [];
142+
this.workflowsBuffer = [];
111143
this.messageSubscribers = new Map();
144+
this.initialized = false;
112145

113146
this.unlistenDB = this.tauri.listen<DBEvent>(`project://${projectId}/db-updates`, (event) => {
114147
this.handleDBEvent(event.payload);
@@ -129,6 +162,10 @@ export class Feed {
129162
);
130163
}
131164

165+
isProjectFeed(projectId: string): boolean {
166+
return this.projectId === projectId;
167+
}
168+
132169
subscribeToMessage(
133170
messageId: InProgressAssistantMessageId,
134171
callback: InProgressSubscribeCallback

apps/desktop/src/routes/+layout.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import DependencyService from '$lib/dependencies/dependencyService.svelte';
3232
import { DragStateService } from '$lib/dragging/dragStateService.svelte';
3333
import { DropzoneRegistry } from '$lib/dragging/registry';
34+
import FeedFactory from '$lib/feed/feed';
3435
import { FileService } from '$lib/files/fileService';
3536
import { UncommitedFilesWatcher } from '$lib/files/watcher';
3637
import { DefaultForgeFactory } from '$lib/forge/forgeFactory.svelte';
@@ -148,6 +149,7 @@
148149
forgeFactory,
149150
uiState
150151
);
152+
const feedFactory = new FeedFactory(data.tauri, stackService);
151153
const rulesService = new RulesService(clientState['backendApi']);
152154
const actionService = new ActionService(clientState['backendApi']);
153155
const oplogService = new OplogService(clientState['backendApi']);
@@ -226,6 +228,7 @@
226228
setContext(AppSettings, data.appSettings);
227229
setContext(EventContext, data.eventContext);
228230
setContext(StackService, stackService);
231+
setContext(FeedFactory, feedFactory);
229232
setContext(RulesService, rulesService);
230233
setContext(ActionService, actionService);
231234
setContext(OplogService, oplogService);

apps/desktop/src/routes/[projectId]/+layout.svelte

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import { SettingsService } from '$lib/config/appSettingsV2';
1919
import { showHistoryView } from '$lib/config/config';
2020
import { StackingReorderDropzoneManagerFactory } from '$lib/dragging/stackingReorderDropzoneManager';
21-
import { Feed } from '$lib/feed/feed';
21+
import FeedFactory from '$lib/feed/feed';
2222
import { FocusManager } from '$lib/focus/focusManager.svelte';
2323
import { DefaultForgeFactory } from '$lib/forge/forgeFactory.svelte';
2424
import { GitHubClient } from '$lib/forge/github/githubClient';
@@ -43,7 +43,7 @@
4343
4444
const { data, children: pageChildren }: { data: LayoutData; children: Snippet } = $props();
4545
46-
const { projectId, userService, posthog, projectMetrics, tauri } = $derived(data);
46+
const { projectId, userService, posthog, projectMetrics } = $derived(data);
4747
4848
const baseBranchService = getContext(BaseBranchService);
4949
const repoInfoResponse = $derived(baseBranchService.repo(projectId));
@@ -56,7 +56,7 @@
5656
const branchService = getContext(BranchService);
5757
5858
const stackService = getContext(StackService);
59-
const feed = $derived(new Feed(tauri, projectId, stackService));
59+
const feedFactory = getContext(FeedFactory);
6060
const modeService = $derived(new ModeService(projectId, stackService));
6161
$effect.pre(() => {
6262
setContext(ModeService, modeService);
@@ -95,9 +95,6 @@
9595
$effect.pre(() => {
9696
setContext(HistoryService, data.historyService);
9797
setContext(BaseBranch, baseBranch);
98-
99-
// Cloud related services
100-
setContext(Feed, feed);
10198
});
10299
103100
const focusManager = new FocusManager();
@@ -280,6 +277,11 @@
280277
$effect(() => {
281278
stackService.stackDetailsUpdateListener(projectId);
282279
});
280+
281+
// Listen for the feed updates from the backend.
282+
$effect(() => {
283+
feedFactory.getFeed(projectId);
284+
});
283285
</script>
284286

285287
<ProjectSettingsMenuAction {projectId} />

0 commit comments

Comments
 (0)