| … | … | @@ -8077,6 +8146,19 @@ export default function ChatView(props: ChatViewProps) { | |
| 8151 | + | if (sideChatsAvailable && !directAnnotation && !composerHasNonPromptContent) { | |
| 8152 | + | const sideCommand = parseSideCommand(promptRef.current); | |
| 8153 | + | if (sideCommand !== null) { | |
| 8154 | + | promptRef.current = ""; | |
🟡 Medium components/ChatView.tsx:8154
A failed /side <question> or /btw <question> permanently discards the user's question, so it cannot be retried from the composer. This branch clears promptRef and the composer draft before runSideChatCommand completes, while useSideChatActions.start only reports the failure; preserve the draft until the side chat is created successfully and clear it only then.
🤖 Copy this AI Prompt to have your agent fix this:
| 1 | In file @apps/web/src/components/ChatView.tsx around line 8154: |
| 2 | |
| 3 | A failed `/side <question>` or `/btw <question>` permanently discards the user's question, so it cannot be retried from the composer. This branch clears `promptRef` and the composer draft before `runSideChatCommand` completes, while `useSideChatActions.start` only reports the failure; preserve the draft until the side chat is created successfully and clear it only then. |
| … | … | @@ -1174,6 +1187,7 @@ function OpenCommandPaletteDialog(props: { | |
| 1174 | 1187 | threads.filter( | |
| 1175 | 1188 | (thread) => | |
| 1176 | 1189 | thread.archivedAt === null && | |
| 1190 | + | !thread.sideChat && | |
🟡 Medium components/CommandPalette.tsx:1190
Selecting a grouped project with only a surviving orphaned side chat creates or navigates to a new thread instead of reopening that chat. The unconditional !thread.sideChat filter removes side chats even when their parent is deleted, although makeSideChatListingFilter treats those chats as ordinary reachable threads; use that same visibility predicate here.
🤖 Copy this AI Prompt to have your agent fix this:
| 1 | In file @apps/web/src/components/CommandPalette.tsx around line 1190: |
| 2 | |
| 3 | Selecting a grouped project with only a surviving orphaned side chat creates or navigates to a new thread instead of reopening that chat. The unconditional `!thread.sideChat` filter removes side chats even when their parent is deleted, although `makeSideChatListingFilter` treats those chats as ordinary reachable threads; use that same visibility predicate here. |
| … | … | @@ -3877,6 +3902,25 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | |
| 3917 | + | ); | |
| 3918 | + | if (applied) { | |
| 3919 | + | setComposerHighlightedItemId(null); | |
| 3920 | + | if (opensNow) void onSideChatCommand(""); | |
🟡 Medium chat/ChatComposer.tsx:3920
Selecting /side from the menu opens an empty side chat even when the composer contains attachments or context chips, leaving that non-prompt content stranded in the parent draft. This branch calls onSideChatCommand("") without applying the typed-command path's composerHasNonPromptContent guard; only take the immediate side-chat path when the composer has no non-prompt content, otherwise preserve /side as prompt text.
🤖 Copy this AI Prompt to have your agent fix this:
| 1 | In file @apps/web/src/components/chat/ChatComposer.tsx around line 3920: |
| 2 | |
| 3 | Selecting `/side` from the menu opens an empty side chat even when the composer contains attachments or context chips, leaving that non-prompt content stranded in the parent draft. This branch calls `onSideChatCommand("")` without applying the typed-command path's `composerHasNonPromptContent` guard; only take the immediate side-chat path when the composer has no non-prompt content, otherwise preserve `/side` as prompt text. |
| … | … | @@ -270,8 +273,9 @@ export function buildThreadActionItems<TThread extends BuildThreadActionItemsThr | |
| 270 | 273 | runThread: (thread: Pick<SidebarThreadSummary, "environmentId" | "id">) => Promise<void>; | |
| 271 | 274 | limit?: number; | |
| 272 | 275 | }): CommandPaletteActionItem[] { | |
| 276 | + | const listable = makeSideChatListingFilter(input.threads); | |
🟡 Medium components/CommandPalette.logic.ts:276
Archiving a parent hides its still-unarchived side chat from the command palette, leaving that chat unreachable unless its panel is already open. listable is built from all input.threads, so makeSideChatListingFilter still sees the archived parent as present even though the subsequent filter removes it; build the listing filter from unarchived threads as well.
| 1 | const listable = makeSideChatListingFilter(input.threads.filter((thread) => thread.archivedAt === null)); |
Also found in 1 other location(s)
apps/web/src/components/Sidebar.logic.ts:524
listable(thread)treats an archived parent as still present, even though this same filter excludes that parent viathread.archivedAt === null. Consequently, archiving a parent with an unarchived side chat hides both the parent and child from the sidebar (and the identical palette filter), leaving the surviving side chat unreachable unless its panel was already open. The presence set must exclude archived parents as well as deleted ones.
🤖 Copy this AI Prompt to have your agent fix this:
| 1 | In file @apps/web/src/components/CommandPalette.logic.ts around line 276: |
| 2 | |
| 3 | Archiving a parent hides its still-unarchived side chat from the command palette, leaving that chat unreachable unless its panel is already open. `listable` is built from all `input.threads`, so `makeSideChatListingFilter` still sees the archived parent as present even though the subsequent filter removes it; build the listing filter from unarchived threads as well. |
| 4 | |
| 5 | Also found in 1 other location(s): |
| 6 | - apps/web/src/components/Sidebar.logic.ts:524 -- `listable(thread)` treats an archived parent as still present, even though this same filter excludes that parent via `thread.archivedAt === null`. Consequently, archiving a parent with an unarchived side chat hides both the parent and child from the sidebar (and the identical palette filter), leaving the surviving side chat unreachable unless its panel was already open. The presence set must exclude archived parents as well as deleted ones. |
| … | … | @@ -2589,6 +2595,24 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) | |
| 2589 | 2595 | label: "/model", | |
| 2590 | 2596 | description: "Switch response model for this thread", | |
| 2591 | 2597 | }, | |
| 2598 | + | ...(props.onSideChatCommand | |
🟡 Medium chat/ChatComposer.tsx:2598
Selecting /btw after existing prompt text inserts the literal command instead of opening or sending to side chat, because parseSideCommand only recognizes prompts that begin with /btw. Restrict this menu entry to prompt-start triggers (and likewise /side), or handle selection as a side-chat action.
| 1 | - ...(props.onSideChatCommand |
| 2 | + ...(props.onSideChatCommand && composerTrigger.rangeStart === 0 |
🤖 Copy this AI Prompt to have your agent fix this:
| 1 | In file @apps/web/src/components/chat/ChatComposer.tsx around line 2598: |
| 2 | |
| 3 | Selecting `/btw` after existing prompt text inserts the literal command instead of opening or sending to side chat, because `parseSideCommand` only recognizes prompts that begin with `/btw`. Restrict this menu entry to prompt-start triggers (and likewise `/side`), or handle selection as a side-chat action. |
| … | … | @@ -11280,3 +11389,37 @@ export default function ChatView(props: ChatViewProps) { | |
| 11415 | + | return ( | |
| 11416 | + | <ComposerHandleContext value={composerRef}> | |
| 11417 | + | <ChatView | |
| 11418 | + | embedded | |
🟡 Medium components/ChatView.tsx:11418
A side-chat composer popup is treated as part of the host chat, so pressing Mod+S while focus is in the side chat's portaled ProviderModelPicker popup stashes the host draft (and can trigger host thread shortcuts) instead of suppressing the shortcut for the embedded chat. Because PopoverPrimitive.Portal moves the popup outside [data-chat-embedded], isInSameChatView sees no embedded ancestor and classifies the target as belonging to the host. Preserve the side-chat ownership on portaled composer popups—for example, by portal-mounting them in the embedded container or checking an explicit chat-owner marker—before applying host shortcuts.
Also found in 1 other location(s)
apps/web/src/components/chat/ChatComposer.tsx:5602
The
isInSameChatViewcheck treats a target outside[data-chat-embedded]as belonging to the host composer. The side chat'sProviderModelPickerrenders its focused popup throughPopoverPrimitive.Portal, so pressingMod+Swhile that popup is focused makes the host handler pass this line and stash the host draft; the side handler rejects it because its form is embedded. This can stash the wrong conversation whenever a side-chat composer popover has focus.
🤖 Copy this AI Prompt to have your agent fix this:
| 1 | In file @apps/web/src/components/ChatView.tsx around line 11418: |
| 2 | |
| 3 | A side-chat composer popup is treated as part of the host chat, so pressing `Mod+S` while focus is in the side chat's portaled `ProviderModelPicker` popup stashes the host draft (and can trigger host thread shortcuts) instead of suppressing the shortcut for the embedded chat. Because `PopoverPrimitive.Portal` moves the popup outside `[data-chat-embedded]`, `isInSameChatView` sees no embedded ancestor and classifies the target as belonging to the host. Preserve the side-chat ownership on portaled composer popups—for example, by portal-mounting them in the embedded container or checking an explicit chat-owner marker—before applying host shortcuts. |
| 4 | |
| 5 | Also found in 1 other location(s): |
| 6 | - apps/web/src/components/chat/ChatComposer.tsx:5602 -- The `isInSameChatView` check treats a target outside `[data-chat-embedded]` as belonging to the host composer. The side chat's `ProviderModelPicker` renders its focused popup through `PopoverPrimitive.Portal`, so pressing `Mod+S` while that popup is focused makes the host handler pass this line and stash the host draft; the side handler rejects it because its form is embedded. This can stash the wrong conversation whenever a side-chat composer popover has focus. |
| … | … | @@ -7612,8 +7679,10 @@ export default function ChatView(props: ChatViewProps) { | |
| 7612 | 7679 | pasteAsTextShortcutUntilRef.current = Date.now() + 1_000; | |
| 7613 | 7680 | } | |
| 7614 | 7681 | }; | |
| 7682 | + | if (embedded) return; | |
🟡 Medium components/ChatView.tsx:7682
Side chats no longer support paste-to-focus or type-to-focus after the composer blurs: text pasted or typed over the timeline is dropped instead of being inserted into and expanding the side-chat composer. The if (embedded) return disables the side chat's listener, while the host listener also rejects [data-chat-embedded] targets; allow the embedded view's own listener to process these inputs and keep the host from duplicating them.
🤖 Copy this AI Prompt to have your agent fix this:
| 1 | In file @apps/web/src/components/ChatView.tsx around line 7682: |
| 2 | |
| 3 | Side chats no longer support paste-to-focus or type-to-focus after the composer blurs: text pasted or typed over the timeline is dropped instead of being inserted into and expanding the side-chat composer. The `if (embedded) return` disables the side chat's listener, while the host listener also rejects `[data-chat-embedded]` targets; allow the embedded view's own listener to process these inputs and keep the host from duplicating them. |
| … | … | @@ -0,0 +1,123 @@ | |
| 64 | + | }; | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | const SETTLED_RUN_STATUSES = new Set(["completed", "failed", "interrupted", "cancelled"]); | |
🟡 Medium src/sideChat.logic.ts:67
A side chat whose latest run is rolled_back is treated as still running, so it remains in the active section, increments the running count, and sideChatActivity returns running instead of a finished status. Add rolled_back to SETTLED_RUN_STATUSES so terminal rolled-back runs follow the finished-run path.
| 1 | const SETTLED_RUN_STATUSES = new Set(["completed", "failed", "interrupted", "cancelled", "rolled_back"]); |
🤖 Copy this AI Prompt to have your agent fix this:
| 1 | In file @apps/web/src/sideChat.logic.ts around line 67: |
| 2 | |
| 3 | A side chat whose latest run is `rolled_back` is treated as still running, so it remains in the active section, increments the running count, and `sideChatActivity` returns `running` instead of a finished status. Add `rolled_back` to `SETTLED_RUN_STATUSES` so terminal rolled-back runs follow the finished-run path. |