Skip to content

Commit eaffc7c

Browse files
fix(issues): address review — chat claim race, soft-deleted chats, collab flag gate, replay-safe migration, Sim review handoff
1 parent 5fbc0d2 commit eaffc7c

36 files changed

Lines changed: 595 additions & 101 deletions

File tree

‎apps/sim/app/api/mothership/chats/[chatId]/route.ts‎

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from '@/lib/api/contracts/mothership-chats'
1212
import { parseRequest } from '@/lib/api/server'
1313
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
14+
import { detachChatFromIssuesInTx } from '@/lib/issues/repository'
1415
import { buildEffectiveChatTranscript } from '@/lib/mothership/chat/effective-transcript'
1516
import {
1617
getAccessibleCopilotChatAuth,
@@ -239,21 +240,25 @@ export const DELETE = withRouteHandler(
239240
return NextResponse.json({ success: true })
240241
}
241242

242-
const [deletedChat] = await db
243-
.update(copilotChats)
244-
.set({ deletedAt: new Date() })
245-
.where(
246-
and(
247-
eq(copilotChats.id, chatId),
248-
eq(copilotChats.userId, userId),
249-
eq(copilotChats.type, 'mothership'),
250-
isNull(copilotChats.deletedAt)
243+
const deletedChat = await db.transaction(async (tx) => {
244+
const [row] = await tx
245+
.update(copilotChats)
246+
.set({ deletedAt: new Date() })
247+
.where(
248+
and(
249+
eq(copilotChats.id, chatId),
250+
eq(copilotChats.userId, userId),
251+
eq(copilotChats.type, 'mothership'),
252+
isNull(copilotChats.deletedAt)
253+
)
251254
)
252-
)
253-
.returning({
254-
workspaceId: copilotChats.workspaceId,
255-
organizationId: copilotChats.organizationId,
256-
})
255+
.returning({
256+
workspaceId: copilotChats.workspaceId,
257+
organizationId: copilotChats.organizationId,
258+
})
259+
if (row) await detachChatFromIssuesInTx(tx, chatId)
260+
return row
261+
})
257262

258263
if (!deletedChat) {
259264
return NextResponse.json({ success: false, error: 'Chat not found' }, { status: 404 })

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.tsx‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,14 +248,24 @@ interface RichMarkdownEditorProps {
248248
/** Source fallback unmounts the rich surface so only one editing engine owns the local draft. */
249249
export const RichMarkdownEditor = memo(function RichMarkdownEditor(props: RichMarkdownEditorProps) {
250250
const [sourceFileId, setSourceFileId] = useState<string | null>(null)
251-
if (sourceFileId === props.file.id)
252-
return (
251+
if (sourceFileId === props.file.id) {
252+
const source = (
253253
<TextEditor
254254
{...props}
255255
previewMode='editor'
256256
disableStreamingAutoScroll={props.disableStreamingAutoScroll ?? false}
257257
/>
258258
)
259+
if (!props.header && !props.footer) return source
260+
// The source editor scrolls itself, so the surrounding chrome scrolls around a fixed-height editor.
261+
return (
262+
<div className='flex min-h-0 flex-1 flex-col overflow-y-auto'>
263+
{props.header}
264+
<div className='flex h-[60vh] min-h-[320px] shrink-0 flex-col'>{source}</div>
265+
{props.footer}
266+
</div>
267+
)
268+
}
259269
return <RichMarkdownSurface {...props} onEditSource={() => setSourceFileId(props.file.id)} />
260270
})
261271

‎apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ const RESOURCE_TO_CONTEXT: Record<
133133
...(r.viewId ? { viewId: r.viewId } : {}),
134134
}),
135135
dashboard: (r) => ({ kind: 'dashboard', dashboardId: r.id, label: r.title }),
136-
/** Sim reads an issue through its document path, `issues/<KEY>.md`, not a chat context. */
136+
/** No chat context: Sim reads an issue by key or as `issues/<KEY>.md`, so the attach menu omits issues. */
137137
issue: () => null,
138138
file: (r) => ({ kind: 'file', fileId: r.id, label: r.title }),
139139
folder: (r) => ({ kind: 'folder', folderId: r.id, label: r.title }),

‎apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown/plus-menu-dropdown.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ import { useSettledTerminalCommands } from '@/hooks/use-settled-terminal-command
4949
import { useBrowserSessionStore } from '@/stores/browser-session/store'
5050
import { useCopilotTerminalStore } from '@/stores/copilot-terminal/store'
5151

52+
/** Resource types with no chat context to attach (see `mapResourceToContext`). */
53+
const CHAT_CONTEXTLESS_RESOURCE_TYPES: readonly MothershipResourceType[] = ['issue']
54+
5255
/**
5356
* The `@` list is shorter than the emcn menu default (420px, sized for right-click
5457
* action menus). This one floats directly over the chat input, so a menu tall enough
@@ -154,6 +157,7 @@ export const PlusMenuDropdown = React.memo(
154157
const workspaceInventory = useAvailableResources(organizationId ? '' : workspaceId, {
155158
enabled: inventoryEnabled,
156159
includeFolderMentions: true,
160+
excludeTypes: CHAT_CONTEXTLESS_RESOURCE_TYPES,
157161
})
158162
const { data: allWorkspaces, isPending: workspacesPending } = useOrderedWorkspacesQuery(
159163
Boolean(organizationId) && inventoryEnabled

‎apps/sim/app/workspace/[workspaceId]/issues/[key]/page.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ interface IssuePageProps {
99

1010
export default async function IssuePage({ params }: IssuePageProps) {
1111
const { workspaceId, key } = await params
12-
return <IssueDetail workspaceId={workspaceId} issueKey={decodeURIComponent(key)} />
12+
return <IssueDetail workspaceId={workspaceId} issueKey={key} />
1313
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export { IssueStatusIcon } from './issue-status'
1+
export { IssueStatusIcon, issueStatusLabel } from './issue-status'
22
export { IssuesFeatureGate } from './issues-feature-gate'
33
export { NewIssueModal } from './new-issue-modal'

‎apps/sim/app/workspace/[workspaceId]/issues/components/issue-activity.tsx‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,12 @@ export function IssueActivity({
114114
)
115115
}
116116

117-
function IssueChange({ event, names }: { event: IssueEvent; names: Map<string, string> }) {
117+
interface IssueChangeProps {
118+
event: IssueEvent
119+
names: Map<string, string>
120+
}
121+
122+
function IssueChange({ event, names }: IssueChangeProps) {
118123
const { what, detail } = describe(event)
119124
return (
120125
<li className='flex flex-col py-1 text-caption'>
@@ -170,7 +175,12 @@ function IssueComment({ workspaceId, issueKey, event, author }: IssueCommentProp
170175
)
171176
}
172177

173-
function CommentComposer({ workspaceId, issueKey }: { workspaceId: string; issueKey: string }) {
178+
interface CommentComposerProps {
179+
workspaceId: string
180+
issueKey: string
181+
}
182+
183+
function CommentComposer({ workspaceId, issueKey }: CommentComposerProps) {
174184
const [body, setBody] = useState('')
175185
const addComment = useAddIssueComment()
176186
const submit = () => {

‎apps/sim/app/workspace/[workspaceId]/issues/components/issue-detail.tsx‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,11 @@ export function IssueView({ workspaceId, issueKey }: IssueDetailProps) {
164164
}
165165

166166
/** Key, who filed it, and when, above the title. */
167-
function IssueEyebrow({ issue }: { issue: IssueRecord }) {
167+
interface IssueEyebrowProps {
168+
issue: IssueRecord
169+
}
170+
171+
function IssueEyebrow({ issue }: IssueEyebrowProps) {
168172
return (
169173
<p className='text-[var(--text-muted)] text-caption'>
170174
{[issue.key, filedBy(issue), dateFormat.format(new Date(issue.createdAt))].join(' · ')}
@@ -332,12 +336,16 @@ function IssueActions({ workspaceId, issue, canEdit, chatHref }: IssueActionsPro
332336
*/
333337
async function startChat(followUp: boolean) {
334338
setOpening(true)
339+
let started = false
335340
try {
336341
const chat = await requestJson(createMothershipChatContract, { body: { workspaceId } })
337-
if (!followUp) await startIssue.mutateAsync({ workspaceId, key: issue.key, chatId: chat.id })
338342
await requestJson(addMothershipChatResourceContract, {
339343
body: { chatId: chat.id, resource: { type: 'issue', id: issue.key, title: issue.title } },
340344
})
345+
if (!followUp) {
346+
await startIssue.mutateAsync({ workspaceId, key: issue.key, chatId: chat.id })
347+
started = true
348+
}
341349
// boundary-raw-fetch: the chat send responds with an SSE stream; only its acceptance matters here, the run continues server-side once the reader is cancelled
342350
const response = await fetch(MOTHERSHIP_CHAT_API_PATH, {
343351
method: 'POST',
@@ -359,7 +367,11 @@ function IssueActions({ workspaceId, issue, canEdit, chatHref }: IssueActionsPro
359367
followUp ? `Started a follow-up on ${issue.key}` : `Sim is working on ${issue.key}`
360368
)
361369
} catch (error) {
362-
toast.error(getErrorMessage(error, 'Sim could not start on this issue'))
370+
toast.error(
371+
started
372+
? 'The chat is ready but its first message did not send. Open the chat to send it again.'
373+
: getErrorMessage(error, 'Sim could not start on this issue')
374+
)
363375
} finally {
364376
setOpening(false)
365377
}

‎apps/sim/app/workspace/[workspaceId]/issues/components/issue-properties.tsx‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ function LinkedResources({ workspaceId, detail, canEdit }: LinkedResourcesProps)
187187
key: detail.issue.key,
188188
type: changed.slice(0, separator) as IssueResourceType,
189189
resourceId: changed.slice(separator + 1),
190-
linked: Boolean(removed && !added),
190+
unlink: Boolean(removed && !added),
191191
})
192192
}}
193193
aria-label='Linked resources'
@@ -196,7 +196,11 @@ function LinkedResources({ workspaceId, detail, canEdit }: LinkedResourcesProps)
196196
}
197197

198198
/** Tickets in Linear or Jira; picking one opens it. */
199-
function LinkedTickets({ tickets }: { tickets: IssueDetail['tickets'] }) {
199+
interface LinkedTicketsProps {
200+
tickets: IssueDetail['tickets']
201+
}
202+
203+
function LinkedTickets({ tickets }: LinkedTicketsProps) {
200204
return (
201205
<ChipDropdown
202206
variant='ghost'

‎apps/sim/app/workspace/[workspaceId]/issues/components/issue-status.tsx‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import type { IssueRecord } from '@/lib/api/contracts/issues'
77
* thinking loader while its chat is running (paused when the chat stopped), and a check or cross
88
* once closed.
99
*/
10-
export function IssueStatusIcon({ issue }: { issue: IssueRecord }) {
10+
interface IssueStatusIconProps {
11+
issue: IssueRecord
12+
}
13+
14+
export function IssueStatusIcon({ issue }: IssueStatusIconProps) {
1115
if (issue.status === 'in_progress')
1216
return issue.workingChat?.running ? (
1317
<ThinkingLoader size={14} />
@@ -22,3 +26,18 @@ export function IssueStatusIcon({ issue }: { issue: IssueRecord }) {
2226
return <span aria-hidden='true' className='size-[8px] rounded-full bg-[var(--caution)]' />
2327
return <Circle className='size-[14px] text-[var(--text-icon)]' />
2428
}
29+
30+
const CLOSE_REASON_LABELS = {
31+
completed: 'Completed',
32+
dismissed: 'Dismissed',
33+
duplicate: 'Duplicate',
34+
} as const
35+
36+
/** The state {@link IssueStatusIcon} draws, in words, for assistive technology. */
37+
export function issueStatusLabel(issue: IssueRecord): string {
38+
if (issue.status === 'in_progress')
39+
return issue.workingChat?.running ? 'Sim is working' : 'In progress'
40+
if (issue.status === 'done')
41+
return issue.closeReason ? CLOSE_REASON_LABELS[issue.closeReason] : 'Done'
42+
return issue.inboxKind === 'review' ? 'Needs review' : 'New'
43+
}

0 commit comments

Comments
 (0)