Skip to content

Commit e16c575

Browse files
committed
Merge branch 'dev' of github.com:NeuralNomadsAI/CodeNomad into dev
2 parents eb67011 + 375f924 commit e16c575

8 files changed

Lines changed: 354 additions & 97 deletions

File tree

packages/ui/src/components/message-item.tsx

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { For, Show, createSignal } from "solid-js"
2-
import { Copy, Split, Trash2, Undo } from "lucide-solid"
2+
import { Copy, ExternalLink, Split, Trash2, Undo } from "lucide-solid"
33
import type { MessageInfo, ClientPart } from "../types/message"
44
import { partHasRenderableText } from "../types/message"
55
import type { MessageRecord } from "../stores/message-v2/types"
@@ -8,6 +8,7 @@ import { copyToClipboard } from "../lib/clipboard"
88
import { useI18n } from "../lib/i18n"
99
import { showAlertDialog } from "../stores/alerts"
1010
import { deleteMessagePart } from "../stores/session-actions"
11+
import { isTauriHost } from "../lib/runtime-env"
1112

1213
interface MessageItemProps {
1314
record: MessageRecord
@@ -45,6 +46,15 @@ export default function MessageItem(props: MessageItemProps) {
4546

4647
const messageParts = () => props.parts
4748

49+
// User messages can temporarily include synthetic helper parts (e.g. tool traces / file reads).
50+
// We only want to display the primary prompt text for the user message; other synthetic text
51+
// parts should be hidden.
52+
const primaryUserTextPartId = () => {
53+
if (!isUser()) return null
54+
const firstText = messageParts().find((part) => part?.type === "text") as { id?: string } | undefined
55+
return typeof firstText?.id === "string" ? firstText.id : null
56+
}
57+
4858
const fileAttachments = () =>
4959
messageParts().filter((part): part is FilePart => part?.type === "file" && typeof (part as FilePart).url === "string")
5060

@@ -96,7 +106,8 @@ export default function MessageItem(props: MessageItemProps) {
96106
}
97107

98108
if (url.startsWith("file://")) {
99-
window.open(url, "_blank", "noopener")
109+
// Local filesystem URLs are not reliably downloadable from the message stream.
110+
// We hide the download action for these chips.
100111
return
101112
}
102113

@@ -373,6 +384,7 @@ export default function MessageItem(props: MessageItemProps) {
373384
messageType={props.record.role}
374385
instanceId={props.instanceId}
375386
sessionId={props.sessionId}
387+
primaryUserTextPartId={primaryUserTextPartId()}
376388
onRendered={props.onContentRendered}
377389
/>
378390
)}
@@ -399,17 +411,20 @@ export default function MessageItem(props: MessageItemProps) {
399411
<img src={attachment.url} alt={name} class="h-5 w-5 rounded object-cover" />
400412
</Show>
401413
<span class="truncate max-w-[180px]">{name}</span>
402-
<button
403-
type="button"
404-
onClick={() => void handleAttachmentDownload(attachment)}
405-
class="attachment-download"
406-
aria-label={t("messageItem.attachment.downloadAriaLabel", { name })}
407-
>
408-
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
409-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2" />
410-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12l4 4 4-4m-4-8v12" />
411-
</svg>
412-
</button>
414+
<Show when={!attachment.url?.startsWith("file://")}>
415+
<button
416+
type="button"
417+
onClick={() => void handleAttachmentDownload(attachment)}
418+
class="attachment-download"
419+
aria-label={t("messageItem.attachment.downloadAriaLabel", { name })}
420+
title={t("messageItem.attachment.downloadAriaLabel", { name })}
421+
>
422+
<svg class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
423+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2" />
424+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12l4 4 4-4m-4-8v12" />
425+
</svg>
426+
</button>
427+
</Show>
413428

414429
<button
415430
type="button"

packages/ui/src/components/message-part.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@ interface MessagePartProps {
1313
messageType?: "user" | "assistant"
1414
instanceId: string
1515
sessionId: string
16+
// For user messages, keep the primary prompt text visible even when synthetic (optimistic).
17+
// Other synthetic text parts (tool traces, read outputs, etc.) should be hidden.
18+
primaryUserTextPartId?: string | null
1619
onRendered?: () => void
1720
}
18-
export default function MessagePart(props: MessagePartProps) {
21+
export default function MessagePart(props: MessagePartProps) {
1922

2023
const { isDark } = useTheme()
2124
const { preferences } = useConfig()
@@ -28,8 +31,19 @@ interface MessagePartProps {
2831
const shouldHideTextPart = () => {
2932
const part = props.part
3033
if (!part || part.type !== "text") return false
31-
// Keep optimistic user prompts visible; hide synthetic assistant text.
32-
return Boolean((part as any).synthetic) && props.messageType !== "user"
34+
35+
const isSynthetic = Boolean((part as any).synthetic)
36+
if (!isSynthetic) return false
37+
38+
// Keep optimistic user prompts visible; hide other synthetic user helper parts.
39+
if (props.messageType === "user") {
40+
const primaryId = props.primaryUserTextPartId
41+
if (!primaryId) return false
42+
return part.id !== primaryId
43+
}
44+
45+
// Hide synthetic assistant text.
46+
return true
3347
}
3448

3549

packages/ui/src/components/prompt-input/usePromptKeyDown.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,25 @@ export function usePromptKeyDown(options: UsePromptKeyDownOptions) {
183183

184184
if (isDeletingFromEnd || isDeletingFromStart || isSelected) {
185185
const currentAttachments = options.getAttachments()
186-
const attachment = currentAttachments.find(
187-
(a) => (a.source.type === "file" || a.source.type === "agent") && a.filename === name,
188-
)
186+
const attachment = currentAttachments.find((a) => {
187+
if (a.source.type === "agent") {
188+
return a.filename === name
189+
}
190+
if (a.source.type === "file") {
191+
// Match either by filename (basename) or by path (for full paths like @docs/file.txt)
192+
return (
193+
a.filename === name ||
194+
a.source.path === name ||
195+
a.source.path.endsWith("/" + name) ||
196+
a.source.path === name.replace(/\/$/, "")
197+
)
198+
}
199+
if (a.source.type === "text") {
200+
// For text attachments (path-only mentions), match by value
201+
return a.source.value === name || a.source.value.endsWith("/" + name)
202+
}
203+
return false
204+
})
189205

190206
if (attachment) {
191207
e.preventDefault()
@@ -205,6 +221,14 @@ export function usePromptKeyDown(options: UsePromptKeyDownOptions) {
205221
textarea.setSelectionRange(mentionStart, mentionStart)
206222
}, 0)
207223

224+
// Check if there are any @ remaining in the text - if not, close the picker
225+
if (!newText.includes("@") && options.isPickerOpen()) {
226+
options.closePicker()
227+
// Clear ignoredAtPositions since we deleted the entire @mention
228+
// This ensures typing @ again will open the picker
229+
options.setIgnoredAtPositions(new Set())
230+
}
231+
208232
return
209233
}
210234
}

0 commit comments

Comments
 (0)