|
@@ -1,6 +1,6 @@
|
|
|
import { createStore, produce } from "solid-js/store"
|
|
import { createStore, produce } from "solid-js/store"
|
|
|
import { createSimpleContext } from "./helper"
|
|
import { createSimpleContext } from "./helper"
|
|
|
-import { batch, createEffect, createMemo } from "solid-js"
|
|
|
|
|
|
|
+import { batch, createEffect, createMemo, createSignal, on } from "solid-js"
|
|
|
import { useSync } from "./sync"
|
|
import { useSync } from "./sync"
|
|
|
import { makePersisted } from "@solid-primitives/storage"
|
|
import { makePersisted } from "@solid-primitives/storage"
|
|
|
import { TextSelection, useLocal } from "./local"
|
|
import { TextSelection, useLocal } from "./local"
|
|
@@ -13,26 +13,74 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
|
|
|
const sync = useSync()
|
|
const sync = useSync()
|
|
|
const local = useLocal()
|
|
const local = useLocal()
|
|
|
|
|
|
|
|
- const [store, setStore] = makePersisted(
|
|
|
|
|
|
|
+ const seed = props.sessionId ?? "new-session"
|
|
|
|
|
+
|
|
|
|
|
+ const [persist, setPersist] = makePersisted(
|
|
|
createStore<{
|
|
createStore<{
|
|
|
- prompt: Prompt
|
|
|
|
|
- cursorPosition?: number
|
|
|
|
|
messageId?: string
|
|
messageId?: string
|
|
|
tabs: {
|
|
tabs: {
|
|
|
active?: string
|
|
active?: string
|
|
|
opened: string[]
|
|
opened: string[]
|
|
|
}
|
|
}
|
|
|
}>({
|
|
}>({
|
|
|
- prompt: [{ type: "text", content: "", start: 0, end: 0 }],
|
|
|
|
|
tabs: {
|
|
tabs: {
|
|
|
opened: [],
|
|
opened: [],
|
|
|
},
|
|
},
|
|
|
}),
|
|
}),
|
|
|
{
|
|
{
|
|
|
- name: props.sessionId ?? "new-session",
|
|
|
|
|
|
|
+ name: seed,
|
|
|
},
|
|
},
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+ const [promptStore, setPromptStore] = createStore<{
|
|
|
|
|
+ prompt: Prompt
|
|
|
|
|
+ cursor?: number
|
|
|
|
|
+ }>({
|
|
|
|
|
+ prompt: clonePrompt(DEFAULT_PROMPT),
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const key = createMemo(() => props.sessionId ?? "new-session")
|
|
|
|
|
+ const [ready, setReady] = createSignal(false)
|
|
|
|
|
+ const prefix = "session-prompt:"
|
|
|
|
|
+
|
|
|
|
|
+ createEffect(
|
|
|
|
|
+ on(
|
|
|
|
|
+ key,
|
|
|
|
|
+ (value) => {
|
|
|
|
|
+ setReady(false)
|
|
|
|
|
+ const record = localStorage.getItem(prefix + value)
|
|
|
|
|
+ if (!record) {
|
|
|
|
|
+ setPromptStore("prompt", clonePrompt(DEFAULT_PROMPT))
|
|
|
|
|
+ setPromptStore("cursor", undefined)
|
|
|
|
|
+ setReady(true)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const payload = JSON.parse(record) as { prompt?: Prompt; cursor?: number }
|
|
|
|
|
+ const parts = payload.prompt ?? DEFAULT_PROMPT
|
|
|
|
|
+ const cursor = typeof payload.cursor === "number" ? payload.cursor : undefined
|
|
|
|
|
+ setPromptStore("prompt", clonePrompt(parts))
|
|
|
|
|
+ setPromptStore("cursor", cursor)
|
|
|
|
|
+ setReady(true)
|
|
|
|
|
+ },
|
|
|
|
|
+ { defer: true },
|
|
|
|
|
+ ),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ createEffect(() => {
|
|
|
|
|
+ if (!ready()) return
|
|
|
|
|
+ const value = key()
|
|
|
|
|
+ const isDefault = isPromptEqual(promptStore.prompt, DEFAULT_PROMPT)
|
|
|
|
|
+ if (isDefault && (promptStore.cursor === undefined || promptStore.cursor <= 0)) {
|
|
|
|
|
+ localStorage.removeItem(prefix + value)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ const next = JSON.stringify({
|
|
|
|
|
+ prompt: clonePrompt(promptStore.prompt),
|
|
|
|
|
+ cursor: promptStore.cursor,
|
|
|
|
|
+ })
|
|
|
|
|
+ localStorage.setItem(prefix + value, next)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
createEffect(() => {
|
|
createEffect(() => {
|
|
|
if (!props.sessionId) return
|
|
if (!props.sessionId) return
|
|
|
sync.session.sync(props.sessionId)
|
|
sync.session.sync(props.sessionId)
|
|
@@ -49,8 +97,8 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
|
|
|
return userMessages()?.at(0)
|
|
return userMessages()?.at(0)
|
|
|
})
|
|
})
|
|
|
const activeMessage = createMemo(() => {
|
|
const activeMessage = createMemo(() => {
|
|
|
- if (!store.messageId) return lastUserMessage()
|
|
|
|
|
- return userMessages()?.find((m) => m.id === store.messageId)
|
|
|
|
|
|
|
+ if (!persist.messageId) return lastUserMessage()
|
|
|
|
|
+ return userMessages()?.find((m) => m.id === persist.messageId)
|
|
|
})
|
|
})
|
|
|
const working = createMemo(() => {
|
|
const working = createMemo(() => {
|
|
|
if (!props.sessionId) return false
|
|
if (!props.sessionId) return false
|
|
@@ -101,13 +149,14 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
|
|
|
working,
|
|
working,
|
|
|
diffs,
|
|
diffs,
|
|
|
prompt: {
|
|
prompt: {
|
|
|
- current: createMemo(() => store.prompt),
|
|
|
|
|
- cursor: createMemo(() => store.cursorPosition),
|
|
|
|
|
- dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
|
|
|
|
|
|
|
+ current: createMemo(() => promptStore.prompt),
|
|
|
|
|
+ cursor: createMemo(() => promptStore.cursor),
|
|
|
|
|
+ dirty: createMemo(() => !isPromptEqual(promptStore.prompt, DEFAULT_PROMPT)),
|
|
|
set(prompt: Prompt, cursorPosition?: number) {
|
|
set(prompt: Prompt, cursorPosition?: number) {
|
|
|
|
|
+ const next = clonePrompt(prompt)
|
|
|
batch(() => {
|
|
batch(() => {
|
|
|
- setStore("prompt", prompt)
|
|
|
|
|
- if (cursorPosition !== undefined) setStore("cursorPosition", cursorPosition)
|
|
|
|
|
|
|
+ setPromptStore("prompt", next)
|
|
|
|
|
+ if (cursorPosition !== undefined) setPromptStore("cursor", cursorPosition)
|
|
|
})
|
|
})
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
@@ -117,7 +166,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
|
|
|
last: lastUserMessage,
|
|
last: lastUserMessage,
|
|
|
active: activeMessage,
|
|
active: activeMessage,
|
|
|
setActive(id: string | undefined) {
|
|
setActive(id: string | undefined) {
|
|
|
- setStore("messageId", id)
|
|
|
|
|
|
|
+ setPersist("messageId", id)
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
usage: {
|
|
usage: {
|
|
@@ -126,53 +175,52 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
|
|
|
context,
|
|
context,
|
|
|
},
|
|
},
|
|
|
layout: {
|
|
layout: {
|
|
|
- tabs: store.tabs,
|
|
|
|
|
|
|
+ tabs: persist.tabs,
|
|
|
setActiveTab(tab: string | undefined) {
|
|
setActiveTab(tab: string | undefined) {
|
|
|
- setStore("tabs", "active", tab)
|
|
|
|
|
|
|
+ setPersist("tabs", "active", tab)
|
|
|
},
|
|
},
|
|
|
setOpenedTabs(tabs: string[]) {
|
|
setOpenedTabs(tabs: string[]) {
|
|
|
- setStore("tabs", "opened", tabs)
|
|
|
|
|
|
|
+ setPersist("tabs", "opened", tabs)
|
|
|
},
|
|
},
|
|
|
async openTab(tab: string) {
|
|
async openTab(tab: string) {
|
|
|
if (tab === "chat") {
|
|
if (tab === "chat") {
|
|
|
- setStore("tabs", "active", undefined)
|
|
|
|
|
|
|
+ setPersist("tabs", "active", undefined)
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
if (tab.startsWith("file://")) {
|
|
if (tab.startsWith("file://")) {
|
|
|
await local.file.open(tab.replace("file://", ""))
|
|
await local.file.open(tab.replace("file://", ""))
|
|
|
}
|
|
}
|
|
|
if (tab !== "review") {
|
|
if (tab !== "review") {
|
|
|
- if (!store.tabs.opened.includes(tab)) {
|
|
|
|
|
- setStore("tabs", "opened", [...store.tabs.opened, tab])
|
|
|
|
|
|
|
+ if (!persist.tabs.opened.includes(tab)) {
|
|
|
|
|
+ setPersist("tabs", "opened", [...persist.tabs.opened, tab])
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- setStore("tabs", "active", tab)
|
|
|
|
|
|
|
+ setPersist("tabs", "active", tab)
|
|
|
},
|
|
},
|
|
|
closeTab(tab: string) {
|
|
closeTab(tab: string) {
|
|
|
batch(() => {
|
|
batch(() => {
|
|
|
- setStore(
|
|
|
|
|
|
|
+ setPersist(
|
|
|
"tabs",
|
|
"tabs",
|
|
|
"opened",
|
|
"opened",
|
|
|
- store.tabs.opened.filter((x) => x !== tab),
|
|
|
|
|
|
|
+ persist.tabs.opened.filter((x) => x !== tab),
|
|
|
)
|
|
)
|
|
|
- if (store.tabs.active === tab) {
|
|
|
|
|
- const index = store.tabs.opened.findIndex((f) => f === tab)
|
|
|
|
|
- const previous = store.tabs.opened[Math.max(0, index - 1)]
|
|
|
|
|
- setStore("tabs", "active", previous)
|
|
|
|
|
|
|
+ if (persist.tabs.active === tab) {
|
|
|
|
|
+ const index = persist.tabs.opened.findIndex((f) => f === tab)
|
|
|
|
|
+ const previous = persist.tabs.opened[Math.max(0, index - 1)]
|
|
|
|
|
+ setPersist("tabs", "active", previous)
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
},
|
|
},
|
|
|
moveTab(tab: string, to: number) {
|
|
moveTab(tab: string, to: number) {
|
|
|
- const index = store.tabs.opened.findIndex((f) => f === tab)
|
|
|
|
|
|
|
+ const index = persist.tabs.opened.findIndex((f) => f === tab)
|
|
|
if (index === -1) return
|
|
if (index === -1) return
|
|
|
- setStore(
|
|
|
|
|
|
|
+ setPersist(
|
|
|
"tabs",
|
|
"tabs",
|
|
|
"opened",
|
|
"opened",
|
|
|
produce((opened) => {
|
|
produce((opened) => {
|
|
|
opened.splice(to, 0, opened.splice(index, 1)[0])
|
|
opened.splice(to, 0, opened.splice(index, 1)[0])
|
|
|
}),
|
|
}),
|
|
|
)
|
|
)
|
|
|
- // setStore("node", path, "pinned", true)
|
|
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
}
|
|
}
|
|
@@ -215,3 +263,20 @@ export function isPromptEqual(promptA: Prompt, promptB: Prompt): boolean {
|
|
|
}
|
|
}
|
|
|
return true
|
|
return true
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+function cloneSelection(selection?: TextSelection) {
|
|
|
|
|
+ if (!selection) return undefined
|
|
|
|
|
+ return { ...selection }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function clonePart(part: ContentPart): ContentPart {
|
|
|
|
|
+ if (part.type === "text") return { ...part }
|
|
|
|
|
+ return {
|
|
|
|
|
+ ...part,
|
|
|
|
|
+ selection: cloneSelection(part.selection),
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function clonePrompt(prompt: Prompt): Prompt {
|
|
|
|
|
+ return prompt.map(clonePart)
|
|
|
|
|
+}
|