Skip to content

Commit f574e6d

Browse files
authored
refactor: Session management refactor (#810)
🧹 🧹 🧹 🧹 🧹 🧹 🧹 🧹 🧹 🧹 - Process queued messages after turn completes - send all as one prompt (like claude code) - Refactor session management into service, the store now only holds state and thin actions - Removes legacy isCloud stuff, many other small improvements SOLID ✅
1 parent 90c12d4 commit f574e6d

File tree

29 files changed

+2752
-1966
lines changed

29 files changed

+2752
-1966
lines changed

apps/twig/src/main/services/posthog-analytics.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,7 @@ export function initializePostHog() {
2424
}
2525

2626
export function withTeamContext<T>(fn: () => T): T {
27-
if (!posthogClient) {
28-
return fn();
29-
}
30-
return posthogClient.withContext({ properties: { team: "twig" } }, fn);
27+
return fn();
3128
}
3229

3330
export function setCurrentUserId(userId: string | null) {

apps/twig/src/renderer/di/container.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import "reflect-metadata";
2+
import { TaskService } from "@features/task-detail/service/service";
23
import type { TrpcRouter } from "@main/trpc/router.js";
34
import { trpcVanilla } from "@renderer/trpc";
45
import type { TRPCClient } from "@trpc/client";
56
import { Container } from "inversify";
6-
import { TaskService } from "../services/task/service";
77
import { RENDERER_TOKENS } from "./tokens";
88

99
/**

apps/twig/src/renderer/features/auth/stores/authStore.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { PostHogAPIClient } from "@api/posthogClient";
2+
import { resetSessionService } from "@features/sessions/service/service";
23
import { identifyUser, resetUser, track } from "@renderer/lib/analytics";
34
import { electronStorage } from "@renderer/lib/electronStorage";
45
import { logger } from "@renderer/lib/logger";
@@ -701,6 +702,9 @@ export const useAuthStore = create<AuthState>()(
701702
track(ANALYTICS_EVENTS.USER_LOGGED_OUT);
702703
resetUser();
703704

705+
// Clean up session service subscriptions before clearing auth state
706+
resetSessionService();
707+
704708
trpcVanilla.analytics.resetUser.mutate();
705709

706710
if (refreshTimeoutId) {

apps/twig/src/renderer/features/message-editor/components/MessageEditor.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ export const MessageEditor = forwardRef<EditorHandle, MessageEditorProps>(
6060
const taskId = context?.taskId;
6161
const disabled = context?.disabled ?? false;
6262
const isLoading = context?.isLoading ?? false;
63-
const isCloud = context?.isCloud ?? false;
6463
const repoPath = context?.repoPath;
6564
const isDisabled = disabled || !isOnline;
6665

@@ -83,7 +82,6 @@ export const MessageEditor = forwardRef<EditorHandle, MessageEditorProps>(
8382
placeholder,
8483
disabled: isDisabled,
8584
isLoading,
86-
isCloud,
8785
autoFocus,
8886
context: { taskId, repoPath },
8987
onSubmit,

apps/twig/src/renderer/features/message-editor/stores/draftStore.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export interface EditorContext {
1313
repoPath: string | null | undefined;
1414
disabled: boolean;
1515
isLoading: boolean;
16-
isCloud: boolean;
1716
}
1817

1918
interface DraftState {
@@ -81,15 +80,13 @@ export const useDraftStore = create<DraftStore>()(
8180
repoPath: context.repoPath ?? existing?.repoPath,
8281
disabled: context.disabled ?? existing?.disabled ?? false,
8382
isLoading: context.isLoading ?? existing?.isLoading ?? false,
84-
isCloud: context.isCloud ?? existing?.isCloud ?? false,
8583
};
8684
if (
8785
existing?.sessionId === newContext.sessionId &&
8886
existing?.taskId === newContext.taskId &&
8987
existing?.repoPath === newContext.repoPath &&
9088
existing?.disabled === newContext.disabled &&
91-
existing?.isLoading === newContext.isLoading &&
92-
existing?.isCloud === newContext.isCloud
89+
existing?.isLoading === newContext.isLoading
9390
) {
9491
return;
9592
}

apps/twig/src/renderer/features/message-editor/tiptap/useTiptapEditor.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { getSessionActions } from "@features/sessions/stores/sessionStore";
1+
import { sessionStoreSetters } from "@features/sessions/stores/sessionStore";
22
import { trpcVanilla } from "@renderer/trpc/client";
33
import { toast } from "@renderer/utils/toast";
44
import { useSettingsStore } from "@stores/settingsStore";
@@ -16,7 +16,6 @@ export interface UseTiptapEditorOptions {
1616
placeholder?: string;
1717
disabled?: boolean;
1818
isLoading?: boolean;
19-
isCloud?: boolean;
2019
autoFocus?: boolean;
2120
context?: DraftContext;
2221
capabilities?: {
@@ -43,7 +42,6 @@ export function useTiptapEditor(options: UseTiptapEditorOptions) {
4342
placeholder = "",
4443
disabled = false,
4544
isLoading = false,
46-
isCloud = false,
4745
autoFocus = false,
4846
context,
4947
capabilities = {},
@@ -153,7 +151,7 @@ export function useTiptapEditor(options: UseTiptapEditorOptions) {
153151

154152
if (event.key === "ArrowUp" && (isEmpty || isAtStart)) {
155153
const queuedContent =
156-
getSessionActions().popQueuedMessagesAsText(taskId);
154+
sessionStoreSetters.dequeueMessagesAsText(taskId);
157155
if (queuedContent !== null && queuedContent !== undefined) {
158156
event.preventDefault();
159157
view.dispatch(
@@ -351,10 +349,6 @@ export function useTiptapEditor(options: UseTiptapEditorOptions) {
351349
const text = editor.getText().trim();
352350

353351
if (text.startsWith("!")) {
354-
if (isCloud) {
355-
toast.error("Bash mode is not supported in cloud sessions");
356-
return;
357-
}
358352
// Bash mode requires immediate execution, can't be queued
359353
if (isLoading) {
360354
toast.error("Cannot run shell commands while agent is generating");
@@ -372,7 +366,7 @@ export function useTiptapEditor(options: UseTiptapEditorOptions) {
372366
prevBashModeRef.current = false;
373367
draft.clearDraft();
374368
}
375-
}, [editor, disabled, isLoading, isCloud, draft, clearOnSubmit]);
369+
}, [editor, disabled, isLoading, draft, clearOnSubmit]);
376370

377371
submitRef.current = submit;
378372

apps/twig/src/renderer/features/sessions/components/ConversationView.tsx

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import type {
55
import {
66
usePendingPermissionsForTask,
77
useQueuedMessagesForTask,
8-
useSessionActions,
98
} from "@features/sessions/stores/sessionStore";
109
import { useSessionViewActions } from "@features/sessions/stores/sessionViewStore";
1110
import type { SessionUpdate, ToolCall } from "@features/sessions/types";
@@ -61,7 +60,6 @@ interface ConversationViewProps {
6160
isPromptPending: boolean;
6261
promptStartedAt?: number | null;
6362
repoPath?: string | null;
64-
isCloud?: boolean;
6563
taskId?: string;
6664
}
6765

@@ -73,7 +71,6 @@ export function ConversationView({
7371
isPromptPending,
7472
promptStartedAt,
7573
repoPath,
76-
isCloud = false,
7774
taskId,
7875
}: ConversationViewProps) {
7976
const scrollRef = useRef<HTMLDivElement>(null);
@@ -84,7 +81,6 @@ export function ConversationView({
8481
const pendingPermissionsCount = pendingPermissions.size;
8582

8683
const queuedMessages = useQueuedMessagesForTask(taskId);
87-
const { removeQueuedMessage } = useSessionActions();
8884
const { saveScrollPosition, getScrollPosition } = useSessionViewActions();
8985

9086
const prevItemsLengthRef = useRef(0);
@@ -160,22 +156,13 @@ export function ConversationView({
160156
<div className="flex flex-col gap-3">
161157
{items.map((item) =>
162158
item.type === "turn" ? (
163-
<TurnView
164-
key={item.id}
165-
turn={item}
166-
repoPath={repoPath}
167-
isCloud={isCloud}
168-
/>
159+
<TurnView key={item.id} turn={item} repoPath={repoPath} />
169160
) : (
170161
<UserShellExecuteView key={item.id} item={item} />
171162
),
172163
)}
173164
{queuedMessages.map((msg) => (
174-
<QueuedMessageView
175-
key={msg.id}
176-
message={msg}
177-
onRemove={() => taskId && removeQueuedMessage(taskId, msg.id)}
178-
/>
165+
<QueuedMessageView key={msg.id} message={msg} />
179166
))}
180167
</div>
181168
<SessionFooter
@@ -205,7 +192,6 @@ export function ConversationView({
205192
interface TurnViewProps {
206193
turn: Turn;
207194
repoPath?: string | null;
208-
isCloud?: boolean;
209195
}
210196

211197
function getInterruptMessage(reason?: string): string {
@@ -217,11 +203,7 @@ function getInterruptMessage(reason?: string): string {
217203
}
218204
}
219205

220-
const TurnView = memo(function TurnView({
221-
turn,
222-
repoPath,
223-
isCloud = false,
224-
}: TurnViewProps) {
206+
const TurnView = memo(function TurnView({ turn, repoPath }: TurnViewProps) {
225207
const wasCancelled = turn.stopReason === "cancelled";
226208
const gitAction = parseGitActionMessage(turn.userContent);
227209
const showGitResult =
@@ -271,7 +253,6 @@ const TurnView = memo(function TurnView({
271253
actionType={gitAction.actionType}
272254
repoPath={repoPath}
273255
turnId={turn.id}
274-
isCloud={isCloud}
275256
/>
276257
)}
277258
{wasCancelled && (

apps/twig/src/renderer/features/sessions/components/GitActionResult.tsx

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,50 +13,33 @@ interface GitActionResultProps {
1313
actionType: GitActionType;
1414
repoPath: string;
1515
turnId: string;
16-
isCloud?: boolean;
1716
}
1817

1918
export function GitActionResult({
2019
actionType,
2120
repoPath,
2221
turnId,
23-
isCloud = false,
2422
}: GitActionResultProps) {
25-
const canQueryGit = !isCloud && !!repoPath;
26-
2723
const { data: commitInfo } = useQuery({
2824
queryKey: ["git-latest-commit", repoPath, turnId],
2925
queryFn: () =>
3026
trpcVanilla.git.getLatestCommit.query({ directoryPath: repoPath }),
31-
enabled: canQueryGit,
27+
enabled: !!repoPath,
3228
staleTime: 0,
3329
});
3430

3531
const { data: repoInfo } = useQuery({
3632
queryKey: ["git-repo-info", repoPath, turnId],
3733
queryFn: () =>
3834
trpcVanilla.git.getGitRepoInfo.query({ directoryPath: repoPath }),
39-
enabled: canQueryGit,
35+
enabled: !!repoPath,
4036
staleTime: 30000,
4137
});
4238

4339
const handleOpenUrl = (url: string) => {
4440
trpcVanilla.os.openExternal.mutate({ url });
4541
};
4642

47-
if (isCloud) {
48-
return (
49-
<Box className="mt-3 rounded-lg border border-green-6 bg-green-2 p-3">
50-
<Flex align="center" gap="2">
51-
<CheckCircle size={16} weight="fill" className="text-green-9" />
52-
<Text size="2" weight="medium" className="text-green-11">
53-
{getCompletionLabel(actionType)}
54-
</Text>
55-
</Flex>
56-
</Box>
57-
);
58-
}
59-
6043
const showCommit = commitInfo != null;
6144
const showPrLink = repoInfo?.compareUrl != null;
6245

apps/twig/src/renderer/features/sessions/components/ModelSelector.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { SessionConfigSelectGroup } from "@agentclientprotocol/sdk";
22
import { Select, Text } from "@radix-ui/themes";
33
import { Fragment, useMemo } from "react";
4+
import { getSessionService } from "../service/service";
45
import {
56
flattenSelectOptions,
67
useModelConfigOptionForTask,
7-
useSessionActions,
88
useSessionForTask,
99
} from "../stores/sessionStore";
1010

@@ -21,7 +21,6 @@ export function ModelSelector({
2121
onModelChange,
2222
adapter: _adapter,
2323
}: ModelSelectorProps) {
24-
const { setSessionConfigOption } = useSessionActions();
2524
const session = useSessionForTask(taskId);
2625
const modelOption = useModelConfigOptionForTask(taskId);
2726

@@ -39,8 +38,8 @@ export function ModelSelector({
3938
const handleChange = (value: string) => {
4039
onModelChange?.(value);
4140

42-
if (taskId && session?.status === "connected" && !session.isCloud) {
43-
setSessionConfigOption(taskId, modelOption.id, value);
41+
if (taskId && session?.status === "connected") {
42+
getSessionService().setSessionConfigOption(taskId, modelOption.id, value);
4443
}
4544
};
4645

apps/twig/src/renderer/features/sessions/components/ReasoningLevelSelector.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Select, Text } from "@radix-ui/themes";
2+
import { getSessionService } from "../service/service";
23
import {
34
flattenSelectOptions,
4-
useSessionActions,
55
useSessionForTask,
66
useThoughtLevelConfigOptionForTask,
77
} from "../stores/sessionStore";
@@ -15,7 +15,6 @@ export function ReasoningLevelSelector({
1515
taskId,
1616
disabled,
1717
}: ReasoningLevelSelectorProps) {
18-
const { setSessionConfigOption } = useSessionActions();
1918
const session = useSessionForTask(taskId);
2019
const thoughtOption = useThoughtLevelConfigOptionForTask(taskId);
2120

@@ -30,8 +29,12 @@ export function ReasoningLevelSelector({
3029
options.find((opt) => opt.value === activeLevel)?.name ?? activeLevel;
3130

3231
const handleChange = (value: string) => {
33-
if (taskId && session?.status === "connected" && !session.isCloud) {
34-
setSessionConfigOption(taskId, thoughtOption.id, value);
32+
if (taskId && session?.status === "connected") {
33+
getSessionService().setSessionConfigOption(
34+
taskId,
35+
thoughtOption.id,
36+
value,
37+
);
3538
}
3639
};
3740

0 commit comments

Comments
 (0)