Align prompt persistence and Electron builds with Node toolchain
This commit is contained in:
@@ -84,7 +84,8 @@ export function Markdown(props: MarkdownProps) {
|
||||
const rendered = await renderMarkdown(text)
|
||||
if (latestRequestedText === text) {
|
||||
setHtml(rendered)
|
||||
part.renderCache = { text, html: rendered }
|
||||
const themeKey = Boolean(props.isDark) ? "dark" : "light"
|
||||
part.renderCache = { text, html: rendered, theme: themeKey }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to re-render markdown after language load:", error)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createSignal, Show, onMount, For, onCleanup } from "solid-js"
|
||||
import { createSignal, Show, onMount, For, onCleanup, createEffect, on, untrack } from "solid-js"
|
||||
import UnifiedPicker from "./unified-picker"
|
||||
import { addToHistory, getHistory } from "../stores/message-history"
|
||||
import { getAttachments, addAttachment, clearAttachments, removeAttachment } from "../stores/attachments"
|
||||
import { getPromptValue, setPromptValue, clearPromptValue } from "../stores/prompt-state"
|
||||
import { createFileAttachment, createTextAttachment, createAgentAttachment } from "../types/attachment"
|
||||
import type { Attachment } from "../types/attachment"
|
||||
import Kbd from "./kbd"
|
||||
@@ -19,7 +20,7 @@ interface PromptInputProps {
|
||||
}
|
||||
|
||||
export default function PromptInput(props: PromptInputProps) {
|
||||
const [prompt, setPrompt] = createSignal("")
|
||||
const [prompt, setPromptInternal] = createSignal("")
|
||||
const [history, setHistory] = createSignal<string[]>([])
|
||||
const [historyIndex, setHistoryIndex] = createSignal(-1)
|
||||
const [isFocused, setIsFocused] = createSignal(false)
|
||||
@@ -27,7 +28,7 @@ export default function PromptInput(props: PromptInputProps) {
|
||||
const [searchQuery, setSearchQuery] = createSignal("")
|
||||
const [atPosition, setAtPosition] = createSignal<number | null>(null)
|
||||
const [isDragging, setIsDragging] = createSignal(false)
|
||||
const [ignoredAtPositions, setIgnoredAtPositions] = createSignal<Set<number>>(new Set())
|
||||
const [ignoredAtPositions, setIgnoredAtPositions] = createSignal<Set<number>>(new Set<number>())
|
||||
const [pasteCount, setPasteCount] = createSignal(0)
|
||||
const [imageCount, setImageCount] = createSignal(0)
|
||||
let textareaRef: HTMLTextAreaElement | undefined
|
||||
@@ -36,6 +37,85 @@ export default function PromptInput(props: PromptInputProps) {
|
||||
const attachments = () => getAttachments(props.instanceId, props.sessionId)
|
||||
const instanceAgents = () => agents().get(props.instanceId) || []
|
||||
|
||||
const setPrompt = (value: string) => {
|
||||
setPromptInternal(value)
|
||||
setPromptValue(props.instanceId, props.sessionId, value)
|
||||
}
|
||||
|
||||
const clearPrompt = () => {
|
||||
clearPromptValue(props.instanceId, props.sessionId)
|
||||
setPromptInternal("")
|
||||
}
|
||||
|
||||
function syncAttachmentCounters(currentPrompt: string, sessionAttachments: Attachment[]) {
|
||||
let highestPaste = 0
|
||||
let highestImage = 0
|
||||
|
||||
for (const match of currentPrompt.matchAll(/\[pasted #(\d+)\]/g)) {
|
||||
const value = Number.parseInt(match[1], 10)
|
||||
if (!Number.isNaN(value)) {
|
||||
highestPaste = Math.max(highestPaste, value)
|
||||
}
|
||||
}
|
||||
|
||||
for (const attachment of sessionAttachments) {
|
||||
if (attachment.source.type === "text") {
|
||||
const placeholderMatch = attachment.display.match(/pasted #(\d+)/)
|
||||
if (placeholderMatch) {
|
||||
const value = Number.parseInt(placeholderMatch[1], 10)
|
||||
if (!Number.isNaN(value)) {
|
||||
highestPaste = Math.max(highestPaste, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (attachment.source.type === "file" && attachment.mediaType.startsWith("image/")) {
|
||||
const imageMatch = attachment.display.match(/Image #(\d+)/)
|
||||
if (imageMatch) {
|
||||
const value = Number.parseInt(imageMatch[1], 10)
|
||||
if (!Number.isNaN(value)) {
|
||||
highestImage = Math.max(highestImage, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const match of currentPrompt.matchAll(/\[Image #(\d+)\]/g)) {
|
||||
const value = Number.parseInt(match[1], 10)
|
||||
if (!Number.isNaN(value)) {
|
||||
highestImage = Math.max(highestImage, value)
|
||||
}
|
||||
}
|
||||
|
||||
setPasteCount(highestPaste)
|
||||
setImageCount(highestImage)
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
() => `${props.instanceId}:${props.sessionId}`,
|
||||
() => {
|
||||
const storedPrompt = getPromptValue(props.instanceId, props.sessionId)
|
||||
const currentAttachments = untrack(() => getAttachments(props.instanceId, props.sessionId))
|
||||
|
||||
setPrompt(storedPrompt)
|
||||
setHistoryIndex(-1)
|
||||
setIgnoredAtPositions(new Set<number>())
|
||||
setShowPicker(false)
|
||||
setAtPosition(null)
|
||||
setSearchQuery("")
|
||||
syncAttachmentCounters(storedPrompt, currentAttachments)
|
||||
|
||||
queueMicrotask(() => {
|
||||
if (textareaRef) {
|
||||
textareaRef.style.height = "auto"
|
||||
textareaRef.style.height = Math.min(textareaRef.scrollHeight, 200) + "px"
|
||||
}
|
||||
})
|
||||
},
|
||||
{ defer: true },
|
||||
),
|
||||
)
|
||||
|
||||
function handleRemoveAttachment(attachmentId: string) {
|
||||
const currentAttachments = attachments()
|
||||
const attachment = currentAttachments.find((a) => a.id === attachmentId)
|
||||
@@ -391,7 +471,7 @@ export default function PromptInput(props: PromptInputProps) {
|
||||
const currentAttachments = attachments()
|
||||
if (!text || props.disabled) return
|
||||
|
||||
setPrompt("")
|
||||
clearPrompt()
|
||||
clearAttachments(props.instanceId, props.sessionId)
|
||||
setIgnoredAtPositions(new Set<number>())
|
||||
setPasteCount(0)
|
||||
|
||||
34
src/stores/prompt-state.ts
Normal file
34
src/stores/prompt-state.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { createSignal } from "solid-js"
|
||||
|
||||
function getSessionKey(instanceId: string, sessionId: string): string {
|
||||
return `${instanceId}:${sessionId}`
|
||||
}
|
||||
|
||||
const [prompts, setPrompts] = createSignal<Map<string, string>>(new Map())
|
||||
|
||||
export function getPromptValue(instanceId: string, sessionId: string): string {
|
||||
const key = getSessionKey(instanceId, sessionId)
|
||||
return prompts().get(key) || ""
|
||||
}
|
||||
|
||||
export function setPromptValue(instanceId: string, sessionId: string, value: string) {
|
||||
const key = getSessionKey(instanceId, sessionId)
|
||||
setPrompts((prev) => {
|
||||
const next = new Map(prev)
|
||||
if (value.length === 0) {
|
||||
next.delete(key)
|
||||
} else {
|
||||
next.set(key, value)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
export function clearPromptValue(instanceId: string, sessionId: string) {
|
||||
const key = getSessionKey(instanceId, sessionId)
|
||||
setPrompts((prev) => {
|
||||
const next = new Map(prev)
|
||||
next.delete(key)
|
||||
return next
|
||||
})
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { partHasRenderableText } from "../types/message"
|
||||
import { instances } from "./instances"
|
||||
|
||||
import { sseManager } from "../lib/sse-manager"
|
||||
import { decodeHtmlEntities } from "../lib/markdown"
|
||||
import { preferences } from "./preferences"
|
||||
|
||||
interface SessionInfo {
|
||||
@@ -29,6 +30,13 @@ const [loading, setLoading] = createSignal({
|
||||
|
||||
const [messagesLoaded, setMessagesLoaded] = createSignal<Map<string, Set<string>>>(new Map())
|
||||
const [sessionInfoByInstance, setSessionInfoByInstance] = createSignal<Map<string, Map<string, SessionInfo>>>(new Map())
|
||||
if (typeof globalThis !== "undefined") {
|
||||
const debugGlobal = globalThis as any
|
||||
debugGlobal.__OPENCODE_DEBUG__ = {
|
||||
...(debugGlobal.__OPENCODE_DEBUG__ ?? {}),
|
||||
getSessions: () => sessions(),
|
||||
}
|
||||
}
|
||||
|
||||
// Message index cache structure: instanceId -> sessionId -> { messageIndex, partIndex }
|
||||
const sessionIndexes = new Map<
|
||||
@@ -36,6 +44,78 @@ const sessionIndexes = new Map<
|
||||
Map<string, { messageIndex: Map<string, number>; partIndex: Map<string, Map<string, number>> }>
|
||||
>()
|
||||
|
||||
function decodeTextSegment(segment: any): any {
|
||||
if (typeof segment === "string") {
|
||||
return decodeHtmlEntities(segment)
|
||||
}
|
||||
|
||||
if (segment && typeof segment === "object") {
|
||||
const updated: Record<string, any> = { ...segment }
|
||||
|
||||
if (typeof updated.text === "string") {
|
||||
updated.text = decodeHtmlEntities(updated.text)
|
||||
}
|
||||
|
||||
if (typeof updated.value === "string") {
|
||||
updated.value = decodeHtmlEntities(updated.value)
|
||||
}
|
||||
|
||||
if (Array.isArray(updated.content)) {
|
||||
updated.content = updated.content.map((item: any) => decodeTextSegment(item))
|
||||
}
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
return segment
|
||||
}
|
||||
|
||||
function normalizeMessagePart(part: any): any {
|
||||
if (!part || typeof part !== "object") {
|
||||
return part
|
||||
}
|
||||
|
||||
if (part.type !== "text") {
|
||||
return part
|
||||
}
|
||||
|
||||
const normalized: Record<string, any> = { ...part, renderCache: undefined }
|
||||
|
||||
if (typeof normalized.text === "string") {
|
||||
normalized.text = decodeHtmlEntities(normalized.text)
|
||||
} else if (normalized.text && typeof normalized.text === "object") {
|
||||
const textObject: Record<string, any> = { ...normalized.text }
|
||||
|
||||
if (typeof textObject.value === "string") {
|
||||
textObject.value = decodeHtmlEntities(textObject.value)
|
||||
}
|
||||
|
||||
if (Array.isArray(textObject.content)) {
|
||||
textObject.content = textObject.content.map((item: any) => decodeTextSegment(item))
|
||||
}
|
||||
|
||||
if (typeof textObject.text === "string") {
|
||||
textObject.text = decodeHtmlEntities(textObject.text)
|
||||
}
|
||||
|
||||
normalized.text = textObject
|
||||
}
|
||||
|
||||
if (Array.isArray(normalized.content)) {
|
||||
normalized.content = normalized.content.map((item: any) => decodeTextSegment(item))
|
||||
}
|
||||
|
||||
if (normalized.thinking && typeof normalized.thinking === "object") {
|
||||
const thinking: Record<string, any> = { ...normalized.thinking }
|
||||
if (Array.isArray(thinking.content)) {
|
||||
thinking.content = thinking.content.map((item: any) => decodeTextSegment(item))
|
||||
}
|
||||
normalized.thinking = thinking
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
function getSessionIndex(instanceId: string, sessionId: string) {
|
||||
let instanceMap = sessionIndexes.get(instanceId)
|
||||
if (!instanceMap) {
|
||||
@@ -716,13 +796,8 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
|
||||
|
||||
messagesInfo.set(messageId, info)
|
||||
|
||||
// Clear render cache for all parts when loading messages
|
||||
const parts = (apiMessage.parts || []).map((part: any) => {
|
||||
if (part.type === "text") {
|
||||
return { ...part, renderCache: undefined }
|
||||
}
|
||||
return part
|
||||
})
|
||||
// Normalize parts to decode entities and clear caches for text segments
|
||||
const parts = (apiMessage.parts || []).map((part: any) => normalizeMessagePart(part))
|
||||
|
||||
const message: Message = {
|
||||
id: messageId,
|
||||
@@ -815,8 +890,10 @@ function handleMessageUpdate(instanceId: string, event: any): void {
|
||||
if (!instanceSessions) return
|
||||
|
||||
if (event.type === "message.part.updated") {
|
||||
const part = event.properties?.part
|
||||
if (!part) return
|
||||
const rawPart = event.properties?.part
|
||||
if (!rawPart) return
|
||||
|
||||
const part = normalizeMessagePart(rawPart)
|
||||
|
||||
const session = instanceSessions.get(part.sessionID)
|
||||
if (!session) return
|
||||
|
||||
@@ -216,16 +216,6 @@
|
||||
line-height: var(--line-height-normal);
|
||||
}
|
||||
|
||||
/* Assistant message compact overrides */
|
||||
.message-text-assistant {
|
||||
font-size: 13.5px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.message-text-assistant .prose {
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
.message-text-assistant .prose p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user