diff options
| author | Adam <[email protected]> | 2025-09-25 07:04:36 -0500 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-09-25 14:41:31 -0500 |
| commit | b207ed2b7b4080c3c6e0b2bc8430abcb4a894cad (patch) | |
| tree | 66a61ab8331c0eafd0435378d324ade8f38f191f | |
| parent | 945de4eddc249a6645b974525eed9f6795eac3aa (diff) | |
| download | opencode-b207ed2b7b4080c3c6e0b2bc8430abcb4a894cad.tar.gz opencode-b207ed2b7b4080c3c6e0b2bc8430abcb4a894cad.zip | |
wip: better desktop file status state and timeline
| -rw-r--r-- | packages/app/src/components/code.tsx | 2 | ||||
| -rw-r--r-- | packages/app/src/components/markdown.tsx | 3 | ||||
| -rw-r--r-- | packages/app/src/components/session-timeline.tsx | 324 | ||||
| -rw-r--r-- | packages/app/src/context/local.tsx | 90 | ||||
| -rw-r--r-- | packages/app/src/context/sync.tsx | 6 | ||||
| -rw-r--r-- | packages/app/src/pages/index.tsx | 17 | ||||
| -rw-r--r-- | packages/app/src/ui/icon.tsx | 5 |
7 files changed, 288 insertions, 159 deletions
diff --git a/packages/app/src/components/code.tsx b/packages/app/src/components/code.tsx index e4121d121..63f527c46 100644 --- a/packages/app/src/components/code.tsx +++ b/packages/app/src/components/code.tsx @@ -435,7 +435,7 @@ function transformerUnifiedDiff(): ShikiTransformer { out.push(s) } - return out.join("\n") + return out.join("\n").trimEnd() }, code(node) { if (isDiff) this.addClassToHast(node, "code-diff") diff --git a/packages/app/src/components/markdown.tsx b/packages/app/src/components/markdown.tsx index ab6232ecf..a60fad149 100644 --- a/packages/app/src/components/markdown.tsx +++ b/packages/app/src/components/markdown.tsx @@ -6,8 +6,7 @@ function strip(text: string): string { const match = text.match(wrappedRe) return match ? match[2] : text } - -export default function Markdown(props: { text: string; class?: string }) { +export function Markdown(props: { text: string; class?: string }) { const marked = useMarked() const [html] = createResource( () => strip(props.text), diff --git a/packages/app/src/components/session-timeline.tsx b/packages/app/src/components/session-timeline.tsx index ac8519a9c..99fa5fac8 100644 --- a/packages/app/src/components/session-timeline.tsx +++ b/packages/app/src/components/session-timeline.tsx @@ -1,5 +1,5 @@ import { useLocal, useSync } from "@/context" -import { Collapsible, Icon, type IconProps } from "@/ui" +import { Collapsible, Icon } from "@/ui" import type { Part, ToolPart } from "@opencode-ai/sdk" import { DateTime } from "luxon" import { @@ -13,58 +13,14 @@ import { type ParentProps, createEffect, createMemo, + Show, } from "solid-js" import { getFilename } from "@/utils" -import Markdown from "./markdown" +import { Markdown } from "./markdown" import { Code } from "./code" import { createElementSize } from "@solid-primitives/resize-observer" import { createScrollPosition } from "@solid-primitives/scroll" -function TimelineIcon(props: { name: IconProps["name"]; class?: string }) { - return ( - <div - classList={{ - "relative flex flex-none self-start items-center justify-center bg-background h-6 w-6": true, - [props.class ?? ""]: !!props.class, - }} - > - <Icon name={props.name} class="text-text/40" size={18} /> - </div> - ) -} - -function CollapsibleTimelineIcon(props: { name: IconProps["name"]; class?: string }) { - return ( - <> - <TimelineIcon - name={props.name} - class={`group-hover/li:hidden group-has-[[data-expanded]]/li:hidden ${props.class ?? ""}`} - /> - <TimelineIcon - name="chevron-right" - class={`hidden group-hover/li:flex group-has-[[data-expanded]]/li:hidden ${props.class ?? ""}`} - /> - <TimelineIcon name="chevron-down" class={`hidden group-has-[[data-expanded]]/li:flex ${props.class ?? ""}`} /> - </> - ) -} - -function ToolIcon(props: { part: ToolPart }) { - return ( - <Switch fallback={<TimelineIcon name="hammer" />}> - <Match when={props.part.tool === "read"}> - <TimelineIcon name="file" /> - </Match> - <Match when={props.part.tool === "edit"}> - <CollapsibleTimelineIcon name="pencil" /> - </Match> - <Match when={props.part.tool === "write"}> - <CollapsibleTimelineIcon name="file-plus" /> - </Match> - </Switch> - ) -} - function Part(props: ParentProps & ComponentProps<"div">) { const [local, others] = splitProps(props, ["class", "classList", "children"]) return ( @@ -97,9 +53,13 @@ function CollapsiblePart(props: { title: ParentProps["children"] } & ParentProps } function ReadToolPart(props: { part: ToolPart }) { + const sync = useSync() const local = useLocal() return ( <Switch> + <Match when={props.part.state.status === "pending"}> + <Part>Reading file...</Part> + </Match> <Match when={props.part.state.status === "completed" && props.part.state}> {(state) => { const path = state().input["filePath"] as string @@ -110,13 +70,27 @@ function ReadToolPart(props: { part: ToolPart }) { ) }} </Match> + <Match when={props.part.state.status === "error" && props.part.state}> + {(state) => ( + <div> + <Part> + <span class="text-text-muted">Read</span> {getFilename(state().input["filePath"] as string)} + </Part> + <div class="text-error">{sync.sanitize(state().error)}</div> + </div> + )} + </Match> </Switch> ) } function EditToolPart(props: { part: ToolPart }) { + const sync = useSync() return ( <Switch> + <Match when={props.part.state.status === "pending"}> + <Part>Preparing edit...</Part> + </Match> <Match when={props.part.state.status === "completed" && props.part.state}> {(state) => ( <CollapsiblePart @@ -135,13 +109,30 @@ function EditToolPart(props: { part: ToolPart }) { </CollapsiblePart> )} </Match> + <Match when={props.part.state.status === "error" && props.part.state}> + {(state) => ( + <CollapsiblePart + title={ + <> + <span class="text-text-muted">Edit</span> {getFilename(state().input["filePath"] as string)} + </> + } + > + <div class="text-error">{sync.sanitize(state().error)}</div> + </CollapsiblePart> + )} + </Match> </Switch> ) } function WriteToolPart(props: { part: ToolPart }) { + const sync = useSync() return ( <Switch> + <Match when={props.part.state.status === "pending"}> + <Part>Preparing write...</Part> + </Match> <Match when={props.part.state.status === "completed" && props.part.state}> {(state) => ( <CollapsiblePart @@ -155,38 +146,98 @@ function WriteToolPart(props: { part: ToolPart }) { </CollapsiblePart> )} </Match> + <Match when={props.part.state.status === "error" && props.part.state}> + {(state) => ( + <div> + <Part> + <span class="text-text-muted">Write</span> {getFilename(state().input["filePath"] as string)} + </Part> + <div class="text-error">{sync.sanitize(state().error)}</div> + </div> + )} + </Match> </Switch> ) } -function ToolPart(props: { part: ToolPart }) { +function BashToolPart(props: { part: ToolPart }) { + const sync = useSync() return ( - <Switch - fallback={ - <div class="flex-auto min-w-0 text-xs"> - {props.part.type}:{props.part.tool} - </div> - } - > - <Match when={props.part.tool === "read"}> - <div class="min-w-0 flex-auto"> - <ReadToolPart part={props.part} /> - </div> + <Switch> + <Match when={props.part.state.status === "pending"}> + <Part>Writing shell command...</Part> </Match> - <Match when={props.part.tool === "edit"}> - <div class="min-w-0 flex-auto"> - <EditToolPart part={props.part} /> - </div> + <Match when={props.part.state.status === "completed" && props.part.state}> + {(state) => ( + <CollapsiblePart + defaultOpen + title={ + <> + <span class="text-text-muted">Run command:</span> {state().input["command"]} + </> + } + > + <Markdown text={`\`\`\`command\n${state().input["command"]}\n${state().output}\`\`\``} /> + </CollapsiblePart> + )} </Match> - <Match when={props.part.tool === "write"}> - <div class="min-w-0 flex-auto"> - <WriteToolPart part={props.part} /> - </div> + <Match when={props.part.state.status === "error" && props.part.state}> + {(state) => ( + <CollapsiblePart + title={ + <> + <span class="text-text-muted">Shell</span> {state().input["command"]} + </> + } + > + <div class="text-error">{sync.sanitize(state().error)}</div> + </CollapsiblePart> + )} </Match> </Switch> ) } +function ToolPart(props: { part: ToolPart }) { + // read + // edit + // write + // bash + // ls + // glob + // grep + // todowrite + // todoread + // webfetch + // websearch + // patch + // task + return ( + <div class="min-w-0 flex-auto text-xs"> + <Switch + fallback={ + <span> + {props.part.type}:{props.part.tool} + </span> + } + > + <Match when={props.part.tool === "read"}> + <ReadToolPart part={props.part} /> + </Match> + <Match when={props.part.tool === "edit"}> + <EditToolPart part={props.part} /> + </Match> + <Match when={props.part.tool === "write"}> + <WriteToolPart part={props.part} /> + </Match> + <Match when={props.part.tool === "bash"}> + <BashToolPart part={props.part} /> + </Match> + </Switch> + </div> + ) +} + export default function SessionTimeline(props: { session: string; class?: string }) { const sync = useSync() const [scrollElement, setScrollElement] = createSignal<HTMLElement | undefined>(undefined) @@ -196,6 +247,7 @@ export default function SessionTimeline(props: { session: string; class?: string const scroll = createScrollPosition(scrollElement) onMount(() => sync.session.sync(props.session)) + const session = createMemo(() => sync.session.get(props.session)) const messages = createMemo(() => sync.data.message[props.session] ?? []) const working = createMemo(() => { const last = messages()[messages().length - 1] @@ -285,60 +337,33 @@ export default function SessionTimeline(props: { session: string; class?: string <div ref={setRoot} classList={{ - "p-4 select-text flex flex-col gap-y-8": true, + "p-4 select-text flex flex-col gap-y-1": true, [props.class ?? ""]: !!props.class, }} > - <For each={messages()}> - {(message) => ( - <ul role="list" class="space-y-2"> + <ul role="list" class="flex flex-col gap-1"> + <For each={messages()}> + {(message) => ( <For each={sync.data.part[message.id]?.filter(valid)}> {(part) => ( - <li classList={{ "relative group/li flex gap-x-4 min-w-0 w-full": true }}> - <div - classList={{ - "absolute top-0 left-0 flex w-6 justify-center": true, - "last:h-10 not-last:-bottom-10": true, - }} - > - <div class="w-px bg-border-subtle" /> - </div> - <Switch - fallback={ - <div class="m-0.5 relative flex size-5 flex-none items-center justify-center bg-background"> - <div class="size-1 rounded-full bg-text/10 ring ring-text/20" /> - </div> - } - > - <Match when={part.type === "text"}> - <Switch> - <Match when={message.role === "user"}> - <TimelineIcon name="avatar-square" /> - </Match> - <Match when={message.role === "assistant"}> - <TimelineIcon name="sparkles" /> - </Match> - </Switch> - </Match> - <Match when={part.type === "reasoning"}> - <CollapsibleTimelineIcon name="brain" /> - </Match> - <Match when={part.type === "tool" && part}>{(part) => <ToolIcon part={part()} />}</Match> - </Switch> + <li class="group/li"> <Switch fallback={<div class="flex-auto min-w-0 text-xs mt-1 text-left">{part.type}</div>}> <Match when={part.type === "text" && part}> {(part) => ( <Switch> <Match when={message.role === "user"}> - <div class="w-full flex flex-col items-end justify-stretch gap-y-1.5 min-w-0"> + <div class="w-full flex flex-col items-end justify-stretch gap-y-1.5 min-w-0 mt-5 group-first/li:mt-0"> <p class="w-full rounded-md p-3 ring-1 ring-text/15 ring-inset text-xs bg-background-panel"> <span class="font-medium text-text whitespace-pre-wrap break-words">{part().text}</span> </p> - <p class="text-xs text-text-muted">12:07pm · adam</p> + <p class="text-xs text-text-muted"> + {DateTime.fromMillis(message.time.created).toRelative()} ·{" "} + {sync.data.config.username ?? "user"} + </p> </div> </Match> <Match when={message.role === "assistant"}> - <Markdown text={part().text} class="text-text" /> + <Markdown text={sync.sanitize(part().text)} class="text-text mt-1" /> </Match> </Switch> )} @@ -347,9 +372,11 @@ export default function SessionTimeline(props: { session: string; class?: string {(part) => ( <CollapsiblePart title={ - <> - <span class="text-text-muted">Thought</span> for {duration(part())}s - </> + <Switch fallback={<span class="text-text-muted">Thinking</span>}> + <Match when={part().time.end}> + <span class="text-text-muted">Thought</span> for {duration(part())}s + </Match> + </Switch> } > <Markdown text={part().text} /> @@ -361,9 +388,84 @@ export default function SessionTimeline(props: { session: string; class?: string </li> )} </For> - </ul> - )} - </For> + )} + </For> + </ul> + <Show when={false}> + <Collapsible defaultOpen={false}> + <Collapsible.Trigger> + <div class="mt-12 ml-1 flex items-center gap-x-2 text-xs text-text-muted"> + <Icon name="file-code" size={16} /> + <span>Raw Session Data</span> + <Collapsible.Arrow size={18} class="text-text-muted" /> + </div> + </Collapsible.Trigger> + <Collapsible.Content class="mt-5"> + <ul role="list" class="space-y-2"> + <li> + <Collapsible> + <Collapsible.Trigger> + <div class="flex items-center gap-x-2 text-xs text-text-muted ml-1"> + <Icon name="file-code" size={16} /> + <span>session</span> + <Collapsible.Arrow size={18} class="text-text-muted" /> + </div> + </Collapsible.Trigger> + <Collapsible.Content> + <Code path="session.json" code={JSON.stringify(session(), null, 2)} class="[&_code]:pb-0!" /> + </Collapsible.Content> + </Collapsible> + </li> + <For each={messages()}> + {(message) => ( + <> + <li> + <Collapsible> + <Collapsible.Trigger> + <div class="flex items-center gap-x-2 text-xs text-text-muted ml-1"> + <Icon name="file-code" size={16} /> + <span>{message.role === "user" ? "user" : "assistant"}</span> + <Collapsible.Arrow size={18} class="text-text-muted" /> + </div> + </Collapsible.Trigger> + <Collapsible.Content> + <Code + path={message.id + ".json"} + code={JSON.stringify(message, null, 2)} + class="[&_code]:pb-0!" + /> + </Collapsible.Content> + </Collapsible> + </li> + <For each={sync.data.part[message.id]?.filter(valid)}> + {(part) => ( + <li> + <Collapsible> + <Collapsible.Trigger> + <div class="flex items-center gap-x-2 text-xs text-text-muted ml-1"> + <Icon name="file-code" size={16} /> + <span>{part.type}</span> + <Collapsible.Arrow size={18} class="text-text-muted" /> + </div> + </Collapsible.Trigger> + <Collapsible.Content> + <Code + path={message.id + "." + part.id + ".json"} + code={JSON.stringify(part, null, 2)} + class="[&_code]:pb-0!" + /> + </Collapsible.Content> + </Collapsible> + </li> + )} + </For> + </> + )} + </For> + </ul> + </Collapsible.Content> + </Collapsible> + </Show> </div> ) } diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx index b512ef470..03d180a4d 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -1,7 +1,7 @@ import { createStore, produce, reconcile } from "solid-js/store" import { batch, createContext, createEffect, createMemo, useContext, type ParentProps } from "solid-js" import { uniqueBy } from "remeda" -import type { FileContent, FileNode, Model, Provider } from "@opencode-ai/sdk" +import type { FileContent, FileNode, Model, Provider, File as FileStatus } from "@opencode-ai/sdk" import { useSDK, useEvent, useSync } from "@/context" export type LocalFile = FileNode & @@ -15,6 +15,7 @@ export type LocalFile = FileNode & view: "raw" | "diff-unified" | "diff-split" folded: string[] selectedChange: number + status: FileStatus }> export type TextSelection = LocalFile["selection"] export type View = LocalFile["view"] @@ -126,9 +127,33 @@ function init() { const opened = createMemo(() => store.opened.map((x) => store.node[x])) const changeset = createMemo(() => new Set(sync.data.changes.map((f) => f.path))) const changes = createMemo(() => Array.from(changeset()).sort((a, b) => a.localeCompare(b))) - const status = (path: string) => sync.data.changes.find((f) => f.path === path) + + createEffect((prev: FileStatus[]) => { + const removed = prev.filter((p) => !sync.data.changes.find((c) => c.path === p.path)) + for (const p of removed) { + setStore( + "node", + p.path, + produce((draft) => { + draft.status = undefined + draft.view = "raw" + }), + ) + load(p.path) + } + for (const p of sync.data.changes) { + if (store.node[p.path] === undefined) { + fetch(p.path).then(() => setStore("node", p.path, "status", p)) + } else { + setStore("node", p.path, "status", p) + } + } + return sync.data.changes + }, sync.data.changes) const changed = (path: string) => { + const node = store.node[path] + if (node?.status) return true const set = changeset() if (set.has(path)) return true for (const p of set) { @@ -138,24 +163,17 @@ function init() { } const resetNode = (path: string) => { - setStore("node", path, { - loaded: undefined, - pinned: undefined, - content: undefined, - selection: undefined, - scrollTop: undefined, - folded: undefined, - view: undefined, - selectedChange: undefined, - }) + setStore("node", path, undefined!) } + const relative = (path: string) => path.replace(sync.data.path.directory + "/", "") + const load = async (path: string) => { - const relative = path.replace(sync.data.path.directory + "/", "") - sdk.file.read({ query: { path: relative } }).then((x) => { + const relativePath = relative(path) + sdk.file.read({ query: { path: relativePath } }).then((x) => { setStore( "node", - relative, + relativePath, produce((draft) => { draft.loaded = true draft.content = x.data @@ -164,28 +182,31 @@ function init() { }) } - const open = async (path: string, options?: { pinned?: boolean; view?: LocalFile["view"] }) => { - const relative = path.replace(sync.data.path.directory + "/", "") - if (!store.node[relative]) { - const parent = relative.split("/").slice(0, -1).join("/") - if (parent) { - await list(parent) - } + const fetch = async (path: string) => { + const relativePath = relative(path) + const parent = relativePath.split("/").slice(0, -1).join("/") + if (parent) { + await list(parent) } + } + + const open = async (path: string, options?: { pinned?: boolean; view?: LocalFile["view"] }) => { + const relativePath = relative(path) + if (!store.node[relativePath]) await fetch(path) setStore("opened", (x) => { - if (x.includes(relative)) return x + if (x.includes(relativePath)) return x return [ ...opened() .filter((x) => x.pinned) .map((x) => x.path), - relative, + relativePath, ] }) - setStore("active", relative) + setStore("active", relativePath) if (options?.pinned) setStore("node", path, "pinned", true) - if (options?.view && store.node[relative].view === undefined) setStore("node", path, "view", options.view) - if (store.node[relative].loaded) return - return load(relative) + if (options?.view && store.node[relativePath].view === undefined) setStore("node", path, "view", options.view) + if (store.node[relativePath].loaded) return + return load(relativePath) } const list = async (path: string) => { @@ -212,10 +233,9 @@ function init() { if (part.type === "tool" && part.state.status === "completed") { switch (part.tool) { case "read": - console.log("read", part.state.input) break case "edit": - load(part.state.input["filePath"] as string) + // load(part.state.input["filePath"] as string) break default: break @@ -223,8 +243,10 @@ function init() { } break case "file.watcher.updated": - load(event.properties.file) - sync.load.changes() + setTimeout(sync.load.changes, 1000) + const relativePath = relative(event.properties.file) + if (relativePath.startsWith(".git/")) return + load(relativePath) break } }) @@ -298,9 +320,8 @@ function init() { setChangeIndex(path: string, index: number | undefined) { setStore("node", path, "selectedChange", index) }, - changed, changes, - status, + changed, children(path: string) { return Object.values(store.node).filter( (x) => @@ -310,6 +331,7 @@ function init() { ) }, search, + relative, } })() diff --git a/packages/app/src/context/sync.tsx b/packages/app/src/context/sync.tsx index a03b8b58a..796919676 100644 --- a/packages/app/src/context/sync.tsx +++ b/packages/app/src/context/sync.tsx @@ -1,6 +1,6 @@ import type { Message, Agent, Provider, Session, Part, Config, Path, File, FileNode } from "@opencode-ai/sdk" import { createStore, produce, reconcile } from "solid-js/store" -import { createContext, Show, useContext, type ParentProps } from "solid-js" +import { createContext, createMemo, Show, useContext, type ParentProps } from "solid-js" import { useSDK, useEvent } from "@/context" import { Binary } from "@/utils/binary" @@ -113,6 +113,9 @@ function init() { Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true)) + const sanitizer = createMemo(() => new RegExp(`${store.path.directory}/`, "g")) + const sanitize = (text: string) => text.replace(sanitizer(), "") + return { data: store, set: setStore, @@ -143,6 +146,7 @@ function init() { }, }, load, + sanitize, } } diff --git a/packages/app/src/pages/index.tsx b/packages/app/src/pages/index.tsx index 6890522e8..08c2a4a39 100644 --- a/packages/app/src/pages/index.tsx +++ b/packages/app/src/pages/index.tsx @@ -241,7 +241,7 @@ export default function Page() { return ( <div class="relative"> <div - class="fixed top-0 left-0 h-full border-r border-border-subtle/30 flex flex-col overflow-hidden" + class="fixed top-0 left-0 h-full border-r border-border-subtle/30 flex flex-col overflow-hidden bg-background z-10" style={`width: ${local.layout.leftWidth()}px`} > <Tabs class="relative flex flex-col h-full" defaultValue="files"> @@ -261,7 +261,7 @@ export default function Page() { <Tabs.Content value="changes" class="grow min-h-0 py-2 bg-background"> <Show when={local.file.changes().length} - fallback={<div class="px-2 text-xs text-text-muted">No changes yet</div>} + fallback={<div class="px-2 text-xs text-text-muted">No changes</div>} > <ul class=""> <For each={local.file.changes()}> @@ -299,7 +299,7 @@ export default function Page() { </div> <Show when={local.layout.rightPane()}> <div - class="fixed top-0 right-0 h-full border-l border-border-subtle/30 flex flex-col overflow-hidden" + class="fixed top-0 right-0 h-full border-l border-border-subtle/30 flex flex-col overflow-hidden bg-background z-10" style={`width: ${local.layout.rightWidth()}px`} > <div class="relative flex-1 min-h-0 overflow-y-auto overflow-x-hidden"> @@ -609,24 +609,21 @@ export default function Page() { } const TabVisual = (props: { file: LocalFile }) => { - const local = useLocal() return ( <div class="flex items-center gap-x-1.5"> <FileIcon node={props.file} class="" /> - <span - classList={{ "text-xs": true, "text-primary": local.file.changed(props.file.path), italic: !props.file.pinned }} - > + <span classList={{ "text-xs": true, "text-primary": !!props.file.status?.status, italic: !props.file.pinned }}> {props.file.name} </span> <span class="text-xs opacity-70"> <Switch> - <Match when={local.file.status(props.file.path)?.status === "modified"}> + <Match when={props.file.status?.status === "modified"}> <span class="text-primary">M</span> </Match> - <Match when={local.file.status(props.file.path)?.status === "added"}> + <Match when={props.file.status?.status === "added"}> <span class="text-success">A</span> </Match> - <Match when={local.file.status(props.file.path)?.status === "deleted"}> + <Match when={props.file.status?.status === "deleted"}> <span class="text-error">D</span> </Match> </Switch> diff --git a/packages/app/src/ui/icon.tsx b/packages/app/src/ui/icon.tsx index 0bbd17f5d..b9ddcfbc4 100644 --- a/packages/app/src/ui/icon.tsx +++ b/packages/app/src/ui/icon.tsx @@ -125,6 +125,11 @@ const icons = { columns: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4.75 6.75C4.75 5.64543 5.64543 4.75 6.75 4.75H17.25C18.3546 4.75 19.25 5.64543 19.25 6.75V17.25C19.25 18.3546 18.3546 19.25 17.25 19.25H6.75C5.64543 19.25 4.75 18.3546 4.75 17.25V6.75Z"></path><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 5V19"></path>', "open-pane": '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14.25 4.75v14.5m-3-8.5L9.75 12l1.5 1.25m-4.5 6h10.5a2 2 0 0 0 2-2V6.75a2 2 0 0 0-2-2H6.75a2 2 0 0 0-2 2v10.5a2 2 0 0 0 2 2Z"></path>', "close-pane": '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.75 4.75v14.5m3-8.5L14.25 12l-1.5 1.25M6.75 19.25h10.5a2 2 0 0 0 2-2V6.75a2 2 0 0 0-2-2H6.75a2 2 0 0 0-2 2v10.5a2 2 0 0 0 2 2Z"></path>', + "file-search": '<path fill="currentColor" d="M17.25 9.25V10a.75.75 0 0 0 .53-1.28l-.53.53Zm-4.5-4.5.53-.53a.75.75 0 0 0-.53-.22v.75ZM10.25 20a.75.75 0 0 0 0-1.5V20Zm7.427-3.383a.75.75 0 0 0-1.06 1.06l1.06-1.06Zm1.043 3.163a.75.75 0 1 0 1.06-1.06l-1.06 1.06Zm-.94-11.06-4.5-4.5-1.06 1.06 4.5 4.5 1.06-1.06ZM12.75 4h-6v1.5h6V4ZM4 6.75v10.5h1.5V6.75H4ZM6.75 20h3.5v-1.5h-3.5V20ZM12 4.75v3.5h1.5v-3.5H12ZM13.75 10h3.5V8.5h-3.5V10ZM12 8.25c0 .966.784 1.75 1.75 1.75V8.5a.25.25 0 0 1-.25-.25H12Zm-8 9A2.75 2.75 0 0 0 6.75 20v-1.5c-.69 0-1.25-.56-1.25-1.25H4ZM6.75 4A2.75 2.75 0 0 0 4 6.75h1.5c0-.69.56-1.25 1.25-1.25V4Zm8.485 14.47a3.235 3.235 0 0 0 3.236-3.235h-1.5c0 .959-.777 1.736-1.736 1.736v1.5Zm0-4.97c.959 0 1.736.777 1.736 1.735h1.5A3.235 3.235 0 0 0 15.235 12v1.5Zm0-1.5A3.235 3.235 0 0 0 12 15.235h1.5c0-.958.777-1.735 1.735-1.735V12Zm0 4.97a1.735 1.735 0 0 1-1.735-1.735H12a3.235 3.235 0 0 0 3.235 3.236v-1.5Zm1.382.707 2.103 2.103 1.06-1.06-2.103-2.103-1.06 1.06Z"></path>', + "folder-search": '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10.25 19.25h-3.5a2 2 0 0 1-2-2v-9.5h12.5a2 2 0 0 1 2 2v.5m-5.75-2.5-.931-1.958a2 2 0 0 0-1.756-1.042H6.75a2 2 0 0 0-2 2V11m12.695 6.445 1.805 1.805m-3.75-1a2.75 2.75 0 1 0 0-5.5 2.75 2.75 0 0 0 0 5.5Z"></path>', + search: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19.25 19.25L15.5 15.5M4.75 11C4.75 7.54822 7.54822 4.75 11 4.75C14.4518 4.75 17.25 7.54822 17.25 11C17.25 14.4518 14.4518 17.25 11 17.25C7.54822 17.25 4.75 14.4518 4.75 11Z"></path>', + "web-search": '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19.25 8.25v-.5a2 2 0 0 0-2-2H6.75a2 2 0 0 0-2 2v.5m14.5 0H4.75m14.5 0v2m-14.5-2v8a2 2 0 0 0 2 2h2.5m7.743-1.257 2.257 2.257m-4.015-1.53a2.485 2.485 0 1 0 0-4.97 2.485 2.485 0 0 0 0 4.97Z"></path>', + loading: '<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 4.75v1.5m5.126.624L16 8m3.25 4h-1.5m-.624 5.126-1.768-1.768M12 16.75v2.5m-3.36-3.891-1.768 1.768M7.25 12h-2.5m3.891-3.358L6.874 6.874"></path>', } as const export function Icon(props: IconProps) { |
