feat(app): add prompt input story (#32308)

This commit is contained in:
Brendan Allan
2026-06-14 13:40:09 +00:00
committed by GitHub
parent 3e523d506c
commit d37ddc501c
15 changed files with 501 additions and 228 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ export default defineMain({
"@storybook/addon-a11y",
"@storybook/addon-vitest",
],
stories: ["../../ui/src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
stories: ["../../ui/src/**/*.stories.@(js|jsx|mjs|ts|tsx)", "../../app/src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
async viteFinal(config) {
const { mergeConfig, searchForWorkspaceRoot } = await import("vite")
return mergeConfig(config, {
@@ -12,6 +12,9 @@ export function usePermission() {
isAutoAccepting(sessionID: string, directory?: string) {
return accepted.has(key(sessionID, directory))
},
isAutoAcceptingDirectory() {
return false
},
toggleAutoAccept(sessionID: string, directory?: string) {
const next = key(sessionID, directory)
if (accepted.has(next)) {
@@ -1,4 +1,4 @@
import { createSignal } from "solid-js"
import { createStore } from "solid-js/store"
interface PartBase {
content: string
@@ -60,48 +60,50 @@ export function isPromptEqual(a: Prompt, b: Prompt) {
return a.every((part, i) => JSON.stringify(part) === JSON.stringify(b[i]))
}
let index = 0
const [prompt, setPrompt] = createSignal<Prompt>(clonePrompt(DEFAULT_PROMPT))
const [cursor, setCursor] = createSignal<number>(0)
const [items, setItems] = createSignal<ContextItem[]>([])
export function createPromptState() {
const [store, setStore] = createStore({
prompt: clonePrompt(DEFAULT_PROMPT),
cursor: 0,
items: [] as ContextItem[],
})
let index = 0
const ready = Object.assign(() => true, { promise: Promise.resolve(true) })
const withKey = (item: Omit<ContextItem, "key"> & { key?: string }): ContextItem => ({
...item,
key: item.key ?? `ctx:${++index}`,
})
const withKey = (item: Omit<ContextItem, "key"> & { key?: string }): ContextItem => ({
...item,
key: item.key ?? `ctx:${++index}`,
})
export function usePrompt() {
return {
ready: () => true,
current: prompt,
cursor,
dirty: () => !isPromptEqual(prompt(), DEFAULT_PROMPT),
ready: () => ready,
current: () => store.prompt,
cursor: () => store.cursor,
dirty: () => !isPromptEqual(store.prompt, DEFAULT_PROMPT),
set(next: Prompt, cursorPosition?: number) {
setPrompt(clonePrompt(next))
if (cursorPosition !== undefined) setCursor(cursorPosition)
setStore("prompt", clonePrompt(next))
if (cursorPosition !== undefined) setStore("cursor", cursorPosition)
},
reset() {
setPrompt(clonePrompt(DEFAULT_PROMPT))
setCursor(0)
setItems((current) => current.filter((item) => !!item.comment?.trim()))
setStore("prompt", clonePrompt(DEFAULT_PROMPT))
setStore("cursor", 0)
setStore("items", (current) => current.filter((item) => !!item.comment?.trim()))
},
context: {
items,
items: () => store.items,
add(item: Omit<ContextItem, "key"> & { key?: string }) {
const next = withKey(item)
if (items().some((current) => current.key === next.key)) return
setItems((current) => [...current, next])
if (store.items.some((current) => current.key === next.key)) return
setStore("items", (current) => [...current, next])
},
remove(key: string) {
setItems((current) => current.filter((item) => item.key !== key))
setStore("items", (current) => current.filter((item) => item.key !== key))
},
removeComment(path: string, commentID: string) {
setItems((current) =>
setStore("items", (current) =>
current.filter((item) => !(item.type === "file" && item.path === path && item.commentID === commentID)),
)
},
updateComment(path: string, commentID: string, next: Partial<ContextItem>) {
setItems((current) =>
setStore("items", (current) =>
current.map((item) => {
if (item.type !== "file" || item.path !== path || item.commentID !== commentID) return item
return withKey({ ...item, ...next })
@@ -109,9 +111,15 @@ export function usePrompt() {
)
},
replaceComments(next: Array<Omit<ContextItem, "key"> & { key?: string }>) {
const nonComment = items().filter((item) => !item.comment?.trim())
setItems([...nonComment, ...next.map(withKey)])
const nonComment = store.items.filter((item) => !item.comment?.trim())
setStore("items", [...nonComment, ...next.map(withKey)])
},
},
}
}
const prompt = createPromptState()
export function usePrompt() {
return prompt
}
@@ -12,14 +12,16 @@ const make = (directory: string) => ({
})
const root = "/tmp/story"
const sdk = {
directory: root,
scope: "story-server",
url: "http://localhost:4096",
client: make(root),
createClient(input: { directory: string }) {
return make(input.directory)
},
}
export function useSDK() {
return {
directory: root,
url: "http://localhost:4096",
client: make(root),
createClient(input: { directory: string }) {
return make(input.directory)
},
}
return () => sdk
}
@@ -9,24 +9,27 @@ const [data, setData] = createStore({
"story-session": [] as Array<{ id: string; role: string }>,
} as Record<string, Array<{ id: string; role: string }>>,
session_status: {} as Record<string, { type: "idle" | "busy" }>,
session_working: () => false,
agent: [{ name: "build", mode: "task", hidden: false }],
command: [{ name: "fix", description: "Run fix command", source: "project" }],
})
export function useSync() {
return {
data,
set(...input: unknown[]) {
;(setData as (...args: unknown[]) => void)(...input)
const sync = {
data,
set(...input: unknown[]) {
;(setData as (...args: unknown[]) => void)(...input)
},
session: {
get(id: string) {
return { id }
},
session: {
get(id: string) {
return { id }
},
optimistic: {
add() {},
remove() {},
},
optimistic: {
add() {},
remove() {},
},
}
},
}
export function useSync() {
return () => sync
}
@@ -11,6 +11,10 @@ export function useNavigate() {
return () => undefined
}
export function useSearchParams<T extends Record<string, string>>() {
return [{} as Partial<T>, () => undefined] as const
}
export function useLocation() {
return {
pathname: "/story/session/story-session",
@@ -1,4 +1,5 @@
import "@opencode-ai/ui/styles/tailwind"
import "@opencode-ai/ui/v2/styles/tailwind.css"
import { createEffect, onCleanup, onMount } from "solid-js"
import addonA11y from "@storybook/addon-a11y"