chore: initialize qiming workspace repository
This commit is contained in:
67
qimingcode/packages/storybook/.storybook/main.ts
Normal file
67
qimingcode/packages/storybook/.storybook/main.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { defineMain } from "storybook-solidjs-vite"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import tailwindcss from "@tailwindcss/vite"
|
||||
import { playgroundCss } from "./playground-css-plugin"
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url))
|
||||
const ui = path.resolve(here, "../../ui")
|
||||
const app = path.resolve(here, "../../app/src")
|
||||
const mocks = path.resolve(here, "./mocks")
|
||||
|
||||
export default defineMain({
|
||||
framework: {
|
||||
name: "storybook-solidjs-vite",
|
||||
options: {},
|
||||
},
|
||||
addons: [
|
||||
"@storybook/addon-onboarding",
|
||||
"@storybook/addon-docs",
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-a11y",
|
||||
"@storybook/addon-vitest",
|
||||
],
|
||||
stories: ["../../ui/src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||
async viteFinal(config) {
|
||||
const { mergeConfig, searchForWorkspaceRoot } = await import("vite")
|
||||
return mergeConfig(config, {
|
||||
plugins: [tailwindcss(), playgroundCss()],
|
||||
resolve: {
|
||||
dedupe: ["solid-js", "solid-js/web", "@solidjs/meta"],
|
||||
alias: [
|
||||
{ find: "@solidjs/router", replacement: path.resolve(mocks, "solid-router.tsx") },
|
||||
{ find: /^@\/context\/local$/, replacement: path.resolve(mocks, "app/context/local.ts") },
|
||||
{ find: /^@\/context\/file$/, replacement: path.resolve(mocks, "app/context/file.ts") },
|
||||
{ find: /^@\/context\/prompt$/, replacement: path.resolve(mocks, "app/context/prompt.ts") },
|
||||
{ find: /^@\/context\/layout$/, replacement: path.resolve(mocks, "app/context/layout.ts") },
|
||||
{ find: /^@\/context\/sdk$/, replacement: path.resolve(mocks, "app/context/sdk.ts") },
|
||||
{ find: /^@\/context\/sync$/, replacement: path.resolve(mocks, "app/context/sync.ts") },
|
||||
{ find: /^@\/context\/comments$/, replacement: path.resolve(mocks, "app/context/comments.ts") },
|
||||
{ find: /^@\/context\/command$/, replacement: path.resolve(mocks, "app/context/command.ts") },
|
||||
{ find: /^@\/context\/permission$/, replacement: path.resolve(mocks, "app/context/permission.ts") },
|
||||
{ find: /^@\/context\/language$/, replacement: path.resolve(mocks, "app/context/language.ts") },
|
||||
{ find: /^@\/context\/platform$/, replacement: path.resolve(mocks, "app/context/platform.ts") },
|
||||
{ find: /^@\/context\/global-sync$/, replacement: path.resolve(mocks, "app/context/global-sync.ts") },
|
||||
{ find: /^@\/hooks\/use-providers$/, replacement: path.resolve(mocks, "app/hooks/use-providers.ts") },
|
||||
{
|
||||
find: /^@\/components\/dialog-select-model$/,
|
||||
replacement: path.resolve(mocks, "app/components/dialog-select-model.tsx"),
|
||||
},
|
||||
{
|
||||
find: /^@\/components\/dialog-select-model-unpaid$/,
|
||||
replacement: path.resolve(mocks, "app/components/dialog-select-model-unpaid.tsx"),
|
||||
},
|
||||
{ find: "@", replacement: app },
|
||||
],
|
||||
},
|
||||
worker: {
|
||||
format: "es",
|
||||
},
|
||||
server: {
|
||||
fs: {
|
||||
allow: [searchForWorkspaceRoot(process.cwd()), ui, app, mocks],
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
11
qimingcode/packages/storybook/.storybook/manager.ts
Normal file
11
qimingcode/packages/storybook/.storybook/manager.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { addons, types } from "storybook/manager-api"
|
||||
import { ThemeTool } from "./theme-tool"
|
||||
|
||||
addons.register("opencode/theme-toggle", () => {
|
||||
addons.add("opencode/theme-toggle/tool", {
|
||||
type: types.TOOL,
|
||||
title: "Theme",
|
||||
match: ({ viewMode }) => viewMode === "story" || viewMode === "docs",
|
||||
render: ThemeTool,
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
export function DialogSelectModelUnpaid() {
|
||||
return <div data-component="dialog-select-model-unpaid">Select model</div>
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { splitProps } from "solid-js"
|
||||
|
||||
export function ModelSelectorPopover(props: { triggerAs: any; triggerProps?: Record<string, unknown>; children: any }) {
|
||||
const [local] = splitProps(props, ["triggerAs", "triggerProps", "children"])
|
||||
const Trigger = local.triggerAs
|
||||
return <Trigger {...(local.triggerProps ?? {})}>{local.children}</Trigger>
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
const keybinds: Record<string, string> = {
|
||||
"file.attach": "mod+u",
|
||||
"prompt.mode.shell": "mod+shift+x",
|
||||
"prompt.mode.normal": "mod+shift+e",
|
||||
"permissions.autoaccept": "mod+shift+a",
|
||||
"agent.cycle": "mod+.",
|
||||
"model.choose": "mod+m",
|
||||
"model.variant.cycle": "mod+shift+m",
|
||||
}
|
||||
|
||||
export function useCommand() {
|
||||
return {
|
||||
options: [],
|
||||
register() {
|
||||
return () => undefined
|
||||
},
|
||||
trigger() {},
|
||||
keybind(id: string) {
|
||||
return keybinds[id]
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { createSignal } from "solid-js"
|
||||
|
||||
type Comment = {
|
||||
id: string
|
||||
file: string
|
||||
selection: { start: number; end: number }
|
||||
comment: string
|
||||
time: number
|
||||
}
|
||||
|
||||
const [list, setList] = createSignal<Comment[]>([])
|
||||
const [focus, setFocus] = createSignal<{ file: string; id: string } | null>(null)
|
||||
const [active, setActive] = createSignal<{ file: string; id: string } | null>(null)
|
||||
|
||||
export function useComments() {
|
||||
return {
|
||||
all: list,
|
||||
replace(next: Comment[]) {
|
||||
setList(next)
|
||||
},
|
||||
remove(file: string, id: string) {
|
||||
setList((current) => current.filter((item) => !(item.file === file && item.id === id)))
|
||||
},
|
||||
clear() {
|
||||
setList([])
|
||||
setFocus(null)
|
||||
setActive(null)
|
||||
},
|
||||
focus,
|
||||
setFocus,
|
||||
active,
|
||||
setActive,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
export type FileSelection = {
|
||||
startLine: number
|
||||
startChar: number
|
||||
endLine: number
|
||||
endChar: number
|
||||
}
|
||||
|
||||
export type SelectedLineRange = {
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
export function selectionFromLines(selection?: SelectedLineRange): FileSelection | undefined {
|
||||
if (!selection) return undefined
|
||||
return {
|
||||
startLine: selection.start,
|
||||
startChar: 0,
|
||||
endLine: selection.end,
|
||||
endChar: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const pool = [
|
||||
"src/session/timeline.tsx",
|
||||
"src/session/composer.tsx",
|
||||
"src/components/prompt-input.tsx",
|
||||
"src/components/session-todo-dock.tsx",
|
||||
"README.md",
|
||||
]
|
||||
|
||||
export function useFile() {
|
||||
return {
|
||||
tab(path: string) {
|
||||
return `file:${path}`
|
||||
},
|
||||
pathFromTab(tab: string) {
|
||||
if (!tab.startsWith("file:")) return ""
|
||||
return tab.slice(5)
|
||||
},
|
||||
load: async () => undefined,
|
||||
async searchFilesAndDirectories(query: string) {
|
||||
const text = query.trim().toLowerCase()
|
||||
if (!text) return pool
|
||||
return pool.filter((path) => path.toLowerCase().includes(text))
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { createStore } from "solid-js/store"
|
||||
|
||||
const provider = {
|
||||
all: [
|
||||
{
|
||||
id: "anthropic",
|
||||
models: {
|
||||
"claude-3-7-sonnet": {
|
||||
id: "claude-3-7-sonnet",
|
||||
name: "Claude 3.7 Sonnet",
|
||||
cost: { input: 1, output: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
connected: ["anthropic"],
|
||||
default: { anthropic: "claude-3-7-sonnet" },
|
||||
}
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
todo: {} as Record<string, any[]>,
|
||||
provider,
|
||||
session: [] as any[],
|
||||
config: { permission: {} },
|
||||
})
|
||||
|
||||
export function useGlobalSync() {
|
||||
return {
|
||||
data: {
|
||||
provider,
|
||||
session_todo: store.todo,
|
||||
},
|
||||
child() {
|
||||
return [store, setStore] as const
|
||||
},
|
||||
todo: {
|
||||
set(sessionID: string, todos: any[]) {
|
||||
setStore("todo", sessionID, todos)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
const dict: Record<string, string> = {
|
||||
"session.todo.title": "Todos",
|
||||
"session.todo.collapse": "Collapse todos",
|
||||
"session.todo.expand": "Expand todos",
|
||||
"prompt.loading": "Loading prompt...",
|
||||
"prompt.placeholder.normal": "Ask anything...",
|
||||
"prompt.placeholder.simple": "Ask anything...",
|
||||
"prompt.placeholder.shell": "Run a shell command... {{example}}",
|
||||
"prompt.placeholder.summarizeComment": "Summarize this comment",
|
||||
"prompt.placeholder.summarizeComments": "Summarize these comments",
|
||||
"prompt.action.attachFile": "Attach files",
|
||||
"prompt.action.send": "Send",
|
||||
"prompt.action.stop": "Stop",
|
||||
"prompt.attachment.remove": "Remove attachment",
|
||||
"prompt.dropzone.label": "Drop image to attach",
|
||||
"prompt.dropzone.file.label": "Drop file to attach",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.normal": "Prompt",
|
||||
"dialog.model.select.title": "Select model",
|
||||
"common.default": "Default",
|
||||
"common.key.esc": "Esc",
|
||||
"command.category.file": "File",
|
||||
"command.category.session": "Session",
|
||||
"command.agent.cycle": "Cycle agent",
|
||||
"command.model.choose": "Choose model",
|
||||
"command.model.variant.cycle": "Cycle model variant",
|
||||
"command.prompt.mode.shell": "Switch to shell mode",
|
||||
"command.prompt.mode.normal": "Switch to prompt mode",
|
||||
"command.permissions.autoaccept.enable": "Enable auto-accept",
|
||||
"command.permissions.autoaccept.disable": "Disable auto-accept",
|
||||
"prompt.example.1": "Refactor this function and keep behavior the same",
|
||||
"prompt.example.2": "Find the root cause of this error",
|
||||
"prompt.example.3": "Write tests for this module",
|
||||
"prompt.example.4": "Explain this diff",
|
||||
"prompt.example.5": "Optimize this query",
|
||||
"prompt.example.6": "Clean up this component",
|
||||
"prompt.example.7": "Summarize the recent changes",
|
||||
"prompt.example.8": "Add accessibility checks",
|
||||
"prompt.example.9": "Review this API design",
|
||||
"prompt.example.10": "Generate migration notes",
|
||||
"prompt.example.11": "Patch this bug",
|
||||
"prompt.example.12": "Make this animation smoother",
|
||||
"prompt.example.13": "Improve error handling",
|
||||
"prompt.example.14": "Document this feature",
|
||||
"prompt.example.15": "Refine these styles",
|
||||
"prompt.example.16": "Check edge cases",
|
||||
"prompt.example.17": "Help me write a commit message",
|
||||
"prompt.example.18": "Reduce re-renders in this component",
|
||||
"prompt.example.19": "Verify keyboard navigation",
|
||||
"prompt.example.20": "Make this copy clearer",
|
||||
"prompt.example.21": "Add telemetry for this flow",
|
||||
"prompt.example.22": "Compare these two implementations",
|
||||
"prompt.example.23": "Create a minimal reproduction",
|
||||
"prompt.example.24": "Suggest naming improvements",
|
||||
"prompt.example.25": "What should we test next?",
|
||||
}
|
||||
|
||||
function render(template: string, params?: Record<string, unknown>) {
|
||||
if (!params) return template
|
||||
return template.replace(/\{\{([^}]+)\}\}/g, (_, key: string) => {
|
||||
const value = params[key.trim()]
|
||||
if (value === undefined || value === null) return ""
|
||||
// oxlint-disable-next-line no-base-to-string -- value is Record<string, unknown>, always coerced intentionally
|
||||
return String(value)
|
||||
})
|
||||
}
|
||||
|
||||
export function useLanguage() {
|
||||
return {
|
||||
locale: () => "en" as const,
|
||||
t(key: string, params?: Record<string, unknown>) {
|
||||
return render(dict[key] ?? key, params)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { createSignal } from "solid-js"
|
||||
|
||||
const [all, setAll] = createSignal<string[]>([])
|
||||
const [active, setActive] = createSignal<string | undefined>(undefined)
|
||||
const [reviewOpen, setReviewOpen] = createSignal(false)
|
||||
|
||||
const tabs = {
|
||||
all,
|
||||
active,
|
||||
open(tab: string) {
|
||||
setAll((current) => (current.includes(tab) ? current : [...current, tab]))
|
||||
},
|
||||
setActive(tab: string) {
|
||||
if (!all().includes(tab)) {
|
||||
tabs.open(tab)
|
||||
}
|
||||
setActive(tab)
|
||||
},
|
||||
}
|
||||
|
||||
const view = {
|
||||
reviewPanel: {
|
||||
opened: reviewOpen,
|
||||
open() {
|
||||
setReviewOpen(true)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export function useLayout() {
|
||||
return {
|
||||
tabs: () => tabs,
|
||||
view: () => view,
|
||||
fileTree: {
|
||||
setTab() {},
|
||||
},
|
||||
handoff: {
|
||||
setTabs() {},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { createSignal } from "solid-js"
|
||||
|
||||
const model = {
|
||||
id: "claude-3-7-sonnet",
|
||||
name: "Claude 3.7 Sonnet",
|
||||
provider: { id: "anthropic" },
|
||||
variants: { fast: {}, thinking: {} },
|
||||
}
|
||||
|
||||
const agents = [{ name: "build" }, { name: "review" }, { name: "plan" }]
|
||||
|
||||
const [agent, setAgent] = createSignal(agents[0].name)
|
||||
const [variant, setVariant] = createSignal<string | undefined>(undefined)
|
||||
|
||||
export function useLocal() {
|
||||
return {
|
||||
slug: () => "c3Rvcnk=",
|
||||
agent: {
|
||||
list: () => agents,
|
||||
current: () => agents.find((item) => item.name === agent()) ?? agents[0],
|
||||
set(value?: string) {
|
||||
if (!value) {
|
||||
setAgent(agents[0].name)
|
||||
return
|
||||
}
|
||||
const hit = agents.find((item) => item.name === value)
|
||||
setAgent(hit?.name ?? agents[0].name)
|
||||
},
|
||||
},
|
||||
model: {
|
||||
current: () => model,
|
||||
variant: {
|
||||
list: () => Object.keys(model.variants),
|
||||
current: () => variant(),
|
||||
set(next?: string) {
|
||||
setVariant(next)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
const accepted = new Set<string>()
|
||||
|
||||
function key(sessionID: string, directory?: string) {
|
||||
return `${directory ?? ""}:${sessionID}`
|
||||
}
|
||||
|
||||
export function usePermission() {
|
||||
return {
|
||||
autoResponds() {
|
||||
return false
|
||||
},
|
||||
isAutoAccepting(sessionID: string, directory?: string) {
|
||||
return accepted.has(key(sessionID, directory))
|
||||
},
|
||||
toggleAutoAccept(sessionID: string, directory?: string) {
|
||||
const next = key(sessionID, directory)
|
||||
if (accepted.has(next)) {
|
||||
accepted.delete(next)
|
||||
return
|
||||
}
|
||||
accepted.add(next)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { Platform } from "../../../../../app/src/context/platform"
|
||||
|
||||
const value: Platform = {
|
||||
platform: "web",
|
||||
openLink() {},
|
||||
restart: async () => {},
|
||||
back() {},
|
||||
forward() {},
|
||||
notify: async () => {},
|
||||
fetch: globalThis.fetch.bind(globalThis),
|
||||
parseMarkdown: async (markdown: string) => markdown,
|
||||
}
|
||||
|
||||
export function usePlatform() {
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { createSignal } from "solid-js"
|
||||
|
||||
interface PartBase {
|
||||
content: string
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
export interface TextPart extends PartBase {
|
||||
type: "text"
|
||||
}
|
||||
|
||||
export interface FileAttachmentPart extends PartBase {
|
||||
type: "file"
|
||||
path: string
|
||||
}
|
||||
|
||||
export interface AgentPart extends PartBase {
|
||||
type: "agent"
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface ImageAttachmentPart {
|
||||
type: "image"
|
||||
id: string
|
||||
filename: string
|
||||
mime: string
|
||||
dataUrl: string
|
||||
}
|
||||
|
||||
export type ContentPart = TextPart | FileAttachmentPart | AgentPart | ImageAttachmentPart
|
||||
export type Prompt = ContentPart[]
|
||||
|
||||
type ContextItem = {
|
||||
key: string
|
||||
type: "file"
|
||||
path: string
|
||||
selection?: { startLine: number; startChar: number; endLine: number; endChar: number }
|
||||
comment?: string
|
||||
commentID?: string
|
||||
commentOrigin?: "review" | "file"
|
||||
preview?: string
|
||||
}
|
||||
|
||||
export const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
|
||||
|
||||
function clonePart(part: ContentPart): ContentPart {
|
||||
if (part.type === "image") return { ...part }
|
||||
if (part.type === "agent") return { ...part }
|
||||
if (part.type === "file") return { ...part }
|
||||
return { ...part }
|
||||
}
|
||||
|
||||
function clonePrompt(prompt: Prompt) {
|
||||
return prompt.map(clonePart)
|
||||
}
|
||||
|
||||
export function isPromptEqual(a: Prompt, b: Prompt) {
|
||||
if (a.length !== b.length) return false
|
||||
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[]>([])
|
||||
|
||||
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),
|
||||
set(next: Prompt, cursorPosition?: number) {
|
||||
setPrompt(clonePrompt(next))
|
||||
if (cursorPosition !== undefined) setCursor(cursorPosition)
|
||||
},
|
||||
reset() {
|
||||
setPrompt(clonePrompt(DEFAULT_PROMPT))
|
||||
setCursor(0)
|
||||
setItems((current) => current.filter((item) => !!item.comment?.trim()))
|
||||
},
|
||||
context: {
|
||||
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])
|
||||
},
|
||||
remove(key: string) {
|
||||
setItems((current) => current.filter((item) => item.key !== key))
|
||||
},
|
||||
removeComment(path: string, commentID: string) {
|
||||
setItems((current) =>
|
||||
current.filter((item) => !(item.type === "file" && item.path === path && item.commentID === commentID)),
|
||||
)
|
||||
},
|
||||
updateComment(path: string, commentID: string, next: Partial<ContextItem>) {
|
||||
setItems((current) =>
|
||||
current.map((item) => {
|
||||
if (item.type !== "file" || item.path !== path || item.commentID !== commentID) return item
|
||||
return withKey({ ...item, ...next })
|
||||
}),
|
||||
)
|
||||
},
|
||||
replaceComments(next: Array<Omit<ContextItem, "key"> & { key?: string }>) {
|
||||
const nonComment = items().filter((item) => !item.comment?.trim())
|
||||
setItems([...nonComment, ...next.map(withKey)])
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
const make = (directory: string) => ({
|
||||
session: {
|
||||
create: async () => ({ data: { id: "story-session" } }),
|
||||
prompt: async () => ({ data: undefined }),
|
||||
shell: async () => ({ data: undefined }),
|
||||
command: async () => ({ data: undefined }),
|
||||
abort: async () => ({ data: undefined }),
|
||||
},
|
||||
worktree: {
|
||||
create: async () => ({ data: { directory: `${directory}/worktree-1` } }),
|
||||
},
|
||||
})
|
||||
|
||||
const root = "/tmp/story"
|
||||
|
||||
export function useSDK() {
|
||||
return {
|
||||
directory: root,
|
||||
url: "http://localhost:4096",
|
||||
client: make(root),
|
||||
createClient(input: { directory: string }) {
|
||||
return make(input.directory)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { createStore } from "solid-js/store"
|
||||
|
||||
const [data, setData] = createStore({
|
||||
session: [] as Array<{ id: string; parentID?: string }>,
|
||||
permission: {} as Record<string, Array<{ id: string; sessionID: string; permission: string; patterns: string[] }>>,
|
||||
question: {} as Record<string, Array<{ id: string; questions: unknown[] }>>,
|
||||
session_diff: {} as Record<string, Array<{ file: string }>>,
|
||||
message: {
|
||||
"story-session": [] as Array<{ id: string; role: string }>,
|
||||
} as Record<string, Array<{ id: string; role: string }>>,
|
||||
session_status: {} as Record<string, { type: "idle" | "busy" }>,
|
||||
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)
|
||||
},
|
||||
session: {
|
||||
get(id: string) {
|
||||
return { id }
|
||||
},
|
||||
optimistic: {
|
||||
add() {},
|
||||
remove() {},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
const model_id = "claude-3-7-sonnet"
|
||||
|
||||
const provider = {
|
||||
id: "anthropic",
|
||||
models: {
|
||||
[model_id]: {
|
||||
id: model_id,
|
||||
name: "Claude 3.7 Sonnet",
|
||||
cost: { input: 1, output: 1 },
|
||||
variants: { fast: {}, thinking: {} },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export function useProviders() {
|
||||
return {
|
||||
all: () => [provider],
|
||||
default: () => ({ anthropic: model_id }),
|
||||
connected: () => [provider],
|
||||
paid: () => [provider],
|
||||
popular: () => [provider],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { ParentProps } from "solid-js"
|
||||
|
||||
export function useParams() {
|
||||
return {
|
||||
dir: "c3Rvcnk=",
|
||||
id: "story-session",
|
||||
}
|
||||
}
|
||||
|
||||
export function useNavigate() {
|
||||
return () => undefined
|
||||
}
|
||||
|
||||
export function useLocation() {
|
||||
return {
|
||||
pathname: "/story/session/story-session",
|
||||
search: "",
|
||||
hash: "",
|
||||
}
|
||||
}
|
||||
|
||||
export function MemoryRouter(props: ParentProps) {
|
||||
return props.children
|
||||
}
|
||||
|
||||
export function Route(props: ParentProps) {
|
||||
return props.children
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Vite plugin that exposes a POST endpoint for the timeline playground
|
||||
* to write CSS changes back to source files on disk.
|
||||
*
|
||||
* POST /__playground/apply-css
|
||||
* Body: { edits: Array<{ file: string; anchor: string; prop: string; value: string }> }
|
||||
*
|
||||
* For each edit the plugin finds `anchor` in the file, then locates the
|
||||
* next `prop: <anything>;` after it and replaces the value portion.
|
||||
* `file` is a basename resolved relative to packages/ui/src/components/.
|
||||
*/
|
||||
import type { Plugin } from "vite"
|
||||
import type { IncomingMessage, ServerResponse } from "node:http"
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url))
|
||||
const root = path.resolve(here, "../../ui/src/components")
|
||||
|
||||
const ENDPOINT = "/__playground/apply-css"
|
||||
|
||||
type Edit = { file: string; anchor: string; prop: string; value: string }
|
||||
type Result = { file: string; prop: string; ok: boolean; error?: string }
|
||||
|
||||
function applyEdits(content: string, edits: Edit[]): { content: string; results: Result[] } {
|
||||
const results: Result[] = []
|
||||
let out = content
|
||||
|
||||
for (const edit of edits) {
|
||||
const name = edit.file
|
||||
const idx = out.indexOf(edit.anchor)
|
||||
if (idx === -1) {
|
||||
results.push({ file: name, prop: edit.prop, ok: false, error: `Anchor not found: ${edit.anchor.slice(0, 50)}` })
|
||||
continue
|
||||
}
|
||||
|
||||
// From the anchor position, find the next occurrence of `prop: <value>`
|
||||
// We match `prop:` followed by any value up to `;`
|
||||
const after = out.slice(idx)
|
||||
const re = new RegExp(`(${escapeRegex(edit.prop)}\\s*:\\s*)([^;]+)(;)`)
|
||||
const match = re.exec(after)
|
||||
if (!match) {
|
||||
results.push({ file: name, prop: edit.prop, ok: false, error: `Property "${edit.prop}" not found after anchor` })
|
||||
continue
|
||||
}
|
||||
|
||||
const start = idx + match.index + match[1].length
|
||||
const end = start + match[2].length
|
||||
out = out.slice(0, start) + edit.value + out.slice(end)
|
||||
results.push({ file: name, prop: edit.prop, ok: true })
|
||||
}
|
||||
|
||||
return { content: out, results }
|
||||
}
|
||||
|
||||
function escapeRegex(s: string) {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
}
|
||||
|
||||
export function playgroundCss(): Plugin {
|
||||
return {
|
||||
name: "playground-css",
|
||||
configureServer(server) {
|
||||
server.middlewares.use((req: IncomingMessage, res: ServerResponse, next: () => void) => {
|
||||
if (req.url !== ENDPOINT) return next()
|
||||
if (req.method !== "POST") {
|
||||
res.statusCode = 405
|
||||
res.setHeader("Content-Type", "application/json")
|
||||
res.end(JSON.stringify({ error: "Method not allowed" }))
|
||||
return
|
||||
}
|
||||
|
||||
let data = ""
|
||||
req.on("data", (chunk: Buffer) => {
|
||||
data += chunk.toString()
|
||||
})
|
||||
req.on("end", () => {
|
||||
let payload: { edits: Edit[] }
|
||||
try {
|
||||
payload = JSON.parse(data)
|
||||
} catch {
|
||||
res.statusCode = 400
|
||||
res.setHeader("Content-Type", "application/json")
|
||||
res.end(JSON.stringify({ error: "Invalid JSON" }))
|
||||
return
|
||||
}
|
||||
|
||||
if (!Array.isArray(payload.edits)) {
|
||||
res.statusCode = 400
|
||||
res.setHeader("Content-Type", "application/json")
|
||||
res.end(JSON.stringify({ error: "Missing edits array" }))
|
||||
return
|
||||
}
|
||||
|
||||
// Group by file
|
||||
const grouped = new Map<string, Edit[]>()
|
||||
for (const edit of payload.edits) {
|
||||
if (!edit.file || !edit.anchor || !edit.prop || edit.value === undefined) continue
|
||||
const abs = path.resolve(root, edit.file)
|
||||
if (!abs.startsWith(root)) continue
|
||||
const key = abs
|
||||
if (!grouped.has(key)) grouped.set(key, [])
|
||||
grouped.get(key)!.push(edit)
|
||||
}
|
||||
|
||||
const results: Result[] = []
|
||||
|
||||
for (const [abs, edits] of grouped) {
|
||||
const name = path.basename(abs)
|
||||
if (!fs.existsSync(abs)) {
|
||||
for (const e of edits) results.push({ file: name, prop: e.prop, ok: false, error: "File not found" })
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(abs, "utf-8")
|
||||
const applied = applyEdits(content, edits)
|
||||
results.push(...applied.results)
|
||||
|
||||
if (applied.results.some((r) => r.ok)) {
|
||||
fs.writeFileSync(abs, applied.content, "utf-8")
|
||||
}
|
||||
} catch (err) {
|
||||
for (const e of edits) results.push({ file: name, prop: e.prop, ok: false, error: String(err) })
|
||||
}
|
||||
}
|
||||
|
||||
res.statusCode = 200
|
||||
res.setHeader("Content-Type", "application/json")
|
||||
res.end(JSON.stringify({ results }))
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
98
qimingcode/packages/storybook/.storybook/preview.tsx
Normal file
98
qimingcode/packages/storybook/.storybook/preview.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import "@opencode-ai/ui/styles/tailwind"
|
||||
|
||||
import { createEffect, onCleanup, onMount } from "solid-js"
|
||||
import addonA11y from "@storybook/addon-a11y"
|
||||
import addonDocs from "@storybook/addon-docs"
|
||||
import { MetaProvider } from "@solidjs/meta"
|
||||
import { addons } from "storybook/preview-api"
|
||||
import { GLOBALS_UPDATED } from "storybook/internal/core-events"
|
||||
import { createJSXDecorator, definePreview } from "storybook-solidjs-vite"
|
||||
import { DialogProvider } from "@opencode-ai/ui/context/dialog"
|
||||
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
|
||||
import { ThemeProvider, useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
|
||||
import { Font } from "@opencode-ai/ui/font"
|
||||
|
||||
function resolveScheme(value: unknown): ColorScheme {
|
||||
if (value === "light" || value === "dark" || value === "system") return value
|
||||
return "system"
|
||||
}
|
||||
|
||||
const channel = addons.getChannel()
|
||||
|
||||
const Scheme = (props: { value?: unknown }) => {
|
||||
const theme = useTheme()
|
||||
const apply = (value?: unknown) => {
|
||||
theme.setColorScheme(resolveScheme(value))
|
||||
}
|
||||
createEffect(() => {
|
||||
apply(props.value)
|
||||
})
|
||||
createEffect(() => {
|
||||
const root = document.documentElement
|
||||
root.classList.remove("light", "dark")
|
||||
root.classList.add(theme.mode())
|
||||
})
|
||||
onMount(() => {
|
||||
const handler = (event: { globals?: Record<string, unknown> }) => {
|
||||
apply(event.globals?.theme)
|
||||
}
|
||||
channel.on(GLOBALS_UPDATED, handler)
|
||||
onCleanup(() => channel.off(GLOBALS_UPDATED, handler))
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
const frame = createJSXDecorator((Story, context) => {
|
||||
const override = context.parameters?.themes?.themeOverride
|
||||
const selected = context.globals?.theme
|
||||
const pick = override === "light" || override === "dark" ? override : selected
|
||||
const scheme = resolveScheme(pick)
|
||||
return (
|
||||
<MetaProvider>
|
||||
<Font />
|
||||
<ThemeProvider>
|
||||
<Scheme value={scheme} />
|
||||
<DialogProvider>
|
||||
<MarkedProvider>
|
||||
<div
|
||||
style={{
|
||||
"min-height": "100vh",
|
||||
padding: "24px",
|
||||
"background-color": "var(--background-base)",
|
||||
color: "var(--text-base)",
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
</div>
|
||||
</MarkedProvider>
|
||||
</DialogProvider>
|
||||
</ThemeProvider>
|
||||
</MetaProvider>
|
||||
)
|
||||
})
|
||||
|
||||
export default definePreview({
|
||||
addons: [addonDocs(), addonA11y()],
|
||||
decorators: [frame],
|
||||
globalTypes: {
|
||||
theme: {
|
||||
name: "Theme",
|
||||
description: "Global theme",
|
||||
defaultValue: "light",
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
actions: {
|
||||
argTypesRegex: "^on.*",
|
||||
},
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
a11y: {
|
||||
test: "todo",
|
||||
},
|
||||
},
|
||||
})
|
||||
21
qimingcode/packages/storybook/.storybook/theme-tool.ts
Normal file
21
qimingcode/packages/storybook/.storybook/theme-tool.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { createElement } from "react"
|
||||
import { useGlobals } from "storybook/manager-api"
|
||||
import { ToggleButton } from "storybook/internal/components"
|
||||
|
||||
export function ThemeTool() {
|
||||
const [globals, updateGlobals] = useGlobals()
|
||||
const mode = globals.theme === "dark" ? "dark" : "light"
|
||||
const toggle = () => {
|
||||
const next = mode === "dark" ? "light" : "dark"
|
||||
updateGlobals({ theme: next })
|
||||
}
|
||||
return createElement(
|
||||
ToggleButton,
|
||||
{
|
||||
title: "Toggle theme",
|
||||
active: mode === "dark",
|
||||
onClick: toggle,
|
||||
},
|
||||
mode === "dark" ? "Dark" : "Light",
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user