mirror of
https://github.com/anomalyco/opencode.git
synced 2026-04-18 20:04:50 +00:00
Compare commits
4 Commits
cli-perf/t
...
handle-cur
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32728ff684 | ||
|
|
8e010e32ae | ||
|
|
c3e4352c21 | ||
|
|
6cabcc84a8 |
@@ -55,7 +55,7 @@ import { KVProvider, useKV } from "./context/kv"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ArgsProvider, useArgs, type Args } from "./context/args"
|
||||
import open from "open"
|
||||
import { PromptRefProvider, usePromptRef } from "./context/prompt"
|
||||
import { homeScope, PromptRefProvider, sessionScope, usePromptRef } from "./context/prompt"
|
||||
import { TuiConfigProvider, useTuiConfig } from "./context/tui-config"
|
||||
import { TuiConfig } from "@/config/tui"
|
||||
import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin"
|
||||
@@ -195,6 +195,7 @@ export function tui(input: {
|
||||
function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
const tuiConfig = useTuiConfig()
|
||||
const route = useRoute()
|
||||
const project = useProject()
|
||||
const dimensions = useTerminalDimensions()
|
||||
const renderer = useRenderer()
|
||||
const dialog = useDialog()
|
||||
@@ -416,12 +417,15 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
||||
aliases: ["clear"],
|
||||
},
|
||||
onSelect: () => {
|
||||
const current = promptRef.current
|
||||
// Don't require focus - if there's any text, preserve it
|
||||
const currentPrompt = current?.current?.input ? current.current : undefined
|
||||
const currentPrompt =
|
||||
route.data.type === "session" ? promptRef.current(sessionScope(route.data.sessionID)) : undefined
|
||||
|
||||
if (currentPrompt) {
|
||||
promptRef.apply(homeScope(project.workspace.current()), currentPrompt)
|
||||
}
|
||||
|
||||
route.navigate({
|
||||
type: "home",
|
||||
initialPrompt: currentPrompt,
|
||||
})
|
||||
dialog.clear()
|
||||
},
|
||||
|
||||
@@ -25,6 +25,11 @@ export type PromptInfo = {
|
||||
)[]
|
||||
}
|
||||
|
||||
export type PromptDraft = {
|
||||
prompt: PromptInfo
|
||||
cursor: number
|
||||
}
|
||||
|
||||
const MAX_HISTORY_ENTRIES = 50
|
||||
|
||||
export const { use: usePromptHistory, provider: PromptHistoryProvider } = createSimpleContext({
|
||||
|
||||
@@ -11,10 +11,11 @@ import { useSDK } from "@tui/context/sdk"
|
||||
import { useRoute } from "@tui/context/route"
|
||||
import { useSync } from "@tui/context/sync"
|
||||
import { useEvent } from "@tui/context/event"
|
||||
import { homeScope, sessionScope, usePromptRef } from "@tui/context/prompt"
|
||||
import { MessageID, PartID } from "@/session/schema"
|
||||
import { createStore, produce } from "solid-js/store"
|
||||
import { createStore, produce, unwrap } from "solid-js/store"
|
||||
import { useKeybind } from "@tui/context/keybind"
|
||||
import { usePromptHistory, type PromptInfo } from "./history"
|
||||
import { usePromptHistory, type PromptDraft, type PromptInfo } from "./history"
|
||||
import { assign } from "./part"
|
||||
import { usePromptStash } from "./stash"
|
||||
import { DialogStash } from "../dialog-stash"
|
||||
@@ -57,6 +58,8 @@ export type PromptProps = {
|
||||
export type PromptRef = {
|
||||
focused: boolean
|
||||
current: PromptInfo
|
||||
snapshot(): PromptDraft
|
||||
restore(draft: PromptDraft): void
|
||||
set(prompt: PromptInfo): void
|
||||
reset(): void
|
||||
blur(): void
|
||||
@@ -85,6 +88,7 @@ export function Prompt(props: PromptProps) {
|
||||
const route = useRoute()
|
||||
const sync = useSync()
|
||||
const dialog = useDialog()
|
||||
const promptState = usePromptRef()
|
||||
const toast = useToast()
|
||||
const status = createMemo(() => sync.data.session_status?.[props.sessionID ?? ""] ?? { type: "idle" })
|
||||
const history = usePromptHistory()
|
||||
@@ -98,6 +102,14 @@ export function Prompt(props: PromptProps) {
|
||||
const [auto, setAuto] = createSignal<AutocompleteRef>()
|
||||
const currentProviderLabel = createMemo(() => local.model.parsed().provider)
|
||||
const hasRightContent = createMemo(() => Boolean(props.right))
|
||||
const scope = createMemo(() => {
|
||||
if (props.sessionID) return sessionScope(props.sessionID)
|
||||
if (props.workspaceID !== undefined) return homeScope(props.workspaceID)
|
||||
if (route.data.type === "session") return sessionScope(route.data.sessionID)
|
||||
if (route.data.type === "plugin") return `plugin:${route.data.id}`
|
||||
return homeScope()
|
||||
})
|
||||
let active: string | undefined
|
||||
|
||||
function promptModelWarning() {
|
||||
toast.show({
|
||||
@@ -177,6 +189,7 @@ export function Prompt(props: PromptProps) {
|
||||
extmarkToPartIndex: new Map(),
|
||||
interrupt: 0,
|
||||
})
|
||||
const [ready, setReady] = createSignal(false)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
@@ -219,8 +232,7 @@ export function Prompt(props: PromptProps) {
|
||||
category: "Prompt",
|
||||
hidden: true,
|
||||
onSelect: (dialog) => {
|
||||
input.extmarks.clear()
|
||||
input.clear()
|
||||
ref.reset()
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
@@ -397,12 +409,70 @@ export function Prompt(props: PromptProps) {
|
||||
]
|
||||
})
|
||||
|
||||
function clearPrompt(mode: "keep" | "normal" = "keep") {
|
||||
input.extmarks.clear()
|
||||
input.clear()
|
||||
setStore("prompt", {
|
||||
input: "",
|
||||
parts: [],
|
||||
})
|
||||
setStore("extmarkToPartIndex", new Map())
|
||||
if (mode === "normal") {
|
||||
setStore("mode", "normal")
|
||||
}
|
||||
}
|
||||
|
||||
function restorePrompt(draft: PromptDraft) {
|
||||
const next = structuredClone(unwrap(draft))
|
||||
input.setText(next.prompt.input)
|
||||
setStore("mode", next.prompt.mode ?? "normal")
|
||||
setStore("prompt", {
|
||||
input: next.prompt.input,
|
||||
parts: next.prompt.parts,
|
||||
})
|
||||
restoreExtmarksFromParts(next.prompt.parts)
|
||||
input.cursorOffset = next.cursor
|
||||
}
|
||||
|
||||
function snapshot() {
|
||||
const value = input && !input.isDestroyed ? input.plainText : store.prompt.input
|
||||
|
||||
if (input && !input.isDestroyed) {
|
||||
if (value !== store.prompt.input) {
|
||||
setStore("prompt", "input", value)
|
||||
}
|
||||
syncExtmarksWithPromptParts()
|
||||
}
|
||||
|
||||
return {
|
||||
prompt: {
|
||||
input: value,
|
||||
mode: store.mode,
|
||||
parts: structuredClone(unwrap(store.prompt.parts)),
|
||||
},
|
||||
cursor: input && !input.isDestroyed ? input.cursorOffset : Bun.stringWidth(value),
|
||||
} satisfies PromptDraft
|
||||
}
|
||||
|
||||
const ref: PromptRef = {
|
||||
get focused() {
|
||||
return input.focused
|
||||
},
|
||||
get current() {
|
||||
return store.prompt
|
||||
return {
|
||||
input: store.prompt.input,
|
||||
mode: store.mode,
|
||||
parts: store.prompt.parts,
|
||||
}
|
||||
},
|
||||
snapshot() {
|
||||
return snapshot()
|
||||
},
|
||||
restore(draft) {
|
||||
restorePrompt(draft)
|
||||
if (active) {
|
||||
promptState.save(active, draft)
|
||||
}
|
||||
},
|
||||
focus() {
|
||||
input.focus()
|
||||
@@ -411,26 +481,67 @@ export function Prompt(props: PromptProps) {
|
||||
input.blur()
|
||||
},
|
||||
set(prompt) {
|
||||
input.setText(prompt.input)
|
||||
setStore("prompt", prompt)
|
||||
restoreExtmarksFromParts(prompt.parts)
|
||||
input.gotoBufferEnd()
|
||||
ref.restore({
|
||||
prompt: structuredClone(unwrap(prompt)),
|
||||
cursor: Bun.stringWidth(prompt.input),
|
||||
})
|
||||
},
|
||||
reset() {
|
||||
input.clear()
|
||||
input.extmarks.clear()
|
||||
setStore("prompt", {
|
||||
input: "",
|
||||
parts: [],
|
||||
})
|
||||
setStore("extmarkToPartIndex", new Map())
|
||||
clearPrompt()
|
||||
if (active) {
|
||||
promptState.drop(active)
|
||||
}
|
||||
},
|
||||
submit() {
|
||||
submit()
|
||||
},
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!ready()) return
|
||||
|
||||
const next = scope()
|
||||
if (active === next) return
|
||||
|
||||
const prev = active
|
||||
const prevDraft = prev ? snapshot() : undefined
|
||||
if (prev) {
|
||||
promptState.save(prev, prevDraft!)
|
||||
promptState.bind(prev, undefined)
|
||||
}
|
||||
|
||||
active = next
|
||||
promptState.bind(next, ref)
|
||||
|
||||
const draft = promptState.load(next)
|
||||
if (draft) {
|
||||
ref.restore(draft)
|
||||
return
|
||||
}
|
||||
|
||||
const carry =
|
||||
prev &&
|
||||
prev.startsWith("session:") &&
|
||||
next.startsWith("session:") &&
|
||||
route.data.type === "session" &&
|
||||
props.sessionID === route.data.sessionID &&
|
||||
!route.data.initialPrompt &&
|
||||
prevDraft &&
|
||||
(prevDraft.prompt.input || prevDraft.prompt.parts.length > 0)
|
||||
|
||||
if (carry) {
|
||||
ref.restore(prevDraft)
|
||||
return
|
||||
}
|
||||
|
||||
clearPrompt("normal")
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
if (active) {
|
||||
promptState.save(active, snapshot())
|
||||
promptState.bind(active, undefined)
|
||||
}
|
||||
props.ref?.(undefined)
|
||||
})
|
||||
|
||||
@@ -538,17 +649,15 @@ export function Prompt(props: PromptProps) {
|
||||
title: "Stash prompt",
|
||||
value: "prompt.stash",
|
||||
category: "Prompt",
|
||||
enabled: !!store.prompt.input,
|
||||
enabled: !!store.prompt.input || store.prompt.parts.length > 0,
|
||||
onSelect: (dialog) => {
|
||||
if (!store.prompt.input) return
|
||||
const prompt = snapshot()
|
||||
if (!prompt.prompt.input && prompt.prompt.parts.length === 0) return
|
||||
stash.push({
|
||||
input: store.prompt.input,
|
||||
parts: store.prompt.parts,
|
||||
input: prompt.prompt.input,
|
||||
parts: prompt.prompt.parts,
|
||||
})
|
||||
input.extmarks.clear()
|
||||
input.clear()
|
||||
setStore("prompt", { input: "", parts: [] })
|
||||
setStore("extmarkToPartIndex", new Map())
|
||||
ref.reset()
|
||||
dialog.clear()
|
||||
},
|
||||
},
|
||||
@@ -560,10 +669,7 @@ export function Prompt(props: PromptProps) {
|
||||
onSelect: (dialog) => {
|
||||
const entry = stash.pop()
|
||||
if (entry) {
|
||||
input.setText(entry.input)
|
||||
setStore("prompt", { input: entry.input, parts: entry.parts })
|
||||
restoreExtmarksFromParts(entry.parts)
|
||||
input.gotoBufferEnd()
|
||||
ref.set({ input: entry.input, parts: entry.parts })
|
||||
}
|
||||
dialog.clear()
|
||||
},
|
||||
@@ -577,10 +683,7 @@ export function Prompt(props: PromptProps) {
|
||||
dialog.replace(() => (
|
||||
<DialogStash
|
||||
onSelect={(entry) => {
|
||||
input.setText(entry.input)
|
||||
setStore("prompt", { input: entry.input, parts: entry.parts })
|
||||
restoreExtmarksFromParts(entry.parts)
|
||||
input.gotoBufferEnd()
|
||||
ref.set({ input: entry.input, parts: entry.parts })
|
||||
}}
|
||||
/>
|
||||
))
|
||||
@@ -589,17 +692,13 @@ export function Prompt(props: PromptProps) {
|
||||
])
|
||||
|
||||
async function submit() {
|
||||
// IME: double-defer may fire before onContentChange flushes the last
|
||||
// composed character (e.g. Korean hangul) to the store, so read
|
||||
// plainText directly and sync before any downstream reads.
|
||||
if (input && !input.isDestroyed && input.plainText !== store.prompt.input) {
|
||||
setStore("prompt", "input", input.plainText)
|
||||
syncExtmarksWithPromptParts()
|
||||
}
|
||||
const prompt = snapshot()
|
||||
|
||||
if (props.disabled) return
|
||||
if (autocomplete?.visible) return
|
||||
if (!store.prompt.input) return
|
||||
const trimmed = store.prompt.input.trim()
|
||||
if (!prompt.prompt.input) return
|
||||
|
||||
const trimmed = prompt.prompt.input.trim()
|
||||
if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") {
|
||||
exit()
|
||||
return
|
||||
@@ -631,7 +730,7 @@ export function Prompt(props: PromptProps) {
|
||||
}
|
||||
|
||||
const messageID = MessageID.ascending()
|
||||
let inputText = store.prompt.input
|
||||
let inputText = prompt.prompt.input
|
||||
|
||||
// Expand pasted text inline before submitting
|
||||
const allExtmarks = input.extmarks.getAllForTypeId(promptPartTypeId)
|
||||
@@ -640,7 +739,7 @@ export function Prompt(props: PromptProps) {
|
||||
for (const extmark of sortedExtmarks) {
|
||||
const partIndex = store.extmarkToPartIndex.get(extmark.id)
|
||||
if (partIndex !== undefined) {
|
||||
const part = store.prompt.parts[partIndex]
|
||||
const part = prompt.prompt.parts[partIndex]
|
||||
if (part?.type === "text" && part.text) {
|
||||
const before = inputText.slice(0, extmark.start)
|
||||
const after = inputText.slice(extmark.end)
|
||||
@@ -650,13 +749,13 @@ export function Prompt(props: PromptProps) {
|
||||
}
|
||||
|
||||
// Filter out text parts (pasted content) since they're now expanded inline
|
||||
const nonTextParts = store.prompt.parts.filter((part) => part.type !== "text")
|
||||
const nonTextParts = prompt.prompt.parts.filter((part) => part.type !== "text")
|
||||
|
||||
// Capture mode before it gets reset
|
||||
const currentMode = store.mode
|
||||
const currentMode = prompt.prompt.mode ?? "normal"
|
||||
const variant = local.model.variant.current()
|
||||
|
||||
if (store.mode === "shell") {
|
||||
if (currentMode === "shell") {
|
||||
sdk.client.session.shell({
|
||||
sessionID,
|
||||
agent: local.agent.current().name,
|
||||
@@ -717,16 +816,11 @@ export function Prompt(props: PromptProps) {
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
history.append({
|
||||
...store.prompt,
|
||||
mode: currentMode,
|
||||
})
|
||||
input.extmarks.clear()
|
||||
setStore("prompt", {
|
||||
input: "",
|
||||
parts: [],
|
||||
})
|
||||
setStore("extmarkToPartIndex", new Map())
|
||||
history.append(prompt.prompt)
|
||||
clearPrompt()
|
||||
if (active) {
|
||||
promptState.drop(active)
|
||||
}
|
||||
props.onSubmit?.()
|
||||
|
||||
// temporary hack to make sure the message is sent
|
||||
@@ -737,7 +831,6 @@ export function Prompt(props: PromptProps) {
|
||||
sessionID,
|
||||
})
|
||||
}, 50)
|
||||
input.clear()
|
||||
}
|
||||
const exit = useExit()
|
||||
|
||||
@@ -944,14 +1037,8 @@ export function Prompt(props: PromptProps) {
|
||||
}
|
||||
// If no image, let the default paste behavior continue
|
||||
}
|
||||
if (keybind.match("input_clear", e) && store.prompt.input !== "") {
|
||||
input.clear()
|
||||
input.extmarks.clear()
|
||||
setStore("prompt", {
|
||||
input: "",
|
||||
parts: [],
|
||||
})
|
||||
setStore("extmarkToPartIndex", new Map())
|
||||
if (keybind.match("input_clear", e) && (store.prompt.input !== "" || store.prompt.parts.length > 0)) {
|
||||
ref.reset()
|
||||
return
|
||||
}
|
||||
if (keybind.match("app_exit", e)) {
|
||||
@@ -1090,6 +1177,7 @@ export function Prompt(props: PromptProps) {
|
||||
if (promptPartTypeId === 0) {
|
||||
promptPartTypeId = input.extmarks.registerType("prompt-part")
|
||||
}
|
||||
setReady(true)
|
||||
props.ref?.(ref)
|
||||
setTimeout(() => {
|
||||
// setTimeout is a workaround and needs to be addressed properly
|
||||
|
||||
@@ -1,17 +1,85 @@
|
||||
import { createSimpleContext } from "./helper"
|
||||
import { unwrap } from "solid-js/store"
|
||||
import type { PromptRef } from "../component/prompt"
|
||||
import type { PromptDraft, PromptInfo } from "../component/prompt/history"
|
||||
|
||||
export function homeScope(workspaceID?: string) {
|
||||
if (!workspaceID) return "home"
|
||||
return `home:${workspaceID}`
|
||||
}
|
||||
|
||||
export function sessionScope(sessionID: string) {
|
||||
return `session:${sessionID}`
|
||||
}
|
||||
|
||||
function clone<T>(value: T) {
|
||||
return structuredClone(unwrap(value))
|
||||
}
|
||||
|
||||
function draft(input: PromptInfo | PromptDraft) {
|
||||
if ("prompt" in input) return clone(input)
|
||||
return {
|
||||
prompt: clone(input),
|
||||
cursor: Bun.stringWidth(input.input),
|
||||
} satisfies PromptDraft
|
||||
}
|
||||
|
||||
function empty(input?: PromptInfo | PromptDraft) {
|
||||
if (!input) return true
|
||||
const prompt = "prompt" in input ? input.prompt : input
|
||||
if (prompt.input) return false
|
||||
return prompt.parts.length === 0
|
||||
}
|
||||
|
||||
export const { use: usePromptRef, provider: PromptRefProvider } = createSimpleContext({
|
||||
name: "PromptRef",
|
||||
init: () => {
|
||||
let current: PromptRef | undefined
|
||||
const drafts = new Map<string, PromptDraft>()
|
||||
let live: { scope: string; ref: PromptRef } | undefined
|
||||
|
||||
function load(scope: string) {
|
||||
const value = drafts.get(scope)
|
||||
if (!value) return
|
||||
return clone(value)
|
||||
}
|
||||
|
||||
function save(scope: string, input: PromptInfo | PromptDraft) {
|
||||
if (empty(input)) {
|
||||
drafts.delete(scope)
|
||||
return
|
||||
}
|
||||
|
||||
drafts.set(scope, draft(input))
|
||||
}
|
||||
|
||||
return {
|
||||
get current() {
|
||||
return current
|
||||
current(scope: string) {
|
||||
if (live?.scope === scope) {
|
||||
const value = live.ref.snapshot()
|
||||
if (!empty(value)) return value
|
||||
return
|
||||
}
|
||||
|
||||
return load(scope)
|
||||
},
|
||||
set(ref: PromptRef | undefined) {
|
||||
current = ref
|
||||
load,
|
||||
save,
|
||||
apply(scope: string, input: PromptInfo | PromptDraft) {
|
||||
const value = draft(input)
|
||||
save(scope, value)
|
||||
if (live?.scope !== scope) return
|
||||
live.ref.restore(value)
|
||||
},
|
||||
drop(scope: string) {
|
||||
drafts.delete(scope)
|
||||
},
|
||||
bind(scope: string, ref: PromptRef | undefined) {
|
||||
if (!ref) {
|
||||
if (live?.scope === scope) live = undefined
|
||||
return
|
||||
}
|
||||
|
||||
live = { scope, ref }
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { createSimpleContext } from "./helper"
|
||||
import type { PromptInfo } from "../component/prompt/history"
|
||||
|
||||
@@ -37,7 +37,7 @@ export const { use: useRoute, provider: RouteProvider } = createSimpleContext({
|
||||
return store
|
||||
},
|
||||
navigate(route: Route) {
|
||||
setStore(route)
|
||||
setStore(reconcile(route))
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,45 +1,63 @@
|
||||
import { Prompt, type PromptRef } from "@tui/component/prompt"
|
||||
import { createEffect, createSignal } from "solid-js"
|
||||
import { createEffect, createMemo, createSignal } from "solid-js"
|
||||
import { Logo } from "../component/logo"
|
||||
import { useProject } from "../context/project"
|
||||
import { useSync } from "../context/sync"
|
||||
import { Toast } from "../ui/toast"
|
||||
import { useArgs } from "../context/args"
|
||||
import { useRouteData } from "@tui/context/route"
|
||||
import { usePromptRef } from "../context/prompt"
|
||||
import { homeScope, usePromptRef } from "../context/prompt"
|
||||
import { useLocal } from "../context/local"
|
||||
import { TuiPluginRuntime } from "../plugin"
|
||||
|
||||
// TODO: what is the best way to do this?
|
||||
let once = false
|
||||
let startup = false
|
||||
const placeholder = {
|
||||
normal: ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"],
|
||||
shell: ["ls -la", "git status", "pwd"],
|
||||
}
|
||||
|
||||
type Ref = Pick<PromptRef, "current" | "submit">
|
||||
|
||||
export function Home() {
|
||||
const sync = useSync()
|
||||
const project = useProject()
|
||||
const route = useRouteData("home")
|
||||
const promptRef = usePromptRef()
|
||||
const [ref, setRef] = createSignal<PromptRef | undefined>()
|
||||
const [ref, setRef] = createSignal<Ref | undefined>()
|
||||
const args = useArgs()
|
||||
const local = useLocal()
|
||||
const scope = createMemo(() => homeScope(project.workspace.current()))
|
||||
let sent = false
|
||||
let seeded: string | undefined
|
||||
|
||||
const bind = (r: PromptRef | undefined) => {
|
||||
const bind = (r: Ref | undefined) => {
|
||||
setRef(r)
|
||||
promptRef.set(r)
|
||||
if (once || !r) return
|
||||
if (route.initialPrompt) {
|
||||
r.set(route.initialPrompt)
|
||||
once = true
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const key = scope()
|
||||
if (seeded === key) return
|
||||
|
||||
if (promptRef.current(key)) {
|
||||
if (route.initialPrompt) {
|
||||
seeded = key
|
||||
startup = true
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!args.prompt) return
|
||||
r.set({ input: args.prompt, parts: [] })
|
||||
once = true
|
||||
}
|
||||
|
||||
if (route.initialPrompt) {
|
||||
promptRef.apply(key, route.initialPrompt)
|
||||
seeded = key
|
||||
startup = true
|
||||
return
|
||||
}
|
||||
|
||||
if (startup || !args.prompt) return
|
||||
|
||||
promptRef.apply(key, { input: args.prompt, parts: [] })
|
||||
startup = true
|
||||
})
|
||||
|
||||
// Wait for sync and model store to be ready before auto-submitting --prompt
|
||||
createEffect(() => {
|
||||
|
||||
@@ -4,18 +4,16 @@ import { DialogSelect } from "@tui/ui/dialog-select"
|
||||
import { useSDK } from "@tui/context/sdk"
|
||||
import { useRoute } from "@tui/context/route"
|
||||
import { Clipboard } from "@tui/util/clipboard"
|
||||
import { sessionScope, usePromptRef } from "@tui/context/prompt"
|
||||
import type { PromptInfo } from "@tui/component/prompt/history"
|
||||
import { strip } from "@tui/component/prompt/part"
|
||||
|
||||
export function DialogMessage(props: {
|
||||
messageID: string
|
||||
sessionID: string
|
||||
setPrompt?: (prompt: PromptInfo) => void
|
||||
}) {
|
||||
export function DialogMessage(props: { messageID: string; sessionID: string }) {
|
||||
const sync = useSync()
|
||||
const sdk = useSDK()
|
||||
const message = createMemo(() => sync.data.message[props.sessionID]?.find((x) => x.id === props.messageID))
|
||||
const route = useRoute()
|
||||
const promptRef = usePromptRef()
|
||||
|
||||
return (
|
||||
<DialogSelect
|
||||
@@ -34,20 +32,18 @@ export function DialogMessage(props: {
|
||||
messageID: msg.id,
|
||||
})
|
||||
|
||||
if (props.setPrompt) {
|
||||
const parts = sync.data.part[msg.id]
|
||||
const promptInfo = parts.reduce(
|
||||
(agg, part) => {
|
||||
if (part.type === "text") {
|
||||
if (!part.synthetic) agg.input += part.text
|
||||
}
|
||||
if (part.type === "file") agg.parts.push(strip(part))
|
||||
return agg
|
||||
},
|
||||
{ input: "", parts: [] as PromptInfo["parts"] },
|
||||
)
|
||||
props.setPrompt(promptInfo)
|
||||
}
|
||||
const parts = sync.data.part[msg.id]
|
||||
const promptInfo = parts.reduce(
|
||||
(agg, part) => {
|
||||
if (part.type === "text") {
|
||||
if (!part.synthetic) agg.input += part.text
|
||||
}
|
||||
if (part.type === "file") agg.parts.push(strip(part))
|
||||
return agg
|
||||
},
|
||||
{ input: "", parts: [] as PromptInfo["parts"] },
|
||||
)
|
||||
promptRef.apply(sessionScope(props.sessionID), promptInfo)
|
||||
|
||||
dialog.clear()
|
||||
},
|
||||
@@ -90,7 +86,7 @@ export function DialogMessage(props: {
|
||||
if (part.type === "text") {
|
||||
if (!part.synthetic) agg.input += part.text
|
||||
}
|
||||
if (part.type === "file") agg.parts.push(part)
|
||||
if (part.type === "file") agg.parts.push(strip(part))
|
||||
return agg
|
||||
},
|
||||
{ input: "", parts: [] as PromptInfo["parts"] },
|
||||
|
||||
@@ -5,13 +5,8 @@ import type { TextPart } from "@opencode-ai/sdk/v2"
|
||||
import { Locale } from "@/util/locale"
|
||||
import { DialogMessage } from "./dialog-message"
|
||||
import { useDialog } from "../../ui/dialog"
|
||||
import type { PromptInfo } from "../../component/prompt/history"
|
||||
|
||||
export function DialogTimeline(props: {
|
||||
sessionID: string
|
||||
onMove: (messageID: string) => void
|
||||
setPrompt?: (prompt: PromptInfo) => void
|
||||
}) {
|
||||
export function DialogTimeline(props: { sessionID: string; onMove: (messageID: string) => void }) {
|
||||
const sync = useSync()
|
||||
const dialog = useDialog()
|
||||
|
||||
@@ -33,9 +28,7 @@ export function DialogTimeline(props: {
|
||||
value: message.id,
|
||||
footer: Locale.time(message.time.created),
|
||||
onSelect: (dialog) => {
|
||||
dialog.replace(() => (
|
||||
<DialogMessage messageID={message.id} sessionID={props.sessionID} setPrompt={props.setPrompt} />
|
||||
))
|
||||
dialog.replace(() => <DialogMessage messageID={message.id} sessionID={props.sessionID} />)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import { SplitBorder } from "@tui/component/border"
|
||||
import { Spinner } from "@tui/component/spinner"
|
||||
import { selectedForeground, useTheme } from "@tui/context/theme"
|
||||
import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA } from "@opentui/core"
|
||||
import { Prompt, type PromptRef } from "@tui/component/prompt"
|
||||
import { Prompt } from "@tui/component/prompt"
|
||||
import type {
|
||||
AssistantMessage,
|
||||
Part,
|
||||
@@ -72,7 +72,7 @@ import { Toast, useToast } from "../../ui/toast"
|
||||
import { useKV } from "../../context/kv.tsx"
|
||||
import { Editor } from "../../util/editor"
|
||||
import stripAnsi from "strip-ansi"
|
||||
import { usePromptRef } from "../../context/prompt"
|
||||
import { sessionScope, usePromptRef } from "../../context/prompt"
|
||||
import { useExit } from "../../context/exit"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { Global } from "@/global"
|
||||
@@ -87,6 +87,7 @@ import { getScrollAcceleration } from "../../util/scroll"
|
||||
import { TuiPluginRuntime } from "../../plugin"
|
||||
import { DialogGoUpsell } from "../../component/dialog-go-upsell"
|
||||
import { SessionRetry } from "@/session/retry"
|
||||
import { strip } from "../../component/prompt/part"
|
||||
|
||||
addDefaultParsers(parsers.parsers)
|
||||
|
||||
@@ -142,6 +143,8 @@ export function Session() {
|
||||
})
|
||||
const visible = createMemo(() => !session()?.parentID && permissions().length === 0 && questions().length === 0)
|
||||
const disabled = createMemo(() => permissions().length > 0 || questions().length > 0)
|
||||
const scope = createMemo(() => sessionScope(route.sessionID))
|
||||
let seeded: string | undefined
|
||||
|
||||
const pending = createMemo(() => {
|
||||
return messages().findLast((x) => x.role === "assistant" && !x.time.completed)?.id
|
||||
@@ -199,8 +202,6 @@ export function Session() {
|
||||
})
|
||||
})
|
||||
|
||||
// Handle initial prompt from fork
|
||||
let seeded = false
|
||||
let lastSwitch: string | undefined = undefined
|
||||
event.on("message.part.updated", (evt) => {
|
||||
const part = evt.properties.part
|
||||
@@ -218,15 +219,23 @@ export function Session() {
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const key = scope()
|
||||
if (seeded === key) return
|
||||
|
||||
if (promptRef.current(key)) {
|
||||
if (route.initialPrompt) seeded = key
|
||||
return
|
||||
}
|
||||
|
||||
if (!route.initialPrompt) return
|
||||
|
||||
promptRef.apply(key, route.initialPrompt)
|
||||
seeded = key
|
||||
})
|
||||
|
||||
let scroll: ScrollBoxRenderable
|
||||
let prompt: PromptRef | undefined
|
||||
const bind = (r: PromptRef | undefined) => {
|
||||
prompt = r
|
||||
promptRef.set(r)
|
||||
if (seeded || !route.initialPrompt || !r) return
|
||||
seeded = true
|
||||
r.set(route.initialPrompt)
|
||||
}
|
||||
const bind = () => {}
|
||||
const keybind = useKeybind()
|
||||
const dialog = useDialog()
|
||||
const renderer = useRenderer()
|
||||
@@ -438,7 +447,6 @@ export function Session() {
|
||||
if (child) scroll.scrollBy(child.y - scroll.y - 1)
|
||||
}}
|
||||
sessionID={route.sessionID}
|
||||
setPrompt={(promptInfo) => prompt?.set(promptInfo)}
|
||||
/>
|
||||
))
|
||||
},
|
||||
@@ -539,13 +547,14 @@ export function Session() {
|
||||
toBottom()
|
||||
})
|
||||
const parts = sync.data.part[message.id]
|
||||
prompt?.set(
|
||||
promptRef.apply(
|
||||
scope(),
|
||||
parts.reduce(
|
||||
(agg, part) => {
|
||||
if (part.type === "text") {
|
||||
if (!part.synthetic) agg.input += part.text
|
||||
}
|
||||
if (part.type === "file") agg.parts.push(part)
|
||||
if (part.type === "file") agg.parts.push(strip(part))
|
||||
return agg
|
||||
},
|
||||
{ input: "", parts: [] as PromptInfo["parts"] },
|
||||
@@ -572,7 +581,7 @@ export function Session() {
|
||||
sdk.client.session.unrevert({
|
||||
sessionID: route.sessionID,
|
||||
})
|
||||
prompt?.set({ input: "", parts: [] })
|
||||
promptRef.apply(scope(), { input: "", parts: [] })
|
||||
return
|
||||
}
|
||||
sdk.client.session.revert({
|
||||
@@ -1149,13 +1158,7 @@ export function Session() {
|
||||
index={index()}
|
||||
onMouseUp={() => {
|
||||
if (renderer.getSelection()?.getSelectedText()) return
|
||||
dialog.replace(() => (
|
||||
<DialogMessage
|
||||
messageID={message.id}
|
||||
sessionID={route.sessionID}
|
||||
setPrompt={(promptInfo) => prompt?.set(promptInfo)}
|
||||
/>
|
||||
))
|
||||
dialog.replace(() => <DialogMessage messageID={message.id} sessionID={route.sessionID} />)
|
||||
}}
|
||||
message={message as UserMessage}
|
||||
parts={sync.data.part[message.id] ?? []}
|
||||
|
||||
Reference in New Issue
Block a user