Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8abfd71684 | ||
|
|
0ca75544ab | ||
|
|
572a037e5d | ||
|
|
ad92181fa7 | ||
|
|
c56f4aa5d8 | ||
|
|
a344a766fd | ||
|
|
bca793d064 | ||
|
|
ad3c192837 | ||
|
|
5512231ca8 | ||
|
|
bad394cd49 | ||
|
|
3b97580621 | ||
|
|
cb88fe26aa | ||
|
|
e345b89ce5 | ||
|
|
26c7b240ba | ||
|
|
d327a2b1cf | ||
|
|
c1b03b728a | ||
|
|
2a2437bf22 | ||
|
|
4ccb82e81a | ||
|
|
92912219df | ||
|
|
bab3124e8b | ||
|
|
7a66ec6bc9 | ||
|
|
3a505b2691 | ||
|
|
20f43372f6 | ||
|
|
fb79dd7bf8 | ||
|
|
4025b655a4 | ||
|
|
7379903568 | ||
|
|
a685e7a805 | ||
|
|
ce7484b4f5 | ||
|
|
0bc1dcbe1b | ||
|
|
a69b339baf | ||
|
|
26f835cdd2 | ||
|
|
bd3d1413fd | ||
|
|
2c17a980ff | ||
|
|
b784c923a8 | ||
|
|
ea96f898c0 | ||
|
|
47435f6e17 |
@@ -69,6 +69,10 @@ Examples:
|
|||||||
- Provider integration issues
|
- Provider integration issues
|
||||||
- New, broken, or poor-quality models
|
- New, broken, or poor-quality models
|
||||||
|
|
||||||
|
#### acp
|
||||||
|
|
||||||
|
If the issue mentions acp support, assign acp label.
|
||||||
|
|
||||||
#### docs
|
#### docs
|
||||||
|
|
||||||
Add if the issue requests better documentation or docs updates.
|
Add if the issue requests better documentation or docs updates.
|
||||||
@@ -130,3 +134,7 @@ Determinism rules:
|
|||||||
- If "desktop" label is added, the tool will override assignee and randomly pick one Desktop / Web owner
|
- If "desktop" label is added, the tool will override assignee and randomly pick one Desktop / Web owner
|
||||||
|
|
||||||
In all other cases, choose the team/section with the most overlap with the issue and assign a member from that team at random.
|
In all other cases, choose the team/section with the most overlap with the issue and assign a member from that team at random.
|
||||||
|
|
||||||
|
ACP:
|
||||||
|
|
||||||
|
- rekram1-node (assign any acp issues to rekram1-node)
|
||||||
|
|||||||
@@ -68,17 +68,7 @@ export default tool({
|
|||||||
results.push("Dropped label: nix (issue does not mention nix)")
|
results.push("Dropped label: nix (issue does not mention nix)")
|
||||||
}
|
}
|
||||||
|
|
||||||
const assignee = nix
|
const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee
|
||||||
? "rekram1-node"
|
|
||||||
: web
|
|
||||||
? pick(TEAM.desktop)
|
|
||||||
: args.assignee === "jlongster"
|
|
||||||
? "thdxr"
|
|
||||||
: args.assignee
|
|
||||||
|
|
||||||
if (args.assignee === "jlongster" && assignee === "thdxr") {
|
|
||||||
results.push("Remapped assignee: jlongster -> thdxr (jlongster not assignable yet)")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (labels.includes("zen") && !zen) {
|
if (labels.includes("zen") && !zen) {
|
||||||
throw new Error("Only add the zen label when issue title/body contains 'zen'")
|
throw new Error("Only add the zen label when issue title/body contains 'zen'")
|
||||||
|
|||||||
@@ -71,13 +71,13 @@ const kindLabel = (kind: Kind) => {
|
|||||||
const kindTextColor = (kind: Kind) => {
|
const kindTextColor = (kind: Kind) => {
|
||||||
if (kind === "add") return "color: var(--icon-diff-add-base)"
|
if (kind === "add") return "color: var(--icon-diff-add-base)"
|
||||||
if (kind === "del") return "color: var(--icon-diff-delete-base)"
|
if (kind === "del") return "color: var(--icon-diff-delete-base)"
|
||||||
return "color: var(--icon-warning-active)"
|
return "color: var(--icon-diff-modified-base)"
|
||||||
}
|
}
|
||||||
|
|
||||||
const kindDotColor = (kind: Kind) => {
|
const kindDotColor = (kind: Kind) => {
|
||||||
if (kind === "add") return "background-color: var(--icon-diff-add-base)"
|
if (kind === "add") return "background-color: var(--icon-diff-add-base)"
|
||||||
if (kind === "del") return "background-color: var(--icon-diff-delete-base)"
|
if (kind === "del") return "background-color: var(--icon-diff-delete-base)"
|
||||||
return "background-color: var(--icon-warning-active)"
|
return "background-color: var(--icon-diff-modified-base)"
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibleKind = (node: FileNode, kinds?: ReadonlyMap<string, Kind>, marks?: Set<string>) => {
|
const visibleKind = (node: FileNode, kinds?: ReadonlyMap<string, Kind>, marks?: Set<string>) => {
|
||||||
@@ -447,12 +447,13 @@ export default function FileTree(props: {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={`flex flex-col gap-0.5 ${props.class ?? ""}`}>
|
<div data-component="filetree" class={`flex flex-col gap-0.5 ${props.class ?? ""}`}>
|
||||||
<For each={nodes()}>
|
<For each={nodes()}>
|
||||||
{(node) => {
|
{(node) => {
|
||||||
const expanded = () => file.tree.state(node.path)?.expanded ?? false
|
const expanded = () => file.tree.state(node.path)?.expanded ?? false
|
||||||
const deep = () => deeps().get(node.path) ?? -1
|
const deep = () => deeps().get(node.path) ?? -1
|
||||||
const kind = () => visibleKind(node, kinds(), marks())
|
const kind = () => visibleKind(node, kinds(), marks())
|
||||||
|
const active = () => !!kind() && !node.ignored
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
@@ -530,7 +531,30 @@ export default function FileTree(props: {
|
|||||||
onClick={() => props.onFileClick?.(node)}
|
onClick={() => props.onFileClick?.(node)}
|
||||||
>
|
>
|
||||||
<div class="w-4 shrink-0" />
|
<div class="w-4 shrink-0" />
|
||||||
<FileIcon node={node} class="text-icon-weak size-4" />
|
<Switch>
|
||||||
|
<Match when={node.ignored}>
|
||||||
|
<FileIcon
|
||||||
|
node={node}
|
||||||
|
class="size-4 filetree-icon filetree-icon--mono"
|
||||||
|
style="color: var(--icon-weak-base)"
|
||||||
|
mono
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={active()}>
|
||||||
|
<FileIcon
|
||||||
|
node={node}
|
||||||
|
class="size-4 filetree-icon filetree-icon--mono"
|
||||||
|
style={kindTextColor(kind()!)}
|
||||||
|
mono
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={!node.ignored}>
|
||||||
|
<span class="filetree-iconpair size-4">
|
||||||
|
<FileIcon node={node} class="size-4 filetree-icon filetree-icon--color" />
|
||||||
|
<FileIcon node={node} class="size-4 filetree-icon filetree-icon--mono" mono />
|
||||||
|
</span>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
</FileTreeNode>
|
</FileTreeNode>
|
||||||
</FileTreeNodeTooltip>
|
</FileTreeNodeTooltip>
|
||||||
</Match>
|
</Match>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
import { useFilteredList } from "@opencode-ai/ui/hooks"
|
||||||
import { createEffect, on, Component, Show, For, onCleanup, Switch, Match, createMemo, createSignal } from "solid-js"
|
import { createEffect, on, Component, Show, onCleanup, Switch, Match, createMemo, createSignal } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { createFocusSignal } from "@solid-primitives/active-element"
|
import { createFocusSignal } from "@solid-primitives/active-element"
|
||||||
import { useLocal } from "@/context/local"
|
import { useLocal } from "@/context/local"
|
||||||
@@ -26,6 +26,7 @@ import type { IconName } from "@opencode-ai/ui/icons/provider"
|
|||||||
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { Select } from "@opencode-ai/ui/select"
|
import { Select } from "@opencode-ai/ui/select"
|
||||||
|
import { RadioGroup } from "@opencode-ai/ui/radio-group"
|
||||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||||
import { ModelSelectorPopover } from "@/components/dialog-select-model"
|
import { ModelSelectorPopover } from "@/components/dialog-select-model"
|
||||||
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
|
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
|
||||||
@@ -108,6 +109,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
let slashPopoverRef!: HTMLDivElement
|
let slashPopoverRef!: HTMLDivElement
|
||||||
|
|
||||||
const mirror = { input: false }
|
const mirror = { input: false }
|
||||||
|
const inset = 44
|
||||||
|
|
||||||
const scrollCursorIntoView = () => {
|
const scrollCursorIntoView = () => {
|
||||||
const container = scrollRef
|
const container = scrollRef
|
||||||
@@ -117,7 +119,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const range = selection.getRangeAt(0)
|
const range = selection.getRangeAt(0)
|
||||||
if (!editorRef.contains(range.startContainer)) return
|
if (!editorRef.contains(range.startContainer)) return
|
||||||
|
|
||||||
const rect = range.getBoundingClientRect()
|
const cursor = getCursorPosition(editorRef)
|
||||||
|
const length = promptLength(prompt.current().filter((part) => part.type !== "image"))
|
||||||
|
if (cursor >= length) {
|
||||||
|
container.scrollTop = container.scrollHeight
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = range.getClientRects().item(0) ?? range.getBoundingClientRect()
|
||||||
if (!rect.height) return
|
if (!rect.height) return
|
||||||
|
|
||||||
const containerRect = container.getBoundingClientRect()
|
const containerRect = container.getBoundingClientRect()
|
||||||
@@ -130,8 +139,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bottom > container.scrollTop + container.clientHeight - padding) {
|
if (bottom > container.scrollTop + container.clientHeight - inset) {
|
||||||
container.scrollTop = bottom - container.clientHeight + padding
|
container.scrollTop = bottom - container.clientHeight + inset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +250,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
return messages.some((m) => m.role === "user")
|
return messages.some((m) => m.role === "user")
|
||||||
})
|
})
|
||||||
|
|
||||||
const MAX_HISTORY = 100
|
|
||||||
const [history, setHistory] = persisted(
|
const [history, setHistory] = persisted(
|
||||||
Persist.global("prompt-history", ["prompt-history.v1"]),
|
Persist.global("prompt-history", ["prompt-history.v1"]),
|
||||||
createStore<{
|
createStore<{
|
||||||
@@ -311,6 +319,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
requestAnimationFrame(() => editorRef?.focus())
|
requestAnimationFrame(() => editorRef?.focus())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shellModeKey = "mod+shift+x"
|
||||||
|
const normalModeKey = "mod+shift+e"
|
||||||
|
|
||||||
command.register("prompt-input", () => [
|
command.register("prompt-input", () => [
|
||||||
{
|
{
|
||||||
id: "file.attach",
|
id: "file.attach",
|
||||||
@@ -320,6 +331,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
disabled: store.mode !== "normal",
|
disabled: store.mode !== "normal",
|
||||||
onSelect: pick,
|
onSelect: pick,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "prompt.mode.shell",
|
||||||
|
title: language.t("command.prompt.mode.shell"),
|
||||||
|
category: language.t("command.category.session"),
|
||||||
|
keybind: shellModeKey,
|
||||||
|
disabled: store.mode === "shell",
|
||||||
|
onSelect: () => setMode("shell"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "prompt.mode.normal",
|
||||||
|
title: language.t("command.prompt.mode.normal"),
|
||||||
|
category: language.t("command.category.session"),
|
||||||
|
keybind: normalModeKey,
|
||||||
|
disabled: store.mode === "normal",
|
||||||
|
onSelect: () => setMode("normal"),
|
||||||
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
const closePopover = () => setStore("popover", null)
|
const closePopover = () => setStore("popover", null)
|
||||||
@@ -1059,8 +1086,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
removeLabel={language.t("prompt.attachment.remove")}
|
removeLabel={language.t("prompt.attachment.remove")}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="relative max-h-[240px] overflow-y-auto"
|
class="relative"
|
||||||
ref={(el) => (scrollRef = el)}
|
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
const target = e.target
|
const target = e.target
|
||||||
if (!(target instanceof HTMLElement)) return
|
if (!(target instanceof HTMLElement)) return
|
||||||
@@ -1074,40 +1100,42 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
editorRef?.focus()
|
editorRef?.focus()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div class="relative max-h-[240px] overflow-y-auto no-scrollbar" ref={(el) => (scrollRef = el)}>
|
||||||
data-component="prompt-input"
|
|
||||||
ref={(el) => {
|
|
||||||
editorRef = el
|
|
||||||
props.ref?.(el)
|
|
||||||
}}
|
|
||||||
role="textbox"
|
|
||||||
aria-multiline="true"
|
|
||||||
aria-label={placeholder()}
|
|
||||||
contenteditable="true"
|
|
||||||
autocapitalize="off"
|
|
||||||
autocorrect="off"
|
|
||||||
spellcheck={false}
|
|
||||||
onInput={handleInput}
|
|
||||||
onPaste={handlePaste}
|
|
||||||
onCompositionStart={() => setComposing(true)}
|
|
||||||
onCompositionEnd={() => setComposing(false)}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
classList={{
|
|
||||||
"select-text": true,
|
|
||||||
"w-full pl-3 pr-2 pt-2 pb-12 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true,
|
|
||||||
"[&_[data-type=file]]:text-syntax-property": true,
|
|
||||||
"[&_[data-type=agent]]:text-syntax-type": true,
|
|
||||||
"font-mono!": store.mode === "shell",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Show when={!prompt.dirty()}>
|
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 pb-12 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
|
data-component="prompt-input"
|
||||||
classList={{ "font-mono!": store.mode === "shell" }}
|
ref={(el) => {
|
||||||
>
|
editorRef = el
|
||||||
{placeholder()}
|
props.ref?.(el)
|
||||||
</div>
|
}}
|
||||||
</Show>
|
role="textbox"
|
||||||
|
aria-multiline="true"
|
||||||
|
aria-label={placeholder()}
|
||||||
|
contenteditable="true"
|
||||||
|
autocapitalize="off"
|
||||||
|
autocorrect="off"
|
||||||
|
spellcheck={false}
|
||||||
|
onInput={handleInput}
|
||||||
|
onPaste={handlePaste}
|
||||||
|
onCompositionStart={() => setComposing(true)}
|
||||||
|
onCompositionEnd={() => setComposing(false)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
classList={{
|
||||||
|
"select-text": true,
|
||||||
|
"w-full pl-3 pr-2 pt-2 pb-11 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true,
|
||||||
|
"[&_[data-type=file]]:text-syntax-property": true,
|
||||||
|
"[&_[data-type=agent]]:text-syntax-type": true,
|
||||||
|
"font-mono!": store.mode === "shell",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Show when={!prompt.dirty()}>
|
||||||
|
<div
|
||||||
|
class="absolute top-0 inset-x-0 pl-3 pr-2 pt-2 pb-11 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate"
|
||||||
|
classList={{ "font-mono!": store.mode === "shell" }}
|
||||||
|
>
|
||||||
|
{placeholder()}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="pointer-events-none absolute bottom-2 right-2 flex items-center gap-2">
|
<div class="pointer-events-none absolute bottom-2 right-2 flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
@@ -1330,59 +1358,35 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
</TooltipKeybind>
|
</TooltipKeybind>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="shrink-0" data-component="prompt-mode-toggle">
|
||||||
<div class="shrink-0">
|
<RadioGroup
|
||||||
<div
|
options={["shell", "normal"] as const}
|
||||||
data-component="prompt-mode-toggle"
|
current={store.mode}
|
||||||
class="relative h-7 w-[68px] rounded-[4px] bg-surface-inset-base border border-[0.5px] border-border-weak-base p-0 flex items-center gap-1 overflow-visible"
|
value={(mode) => mode}
|
||||||
>
|
label={(mode) => (
|
||||||
<div
|
<TooltipKeybind
|
||||||
class="absolute inset-y-0 left-0 w-[calc((100%-4px)/2)] rounded-[4px] bg-surface-raised-stronger-non-alpha shadow-[var(--shadow-xs-border)] transition-transform duration-200 ease-out will-change-transform"
|
placement="top"
|
||||||
style={{
|
gutter={4}
|
||||||
transform: store.mode === "shell" ? "translateX(0px)" : "translateX(calc(100% + 4px))",
|
title={language.t(mode === "shell" ? "command.prompt.mode.shell" : "command.prompt.mode.normal")}
|
||||||
}}
|
keybind={command.keybind(mode === "shell" ? "prompt.mode.shell" : "prompt.mode.normal")}
|
||||||
/>
|
class="size-full flex items-center justify-center"
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="relative z-10 flex-1 h-full p-0.5 flex items-center justify-center"
|
|
||||||
aria-pressed={store.mode === "shell"}
|
|
||||||
onClick={() => setMode("shell")}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="w-full h-full flex items-center justify-center rounded-[2px] transition-colors hover:bg-surface-inset-base"
|
|
||||||
classList={{ "hover:bg-transparent": store.mode === "shell" }}
|
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
name="console"
|
name={mode === "shell" ? "console" : "prompt"}
|
||||||
class="size-[18px]"
|
class="size-[18px]"
|
||||||
classList={{
|
classList={{
|
||||||
"text-icon-strong-base": store.mode === "shell",
|
"text-icon-strong-base": mode === "shell" && store.mode === "shell",
|
||||||
"text-icon-weak": store.mode !== "shell",
|
"text-icon-interactive-base": mode === "normal" && store.mode === "normal",
|
||||||
|
"text-icon-weak": store.mode !== mode,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</TooltipKeybind>
|
||||||
</button>
|
)}
|
||||||
<button
|
onSelect={(mode) => mode && setMode(mode)}
|
||||||
type="button"
|
fill
|
||||||
class="relative z-10 flex-1 h-full p-0.5 flex items-center justify-center"
|
pad="none"
|
||||||
aria-pressed={store.mode === "normal"}
|
class="w-[68px]"
|
||||||
onClick={() => setMode("normal")}
|
/>
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="w-full h-full flex items-center justify-center rounded-[2px] transition-colors hover:bg-surface-inset-base"
|
|
||||||
classList={{ "hover:bg-transparent": store.mode === "normal" }}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name="prompt"
|
|
||||||
class="size-[18px]"
|
|
||||||
classList={{
|
|
||||||
"text-icon-interactive-base": store.mode === "normal",
|
|
||||||
"text-icon-weak": store.mode !== "normal",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { For, Show, createMemo, onCleanup, onMount, type Component } from "solid-js"
|
import { For, Show, createMemo, onCleanup, onMount, type Component } from "solid-js"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
|
import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
|
||||||
import { Icon } from "@opencode-ai/ui/icon"
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
|
import type { QuestionAnswer, QuestionRequest } from "@opencode-ai/sdk/v2"
|
||||||
@@ -232,9 +233,11 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-component="question-prompt" ref={(el) => (root = el)}>
|
<DockPrompt
|
||||||
<div data-slot="question-body">
|
kind="question"
|
||||||
<div data-slot="question-header">
|
ref={(el) => (root = el)}
|
||||||
|
header={
|
||||||
|
<>
|
||||||
<div data-slot="question-header-title">{summary()}</div>
|
<div data-slot="question-header-title">{summary()}</div>
|
||||||
<div data-slot="question-progress">
|
<div data-slot="question-progress">
|
||||||
<For each={questions()}>
|
<For each={questions()}>
|
||||||
@@ -254,172 +257,169 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
|
}
|
||||||
<div data-slot="question-content">
|
footer={
|
||||||
<div data-slot="question-text">{question()?.question}</div>
|
<>
|
||||||
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
|
<Button variant="ghost" size="large" disabled={store.sending} onClick={reject}>
|
||||||
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
|
{language.t("ui.common.dismiss")}
|
||||||
</Show>
|
</Button>
|
||||||
<div data-slot="question-options">
|
<div data-slot="question-footer-actions">
|
||||||
<For each={options()}>
|
<Show when={store.tab > 0}>
|
||||||
{(opt, i) => {
|
<Button variant="secondary" size="large" disabled={store.sending} onClick={back}>
|
||||||
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
|
{language.t("ui.common.back")}
|
||||||
return (
|
</Button>
|
||||||
<button
|
</Show>
|
||||||
data-slot="question-option"
|
<Button variant={last() ? "primary" : "secondary"} size="large" disabled={store.sending} onClick={next}>
|
||||||
data-picked={picked()}
|
{last() ? language.t("ui.common.submit") : language.t("ui.common.next")}
|
||||||
role={multi() ? "checkbox" : "radio"}
|
</Button>
|
||||||
aria-checked={picked()}
|
</div>
|
||||||
disabled={store.sending}
|
</>
|
||||||
onClick={() => selectOption(i())}
|
}
|
||||||
>
|
>
|
||||||
<span data-slot="question-option-check" aria-hidden="true">
|
<div data-slot="question-text">{question()?.question}</div>
|
||||||
<span
|
<Show when={multi()} fallback={<div data-slot="question-hint">{language.t("ui.question.singleHint")}</div>}>
|
||||||
data-slot="question-option-box"
|
<div data-slot="question-hint">{language.t("ui.question.multiHint")}</div>
|
||||||
data-type={multi() ? "checkbox" : "radio"}
|
</Show>
|
||||||
data-picked={picked()}
|
<div data-slot="question-options">
|
||||||
>
|
<For each={options()}>
|
||||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
{(opt, i) => {
|
||||||
<Icon name="check-small" size="small" />
|
const picked = () => store.answers[store.tab]?.includes(opt.label) ?? false
|
||||||
</Show>
|
return (
|
||||||
</span>
|
<button
|
||||||
</span>
|
|
||||||
<span data-slot="question-option-main">
|
|
||||||
<span data-slot="option-label">{opt.label}</span>
|
|
||||||
<Show when={opt.description}>
|
|
||||||
<span data-slot="option-description">{opt.description}</span>
|
|
||||||
</Show>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
|
|
||||||
<Show
|
|
||||||
when={store.editing}
|
|
||||||
fallback={
|
|
||||||
<button
|
|
||||||
data-slot="question-option"
|
|
||||||
data-custom="true"
|
|
||||||
data-picked={on()}
|
|
||||||
role={multi() ? "checkbox" : "radio"}
|
|
||||||
aria-checked={on()}
|
|
||||||
disabled={store.sending}
|
|
||||||
onClick={customOpen}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
data-slot="question-option-check"
|
|
||||||
aria-hidden="true"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
customToggle()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
|
|
||||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
|
||||||
<Icon name="check-small" size="small" />
|
|
||||||
</Show>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span data-slot="question-option-main">
|
|
||||||
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
|
||||||
<span data-slot="option-description">
|
|
||||||
{input() || language.t("ui.question.custom.placeholder")}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<form
|
|
||||||
data-slot="question-option"
|
data-slot="question-option"
|
||||||
data-custom="true"
|
data-picked={picked()}
|
||||||
data-picked={on()}
|
|
||||||
role={multi() ? "checkbox" : "radio"}
|
role={multi() ? "checkbox" : "radio"}
|
||||||
aria-checked={on()}
|
aria-checked={picked()}
|
||||||
onMouseDown={(e) => {
|
disabled={store.sending}
|
||||||
if (store.sending) {
|
onClick={() => selectOption(i())}
|
||||||
e.preventDefault()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (e.target instanceof HTMLTextAreaElement) return
|
|
||||||
const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
|
|
||||||
if (input instanceof HTMLTextAreaElement) input.focus()
|
|
||||||
}}
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
commitCustom()
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<span
|
<span data-slot="question-option-check" aria-hidden="true">
|
||||||
data-slot="question-option-check"
|
<span
|
||||||
aria-hidden="true"
|
data-slot="question-option-box"
|
||||||
onClick={(e) => {
|
data-type={multi() ? "checkbox" : "radio"}
|
||||||
e.preventDefault()
|
data-picked={picked()}
|
||||||
e.stopPropagation()
|
>
|
||||||
customToggle()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
|
|
||||||
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
||||||
<Icon name="check-small" size="small" />
|
<Icon name="check-small" size="small" />
|
||||||
</Show>
|
</Show>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span data-slot="question-option-main">
|
<span data-slot="question-option-main">
|
||||||
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
<span data-slot="option-label">{opt.label}</span>
|
||||||
<textarea
|
<Show when={opt.description}>
|
||||||
ref={(el) =>
|
<span data-slot="option-description">{opt.description}</span>
|
||||||
setTimeout(() => {
|
</Show>
|
||||||
el.focus()
|
|
||||||
el.style.height = "0px"
|
|
||||||
el.style.height = `${el.scrollHeight}px`
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
data-slot="question-custom-input"
|
|
||||||
placeholder={language.t("ui.question.custom.placeholder")}
|
|
||||||
value={input()}
|
|
||||||
rows={1}
|
|
||||||
disabled={store.sending}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
e.preventDefault()
|
|
||||||
setStore("editing", false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (e.key !== "Enter" || e.shiftKey) return
|
|
||||||
e.preventDefault()
|
|
||||||
commitCustom()
|
|
||||||
}}
|
|
||||||
onInput={(e) => {
|
|
||||||
customUpdate(e.currentTarget.value)
|
|
||||||
e.currentTarget.style.height = "0px"
|
|
||||||
e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
</form>
|
</button>
|
||||||
</Show>
|
)
|
||||||
</div>
|
}}
|
||||||
</div>
|
</For>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div data-slot="question-footer">
|
<Show
|
||||||
<Button variant="ghost" size="large" disabled={store.sending} onClick={reject}>
|
when={store.editing}
|
||||||
{language.t("ui.common.dismiss")}
|
fallback={
|
||||||
</Button>
|
<button
|
||||||
<div data-slot="question-footer-actions">
|
data-slot="question-option"
|
||||||
<Show when={store.tab > 0}>
|
data-custom="true"
|
||||||
<Button variant="secondary" size="large" disabled={store.sending} onClick={back}>
|
data-picked={on()}
|
||||||
{language.t("ui.common.back")}
|
role={multi() ? "checkbox" : "radio"}
|
||||||
</Button>
|
aria-checked={on()}
|
||||||
</Show>
|
disabled={store.sending}
|
||||||
<Button variant={last() ? "primary" : "secondary"} size="large" disabled={store.sending} onClick={next}>
|
onClick={customOpen}
|
||||||
{last() ? language.t("ui.common.submit") : language.t("ui.common.next")}
|
>
|
||||||
</Button>
|
<span
|
||||||
</div>
|
data-slot="question-option-check"
|
||||||
|
aria-hidden="true"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
customToggle()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
|
||||||
|
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
||||||
|
<Icon name="check-small" size="small" />
|
||||||
|
</Show>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span data-slot="question-option-main">
|
||||||
|
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
||||||
|
<span data-slot="option-description">{input() || language.t("ui.question.custom.placeholder")}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
data-slot="question-option"
|
||||||
|
data-custom="true"
|
||||||
|
data-picked={on()}
|
||||||
|
role={multi() ? "checkbox" : "radio"}
|
||||||
|
aria-checked={on()}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
if (store.sending) {
|
||||||
|
e.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.target instanceof HTMLTextAreaElement) return
|
||||||
|
const input = e.currentTarget.querySelector('[data-slot="question-custom-input"]')
|
||||||
|
if (input instanceof HTMLTextAreaElement) input.focus()
|
||||||
|
}}
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
commitCustom()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
data-slot="question-option-check"
|
||||||
|
aria-hidden="true"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
customToggle()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span data-slot="question-option-box" data-type={multi() ? "checkbox" : "radio"} data-picked={on()}>
|
||||||
|
<Show when={multi()} fallback={<span data-slot="question-option-radio-dot" />}>
|
||||||
|
<Icon name="check-small" size="small" />
|
||||||
|
</Show>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span data-slot="question-option-main">
|
||||||
|
<span data-slot="option-label">{language.t("ui.messagePart.option.typeOwnAnswer")}</span>
|
||||||
|
<textarea
|
||||||
|
ref={(el) =>
|
||||||
|
setTimeout(() => {
|
||||||
|
el.focus()
|
||||||
|
el.style.height = "0px"
|
||||||
|
el.style.height = `${el.scrollHeight}px`
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
data-slot="question-custom-input"
|
||||||
|
placeholder={language.t("ui.question.custom.placeholder")}
|
||||||
|
value={input()}
|
||||||
|
rows={1}
|
||||||
|
disabled={store.sending}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
e.preventDefault()
|
||||||
|
setStore("editing", false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.key !== "Enter" || e.shiftKey) return
|
||||||
|
e.preventDefault()
|
||||||
|
commitCustom()
|
||||||
|
}}
|
||||||
|
onInput={(e) => {
|
||||||
|
customUpdate(e.currentTarget.value)
|
||||||
|
e.currentTarget.style.height = "0px"
|
||||||
|
e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</form>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</DockPrompt>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ export function SessionHeader() {
|
|||||||
<div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
|
<div class="flex h-[24px] box-border items-center rounded-md border border-border-weak-base bg-surface-panel overflow-hidden">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="rounded-none h-full py-0 pr-2 pl-0.5 gap-1.5 border-none shadow-none"
|
class="rounded-none h-full py-0 pr-3 pl-0.5 gap-1.5 border-none shadow-none"
|
||||||
onClick={copyPath}
|
onClick={copyPath}
|
||||||
aria-label={language.t("session.header.open.copyPath")}
|
aria-label={language.t("session.header.open.copyPath")}
|
||||||
>
|
>
|
||||||
@@ -468,7 +468,7 @@ export function SessionHeader() {
|
|||||||
classList: { "rounded-r-none": share.shareUrl() !== undefined },
|
classList: { "rounded-r-none": share.shareUrl() !== undefined },
|
||||||
style: { scale: 1 },
|
style: { scale: 1 },
|
||||||
}}
|
}}
|
||||||
trigger={language.t("session.share.action.share")}
|
trigger={<span class="text-12-regular">{language.t("session.share.action.share")}</span>}
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<Show
|
<Show
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ export function StatusPopover() {
|
|||||||
triggerProps={{
|
triggerProps={{
|
||||||
variant: "ghost",
|
variant: "ghost",
|
||||||
class:
|
class:
|
||||||
"rounded-md h-[24px] pr-2 pl-0.5 gap-2 border border-border-weak-base bg-surface-panel shadow-none data-[expanded]:bg-surface-raised-base-hover",
|
"rounded-md h-[24px] pr-3 pl-0.5 gap-2 border border-border-weak-base bg-surface-panel shadow-none data-[expanded]:bg-surface-raised-base-hover",
|
||||||
style: { scale: 1 },
|
style: { scale: 1 },
|
||||||
}}
|
}}
|
||||||
trigger={
|
trigger={
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
}
|
}
|
||||||
ghostty = g
|
ghostty = g
|
||||||
term = t
|
term = t
|
||||||
output = terminalWriter((data) => t.write(data))
|
output = terminalWriter((data, done) => t.write(data, done))
|
||||||
|
|
||||||
t.attachCustomKeyEventHandler((event) => {
|
t.attachCustomKeyEventHandler((event) => {
|
||||||
const key = event.key.toLowerCase()
|
const key = event.key.toLowerCase()
|
||||||
@@ -520,9 +520,19 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
disposed = true
|
disposed = true
|
||||||
if (fitFrame !== undefined) cancelAnimationFrame(fitFrame)
|
if (fitFrame !== undefined) cancelAnimationFrame(fitFrame)
|
||||||
if (sizeTimer !== undefined) clearTimeout(sizeTimer)
|
if (sizeTimer !== undefined) clearTimeout(sizeTimer)
|
||||||
output?.flush()
|
if (ws && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) ws.close()
|
||||||
persistTerminal({ term, addon: serializeAddon, cursor, pty: local.pty, onCleanup: props.onCleanup })
|
|
||||||
cleanup()
|
const finalize = () => {
|
||||||
|
persistTerminal({ term, addon: serializeAddon, cursor, pty: local.pty, onCleanup: props.onCleanup })
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!output) {
|
||||||
|
finalize()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output.flush(finalize)
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "التبديل إلى الوكيل السابق",
|
"command.agent.cycle.reverse.description": "التبديل إلى الوكيل السابق",
|
||||||
"command.model.variant.cycle": "تغيير جهد التفكير",
|
"command.model.variant.cycle": "تغيير جهد التفكير",
|
||||||
"command.model.variant.cycle.description": "التبديل إلى مستوى الجهد التالي",
|
"command.model.variant.cycle.description": "التبديل إلى مستوى الجهد التالي",
|
||||||
|
"command.prompt.mode.shell": "التبديل إلى وضع Shell",
|
||||||
|
"command.prompt.mode.normal": "التبديل إلى وضع Prompt",
|
||||||
"command.permissions.autoaccept.enable": "قبول التعديلات تلقائيًا",
|
"command.permissions.autoaccept.enable": "قبول التعديلات تلقائيًا",
|
||||||
"command.permissions.autoaccept.disable": "إيقاف قبول التعديلات تلقائيًا",
|
"command.permissions.autoaccept.disable": "إيقاف قبول التعديلات تلقائيًا",
|
||||||
"command.workspace.toggle": "تبديل مساحات العمل",
|
"command.workspace.toggle": "تبديل مساحات العمل",
|
||||||
@@ -210,6 +212,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "لخّص التعليقات…",
|
"prompt.placeholder.summarizeComments": "لخّص التعليقات…",
|
||||||
"prompt.placeholder.summarizeComment": "لخّص التعليق…",
|
"prompt.placeholder.summarizeComment": "لخّص التعليق…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc للخروج",
|
"prompt.mode.shell.exit": "esc للخروج",
|
||||||
"prompt.example.1": "إصلاح TODO في قاعدة التعليمات البرمجية",
|
"prompt.example.1": "إصلاح TODO في قاعدة التعليمات البرمجية",
|
||||||
"prompt.example.2": "ما هو المكدس التقني لهذا المشروع؟",
|
"prompt.example.2": "ما هو المكدس التقني لهذا المشروع؟",
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "Mudar para o agente anterior",
|
"command.agent.cycle.reverse.description": "Mudar para o agente anterior",
|
||||||
"command.model.variant.cycle": "Alternar nível de raciocínio",
|
"command.model.variant.cycle": "Alternar nível de raciocínio",
|
||||||
"command.model.variant.cycle.description": "Mudar para o próximo nível de esforço",
|
"command.model.variant.cycle.description": "Mudar para o próximo nível de esforço",
|
||||||
|
"command.prompt.mode.shell": "Alternar para o modo Shell",
|
||||||
|
"command.prompt.mode.normal": "Alternar para o modo Prompt",
|
||||||
"command.permissions.autoaccept.enable": "Aceitar edições automaticamente",
|
"command.permissions.autoaccept.enable": "Aceitar edições automaticamente",
|
||||||
"command.permissions.autoaccept.disable": "Parar de aceitar edições automaticamente",
|
"command.permissions.autoaccept.disable": "Parar de aceitar edições automaticamente",
|
||||||
"command.workspace.toggle": "Alternar espaços de trabalho",
|
"command.workspace.toggle": "Alternar espaços de trabalho",
|
||||||
@@ -210,6 +212,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "Resumir comentários…",
|
"prompt.placeholder.summarizeComments": "Resumir comentários…",
|
||||||
"prompt.placeholder.summarizeComment": "Resumir comentário…",
|
"prompt.placeholder.summarizeComment": "Resumir comentário…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc para sair",
|
"prompt.mode.shell.exit": "esc para sair",
|
||||||
"prompt.example.1": "Corrigir um TODO no código",
|
"prompt.example.1": "Corrigir um TODO no código",
|
||||||
"prompt.example.2": "Qual é a stack tecnológica deste projeto?",
|
"prompt.example.2": "Qual é a stack tecnológica deste projeto?",
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "Prebaci na prethodnog agenta",
|
"command.agent.cycle.reverse.description": "Prebaci na prethodnog agenta",
|
||||||
"command.model.variant.cycle": "Promijeni nivo razmišljanja",
|
"command.model.variant.cycle": "Promijeni nivo razmišljanja",
|
||||||
"command.model.variant.cycle.description": "Prebaci na sljedeći nivo",
|
"command.model.variant.cycle.description": "Prebaci na sljedeći nivo",
|
||||||
|
"command.prompt.mode.shell": "Prebaci na Shell način",
|
||||||
|
"command.prompt.mode.normal": "Prebaci na Prompt način",
|
||||||
"command.permissions.autoaccept.enable": "Automatski prihvataj izmjene",
|
"command.permissions.autoaccept.enable": "Automatski prihvataj izmjene",
|
||||||
"command.permissions.autoaccept.disable": "Zaustavi automatsko prihvatanje izmjena",
|
"command.permissions.autoaccept.disable": "Zaustavi automatsko prihvatanje izmjena",
|
||||||
"command.workspace.toggle": "Prikaži/sakrij radne prostore",
|
"command.workspace.toggle": "Prikaži/sakrij radne prostore",
|
||||||
@@ -228,6 +230,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "Sažmi komentare…",
|
"prompt.placeholder.summarizeComments": "Sažmi komentare…",
|
||||||
"prompt.placeholder.summarizeComment": "Sažmi komentar…",
|
"prompt.placeholder.summarizeComment": "Sažmi komentar…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc za izlaz",
|
"prompt.mode.shell.exit": "esc za izlaz",
|
||||||
|
|
||||||
"prompt.example.1": "Popravi TODO u bazi koda",
|
"prompt.example.1": "Popravi TODO u bazi koda",
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "Skift til forrige agent",
|
"command.agent.cycle.reverse.description": "Skift til forrige agent",
|
||||||
"command.model.variant.cycle": "Skift tænkeindsats",
|
"command.model.variant.cycle": "Skift tænkeindsats",
|
||||||
"command.model.variant.cycle.description": "Skift til næste indsatsniveau",
|
"command.model.variant.cycle.description": "Skift til næste indsatsniveau",
|
||||||
|
"command.prompt.mode.shell": "Skift til shell-tilstand",
|
||||||
|
"command.prompt.mode.normal": "Skift til prompt-tilstand",
|
||||||
"command.permissions.autoaccept.enable": "Accepter ændringer automatisk",
|
"command.permissions.autoaccept.enable": "Accepter ændringer automatisk",
|
||||||
"command.permissions.autoaccept.disable": "Stop automatisk accept af ændringer",
|
"command.permissions.autoaccept.disable": "Stop automatisk accept af ændringer",
|
||||||
"command.workspace.toggle": "Skift arbejdsområder",
|
"command.workspace.toggle": "Skift arbejdsområder",
|
||||||
@@ -226,6 +228,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "Opsummér kommentarer…",
|
"prompt.placeholder.summarizeComments": "Opsummér kommentarer…",
|
||||||
"prompt.placeholder.summarizeComment": "Opsummér kommentar…",
|
"prompt.placeholder.summarizeComment": "Opsummér kommentar…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc for at afslutte",
|
"prompt.mode.shell.exit": "esc for at afslutte",
|
||||||
|
|
||||||
"prompt.example.1": "Ret en TODO i koden",
|
"prompt.example.1": "Ret en TODO i koden",
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "Zum vorherigen Agenten wechseln",
|
"command.agent.cycle.reverse.description": "Zum vorherigen Agenten wechseln",
|
||||||
"command.model.variant.cycle": "Denkaufwand wechseln",
|
"command.model.variant.cycle": "Denkaufwand wechseln",
|
||||||
"command.model.variant.cycle.description": "Zum nächsten Aufwandslevel wechseln",
|
"command.model.variant.cycle.description": "Zum nächsten Aufwandslevel wechseln",
|
||||||
|
"command.prompt.mode.shell": "In den Shell-Modus wechseln",
|
||||||
|
"command.prompt.mode.normal": "In den Prompt-Modus wechseln",
|
||||||
"command.permissions.autoaccept.enable": "Änderungen automatisch akzeptieren",
|
"command.permissions.autoaccept.enable": "Änderungen automatisch akzeptieren",
|
||||||
"command.permissions.autoaccept.disable": "Automatische Annahme von Änderungen stoppen",
|
"command.permissions.autoaccept.disable": "Automatische Annahme von Änderungen stoppen",
|
||||||
"command.workspace.toggle": "Arbeitsbereiche umschalten",
|
"command.workspace.toggle": "Arbeitsbereiche umschalten",
|
||||||
@@ -215,6 +217,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "Kommentare zusammenfassen…",
|
"prompt.placeholder.summarizeComments": "Kommentare zusammenfassen…",
|
||||||
"prompt.placeholder.summarizeComment": "Kommentar zusammenfassen…",
|
"prompt.placeholder.summarizeComment": "Kommentar zusammenfassen…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc zum Verlassen",
|
"prompt.mode.shell.exit": "esc zum Verlassen",
|
||||||
"prompt.example.1": "Ein TODO in der Codebasis beheben",
|
"prompt.example.1": "Ein TODO in der Codebasis beheben",
|
||||||
"prompt.example.2": "Was ist der Tech-Stack dieses Projekts?",
|
"prompt.example.2": "Was ist der Tech-Stack dieses Projekts?",
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "Switch to the previous agent",
|
"command.agent.cycle.reverse.description": "Switch to the previous agent",
|
||||||
"command.model.variant.cycle": "Cycle thinking effort",
|
"command.model.variant.cycle": "Cycle thinking effort",
|
||||||
"command.model.variant.cycle.description": "Switch to the next effort level",
|
"command.model.variant.cycle.description": "Switch to the next effort level",
|
||||||
|
"command.prompt.mode.shell": "Switch to shell mode",
|
||||||
|
"command.prompt.mode.normal": "Switch to prompt mode",
|
||||||
"command.permissions.autoaccept.enable": "Auto-accept edits",
|
"command.permissions.autoaccept.enable": "Auto-accept edits",
|
||||||
"command.permissions.autoaccept.disable": "Stop auto-accepting edits",
|
"command.permissions.autoaccept.disable": "Stop auto-accepting edits",
|
||||||
"command.workspace.toggle": "Toggle workspaces",
|
"command.workspace.toggle": "Toggle workspaces",
|
||||||
@@ -228,6 +230,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "Summarize comments…",
|
"prompt.placeholder.summarizeComments": "Summarize comments…",
|
||||||
"prompt.placeholder.summarizeComment": "Summarize comment…",
|
"prompt.placeholder.summarizeComment": "Summarize comment…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc to exit",
|
"prompt.mode.shell.exit": "esc to exit",
|
||||||
|
|
||||||
"prompt.example.1": "Fix a TODO in the codebase",
|
"prompt.example.1": "Fix a TODO in the codebase",
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "Cambiar al agente anterior",
|
"command.agent.cycle.reverse.description": "Cambiar al agente anterior",
|
||||||
"command.model.variant.cycle": "Alternar esfuerzo de pensamiento",
|
"command.model.variant.cycle": "Alternar esfuerzo de pensamiento",
|
||||||
"command.model.variant.cycle.description": "Cambiar al siguiente nivel de esfuerzo",
|
"command.model.variant.cycle.description": "Cambiar al siguiente nivel de esfuerzo",
|
||||||
|
"command.prompt.mode.shell": "Cambiar al modo Shell",
|
||||||
|
"command.prompt.mode.normal": "Cambiar al modo Prompt",
|
||||||
"command.permissions.autoaccept.enable": "Aceptar ediciones automáticamente",
|
"command.permissions.autoaccept.enable": "Aceptar ediciones automáticamente",
|
||||||
"command.permissions.autoaccept.disable": "Dejar de aceptar ediciones automáticamente",
|
"command.permissions.autoaccept.disable": "Dejar de aceptar ediciones automáticamente",
|
||||||
"command.workspace.toggle": "Alternar espacios de trabajo",
|
"command.workspace.toggle": "Alternar espacios de trabajo",
|
||||||
@@ -227,6 +229,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "Resumir comentarios…",
|
"prompt.placeholder.summarizeComments": "Resumir comentarios…",
|
||||||
"prompt.placeholder.summarizeComment": "Resumir comentario…",
|
"prompt.placeholder.summarizeComment": "Resumir comentario…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc para salir",
|
"prompt.mode.shell.exit": "esc para salir",
|
||||||
|
|
||||||
"prompt.example.1": "Arreglar un TODO en el código",
|
"prompt.example.1": "Arreglar un TODO en el código",
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "Passer à l'agent précédent",
|
"command.agent.cycle.reverse.description": "Passer à l'agent précédent",
|
||||||
"command.model.variant.cycle": "Changer l'effort de réflexion",
|
"command.model.variant.cycle": "Changer l'effort de réflexion",
|
||||||
"command.model.variant.cycle.description": "Passer au niveau d'effort suivant",
|
"command.model.variant.cycle.description": "Passer au niveau d'effort suivant",
|
||||||
|
"command.prompt.mode.shell": "Passer en mode Shell",
|
||||||
|
"command.prompt.mode.normal": "Passer en mode Prompt",
|
||||||
"command.permissions.autoaccept.enable": "Accepter automatiquement les modifications",
|
"command.permissions.autoaccept.enable": "Accepter automatiquement les modifications",
|
||||||
"command.permissions.autoaccept.disable": "Arrêter l'acceptation automatique des modifications",
|
"command.permissions.autoaccept.disable": "Arrêter l'acceptation automatique des modifications",
|
||||||
"command.workspace.toggle": "Basculer les espaces de travail",
|
"command.workspace.toggle": "Basculer les espaces de travail",
|
||||||
@@ -210,6 +212,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "Résumer les commentaires…",
|
"prompt.placeholder.summarizeComments": "Résumer les commentaires…",
|
||||||
"prompt.placeholder.summarizeComment": "Résumer le commentaire…",
|
"prompt.placeholder.summarizeComment": "Résumer le commentaire…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc pour quitter",
|
"prompt.mode.shell.exit": "esc pour quitter",
|
||||||
"prompt.example.1": "Corriger un TODO dans la base de code",
|
"prompt.example.1": "Corriger un TODO dans la base de code",
|
||||||
"prompt.example.2": "Quelle est la pile technique de ce projet ?",
|
"prompt.example.2": "Quelle est la pile technique de ce projet ?",
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "前のエージェントに切り替え",
|
"command.agent.cycle.reverse.description": "前のエージェントに切り替え",
|
||||||
"command.model.variant.cycle": "思考レベルの切り替え",
|
"command.model.variant.cycle": "思考レベルの切り替え",
|
||||||
"command.model.variant.cycle.description": "次の思考レベルに切り替え",
|
"command.model.variant.cycle.description": "次の思考レベルに切り替え",
|
||||||
|
"command.prompt.mode.shell": "シェルモードに切り替える",
|
||||||
|
"command.prompt.mode.normal": "プロンプトモードに切り替える",
|
||||||
"command.permissions.autoaccept.enable": "編集を自動承認",
|
"command.permissions.autoaccept.enable": "編集を自動承認",
|
||||||
"command.permissions.autoaccept.disable": "編集の自動承認を停止",
|
"command.permissions.autoaccept.disable": "編集の自動承認を停止",
|
||||||
"command.workspace.toggle": "ワークスペースを切り替え",
|
"command.workspace.toggle": "ワークスペースを切り替え",
|
||||||
@@ -209,6 +211,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "コメントを要約…",
|
"prompt.placeholder.summarizeComments": "コメントを要約…",
|
||||||
"prompt.placeholder.summarizeComment": "コメントを要約…",
|
"prompt.placeholder.summarizeComment": "コメントを要約…",
|
||||||
"prompt.mode.shell": "シェル",
|
"prompt.mode.shell": "シェル",
|
||||||
|
"prompt.mode.normal": "プロンプト",
|
||||||
"prompt.mode.shell.exit": "escで終了",
|
"prompt.mode.shell.exit": "escで終了",
|
||||||
"prompt.example.1": "コードベースのTODOを修正",
|
"prompt.example.1": "コードベースのTODOを修正",
|
||||||
"prompt.example.2": "このプロジェクトの技術スタックは何ですか?",
|
"prompt.example.2": "このプロジェクトの技術スタックは何ですか?",
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "이전 에이전트로 전환",
|
"command.agent.cycle.reverse.description": "이전 에이전트로 전환",
|
||||||
"command.model.variant.cycle": "생각 수준 순환",
|
"command.model.variant.cycle": "생각 수준 순환",
|
||||||
"command.model.variant.cycle.description": "다음 생각 수준으로 전환",
|
"command.model.variant.cycle.description": "다음 생각 수준으로 전환",
|
||||||
|
"command.prompt.mode.shell": "셸 모드로 전환",
|
||||||
|
"command.prompt.mode.normal": "프롬프트 모드로 전환",
|
||||||
"command.permissions.autoaccept.enable": "편집 자동 수락",
|
"command.permissions.autoaccept.enable": "편집 자동 수락",
|
||||||
"command.permissions.autoaccept.disable": "편집 자동 수락 중지",
|
"command.permissions.autoaccept.disable": "편집 자동 수락 중지",
|
||||||
"command.workspace.toggle": "작업 공간 전환",
|
"command.workspace.toggle": "작업 공간 전환",
|
||||||
@@ -213,6 +215,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "댓글 요약…",
|
"prompt.placeholder.summarizeComments": "댓글 요약…",
|
||||||
"prompt.placeholder.summarizeComment": "댓글 요약…",
|
"prompt.placeholder.summarizeComment": "댓글 요약…",
|
||||||
"prompt.mode.shell": "셸",
|
"prompt.mode.shell": "셸",
|
||||||
|
"prompt.mode.normal": "프롬프트",
|
||||||
"prompt.mode.shell.exit": "종료하려면 esc",
|
"prompt.mode.shell.exit": "종료하려면 esc",
|
||||||
"prompt.example.1": "코드베이스의 TODO 수정",
|
"prompt.example.1": "코드베이스의 TODO 수정",
|
||||||
"prompt.example.2": "이 프로젝트의 기술 스택이 무엇인가요?",
|
"prompt.example.2": "이 프로젝트의 기술 스택이 무엇인가요?",
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "Bytt til forrige agent",
|
"command.agent.cycle.reverse.description": "Bytt til forrige agent",
|
||||||
"command.model.variant.cycle": "Bytt tenkeinnsats",
|
"command.model.variant.cycle": "Bytt tenkeinnsats",
|
||||||
"command.model.variant.cycle.description": "Bytt til neste innsatsnivå",
|
"command.model.variant.cycle.description": "Bytt til neste innsatsnivå",
|
||||||
|
"command.prompt.mode.shell": "Bytt til Shell-modus",
|
||||||
|
"command.prompt.mode.normal": "Bytt til Prompt-modus",
|
||||||
"command.permissions.autoaccept.enable": "Godta endringer automatisk",
|
"command.permissions.autoaccept.enable": "Godta endringer automatisk",
|
||||||
"command.permissions.autoaccept.disable": "Slutt å godta endringer automatisk",
|
"command.permissions.autoaccept.disable": "Slutt å godta endringer automatisk",
|
||||||
"command.workspace.toggle": "Veksle arbeidsområder",
|
"command.workspace.toggle": "Veksle arbeidsområder",
|
||||||
@@ -230,6 +232,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "Oppsummer kommentarer…",
|
"prompt.placeholder.summarizeComments": "Oppsummer kommentarer…",
|
||||||
"prompt.placeholder.summarizeComment": "Oppsummer kommentar…",
|
"prompt.placeholder.summarizeComment": "Oppsummer kommentar…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "ESC for å avslutte",
|
"prompt.mode.shell.exit": "ESC for å avslutte",
|
||||||
|
|
||||||
"prompt.example.1": "Fiks en TODO i kodebasen",
|
"prompt.example.1": "Fiks en TODO i kodebasen",
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "Przełącz na poprzedniego agenta",
|
"command.agent.cycle.reverse.description": "Przełącz na poprzedniego agenta",
|
||||||
"command.model.variant.cycle": "Przełącz wysiłek myślowy",
|
"command.model.variant.cycle": "Przełącz wysiłek myślowy",
|
||||||
"command.model.variant.cycle.description": "Przełącz na następny poziom wysiłku",
|
"command.model.variant.cycle.description": "Przełącz na następny poziom wysiłku",
|
||||||
|
"command.prompt.mode.shell": "Przełącz na tryb terminala",
|
||||||
|
"command.prompt.mode.normal": "Przełącz na tryb Prompt",
|
||||||
"command.permissions.autoaccept.enable": "Automatyczne akceptowanie edycji",
|
"command.permissions.autoaccept.enable": "Automatyczne akceptowanie edycji",
|
||||||
"command.permissions.autoaccept.disable": "Zatrzymaj automatyczne akceptowanie edycji",
|
"command.permissions.autoaccept.disable": "Zatrzymaj automatyczne akceptowanie edycji",
|
||||||
"command.workspace.toggle": "Przełącz przestrzenie robocze",
|
"command.workspace.toggle": "Przełącz przestrzenie robocze",
|
||||||
@@ -211,6 +213,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "Podsumuj komentarze…",
|
"prompt.placeholder.summarizeComments": "Podsumuj komentarze…",
|
||||||
"prompt.placeholder.summarizeComment": "Podsumuj komentarz…",
|
"prompt.placeholder.summarizeComment": "Podsumuj komentarz…",
|
||||||
"prompt.mode.shell": "Terminal",
|
"prompt.mode.shell": "Terminal",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "esc aby wyjść",
|
"prompt.mode.shell.exit": "esc aby wyjść",
|
||||||
"prompt.example.1": "Napraw TODO w bazie kodu",
|
"prompt.example.1": "Napraw TODO w bazie kodu",
|
||||||
"prompt.example.2": "Jaki jest stos technologiczny tego projektu?",
|
"prompt.example.2": "Jaki jest stos technologiczny tego projektu?",
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "Переключиться к предыдущему агенту",
|
"command.agent.cycle.reverse.description": "Переключиться к предыдущему агенту",
|
||||||
"command.model.variant.cycle": "Цикл режимов мышления",
|
"command.model.variant.cycle": "Цикл режимов мышления",
|
||||||
"command.model.variant.cycle.description": "Переключиться к следующему уровню усилий",
|
"command.model.variant.cycle.description": "Переключиться к следующему уровню усилий",
|
||||||
|
"command.prompt.mode.shell": "Переключиться в режим оболочки",
|
||||||
|
"command.prompt.mode.normal": "Переключиться в режим промпта",
|
||||||
"command.permissions.autoaccept.enable": "Авто-принятие изменений",
|
"command.permissions.autoaccept.enable": "Авто-принятие изменений",
|
||||||
"command.permissions.autoaccept.disable": "Прекратить авто-принятие изменений",
|
"command.permissions.autoaccept.disable": "Прекратить авто-принятие изменений",
|
||||||
"command.workspace.toggle": "Переключить рабочие пространства",
|
"command.workspace.toggle": "Переключить рабочие пространства",
|
||||||
@@ -227,6 +229,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "Суммировать комментарии…",
|
"prompt.placeholder.summarizeComments": "Суммировать комментарии…",
|
||||||
"prompt.placeholder.summarizeComment": "Суммировать комментарий…",
|
"prompt.placeholder.summarizeComment": "Суммировать комментарий…",
|
||||||
"prompt.mode.shell": "Оболочка",
|
"prompt.mode.shell": "Оболочка",
|
||||||
|
"prompt.mode.normal": "Промпт",
|
||||||
"prompt.mode.shell.exit": "esc для выхода",
|
"prompt.mode.shell.exit": "esc для выхода",
|
||||||
|
|
||||||
"prompt.example.1": "Исправить TODO в коде",
|
"prompt.example.1": "Исправить TODO в коде",
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "สลับไปยังเอเจนต์ก่อนหน้า",
|
"command.agent.cycle.reverse.description": "สลับไปยังเอเจนต์ก่อนหน้า",
|
||||||
"command.model.variant.cycle": "เปลี่ยนความพยายามในการคิด",
|
"command.model.variant.cycle": "เปลี่ยนความพยายามในการคิด",
|
||||||
"command.model.variant.cycle.description": "สลับไปยังระดับความพยายามถัดไป",
|
"command.model.variant.cycle.description": "สลับไปยังระดับความพยายามถัดไป",
|
||||||
|
"command.prompt.mode.shell": "สลับไปยังโหมดเชลล์",
|
||||||
|
"command.prompt.mode.normal": "สลับไปยังโหมดพรอมต์",
|
||||||
"command.permissions.autoaccept.enable": "ยอมรับการแก้ไขโดยอัตโนมัติ",
|
"command.permissions.autoaccept.enable": "ยอมรับการแก้ไขโดยอัตโนมัติ",
|
||||||
"command.permissions.autoaccept.disable": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ",
|
"command.permissions.autoaccept.disable": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ",
|
||||||
"command.workspace.toggle": "สลับพื้นที่ทำงาน",
|
"command.workspace.toggle": "สลับพื้นที่ทำงาน",
|
||||||
@@ -227,6 +229,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "สรุปความคิดเห็น…",
|
"prompt.placeholder.summarizeComments": "สรุปความคิดเห็น…",
|
||||||
"prompt.placeholder.summarizeComment": "สรุปความคิดเห็น…",
|
"prompt.placeholder.summarizeComment": "สรุปความคิดเห็น…",
|
||||||
"prompt.mode.shell": "เชลล์",
|
"prompt.mode.shell": "เชลล์",
|
||||||
|
"prompt.mode.normal": "พรอมต์",
|
||||||
"prompt.mode.shell.exit": "กด esc เพื่อออก",
|
"prompt.mode.shell.exit": "กด esc เพื่อออก",
|
||||||
|
|
||||||
"prompt.example.1": "แก้ไข TODO ในโค้ดเบส",
|
"prompt.example.1": "แก้ไข TODO ในโค้ดเบส",
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ export const dict = {
|
|||||||
"command.model.variant.cycle": "切换思考强度",
|
"command.model.variant.cycle": "切换思考强度",
|
||||||
"command.model.variant.cycle.description": "切换到下一个强度等级",
|
"command.model.variant.cycle.description": "切换到下一个强度等级",
|
||||||
|
|
||||||
|
"command.prompt.mode.shell": "切换到 Shell 模式",
|
||||||
|
"command.prompt.mode.normal": "切换到 Prompt 模式",
|
||||||
|
|
||||||
"command.permissions.autoaccept.enable": "自动接受编辑",
|
"command.permissions.autoaccept.enable": "自动接受编辑",
|
||||||
"command.permissions.autoaccept.disable": "停止自动接受编辑",
|
"command.permissions.autoaccept.disable": "停止自动接受编辑",
|
||||||
|
|
||||||
@@ -248,6 +251,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "总结评论…",
|
"prompt.placeholder.summarizeComments": "总结评论…",
|
||||||
"prompt.placeholder.summarizeComment": "总结该评论…",
|
"prompt.placeholder.summarizeComment": "总结该评论…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "按 esc 退出",
|
"prompt.mode.shell.exit": "按 esc 退出",
|
||||||
"prompt.example.1": "修复代码库中的一个 TODO",
|
"prompt.example.1": "修复代码库中的一个 TODO",
|
||||||
"prompt.example.2": "这个项目的技术栈是什么?",
|
"prompt.example.2": "这个项目的技术栈是什么?",
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ export const dict = {
|
|||||||
"command.agent.cycle.reverse.description": "切換到上一個代理程式",
|
"command.agent.cycle.reverse.description": "切換到上一個代理程式",
|
||||||
"command.model.variant.cycle": "循環思考強度",
|
"command.model.variant.cycle": "循環思考強度",
|
||||||
"command.model.variant.cycle.description": "切換到下一個強度等級",
|
"command.model.variant.cycle.description": "切換到下一個強度等級",
|
||||||
|
"command.prompt.mode.shell": "切換到 Shell 模式",
|
||||||
|
"command.prompt.mode.normal": "切換到 Prompt 模式",
|
||||||
"command.permissions.autoaccept.enable": "自動接受編輯",
|
"command.permissions.autoaccept.enable": "自動接受編輯",
|
||||||
"command.permissions.autoaccept.disable": "停止自動接受編輯",
|
"command.permissions.autoaccept.disable": "停止自動接受編輯",
|
||||||
"command.workspace.toggle": "切換工作區",
|
"command.workspace.toggle": "切換工作區",
|
||||||
@@ -227,6 +229,7 @@ export const dict = {
|
|||||||
"prompt.placeholder.summarizeComments": "摘要評論…",
|
"prompt.placeholder.summarizeComments": "摘要評論…",
|
||||||
"prompt.placeholder.summarizeComment": "摘要這則評論…",
|
"prompt.placeholder.summarizeComment": "摘要這則評論…",
|
||||||
"prompt.mode.shell": "Shell",
|
"prompt.mode.shell": "Shell",
|
||||||
|
"prompt.mode.normal": "Prompt",
|
||||||
"prompt.mode.shell.exit": "按 esc 退出",
|
"prompt.mode.shell.exit": "按 esc 退出",
|
||||||
|
|
||||||
"prompt.example.1": "修復程式碼庫中的一個 TODO",
|
"prompt.example.1": "修復程式碼庫中的一個 TODO",
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ export function MessageTimeline(props: {
|
|||||||
"sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true,
|
"sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true,
|
||||||
"w-full": true,
|
"w-full": true,
|
||||||
"pb-4": true,
|
"pb-4": true,
|
||||||
"pl-2 pr-4 md:pl-4 md:pr-6": true,
|
"pl-2 pr-3 md:pl-4 md:pr-3": true,
|
||||||
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
"md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { For, Show, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js"
|
import { For, Show, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js"
|
||||||
import type { QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
|
import type { QuestionRequest, Todo } from "@opencode-ai/sdk/v2"
|
||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { BasicTool } from "@opencode-ai/ui/basic-tool"
|
import { DockPrompt } from "@opencode-ai/ui/dock-prompt"
|
||||||
|
import { Icon } from "@opencode-ai/ui/icon"
|
||||||
import { PromptInput } from "@/components/prompt-input"
|
import { PromptInput } from "@/components/prompt-input"
|
||||||
import { QuestionDock } from "@/components/question-dock"
|
import { QuestionDock } from "@/components/question-dock"
|
||||||
import { SessionTodoDock } from "@/components/session-todo-dock"
|
import { SessionTodoDock } from "@/components/session-todo-dock"
|
||||||
@@ -123,63 +124,79 @@ export function SessionPromptDock(props: {
|
|||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={props.permissionRequest()} keyed>
|
<Show when={props.permissionRequest()} keyed>
|
||||||
{(perm) => (
|
{(perm) => {
|
||||||
<div data-component="tool-part-wrapper" data-permission="true" class="mb-3">
|
const toolDescription = () => {
|
||||||
<BasicTool
|
const key = `settings.permissions.tool.${perm.permission}.description`
|
||||||
icon="checklist"
|
const value = props.t(key)
|
||||||
locked
|
if (value === key) return ""
|
||||||
defaultOpen
|
return value
|
||||||
trigger={{
|
}
|
||||||
title: props.t("notification.permission.title"),
|
|
||||||
subtitle:
|
return (
|
||||||
perm.permission === "doom_loop"
|
<div>
|
||||||
? props.t("settings.permissions.tool.doom_loop.title")
|
<DockPrompt
|
||||||
: perm.permission,
|
kind="permission"
|
||||||
}}
|
header={
|
||||||
>
|
<div data-slot="permission-row" data-variant="header">
|
||||||
<Show when={perm.patterns.length > 0}>
|
<span data-slot="permission-icon">
|
||||||
<div class="flex flex-col gap-1 py-2 px-3 max-h-40 overflow-y-auto no-scrollbar">
|
<Icon name="warning" size="normal" />
|
||||||
<For each={perm.patterns}>
|
</span>
|
||||||
{(pattern) => <code class="text-12-regular text-text-base break-all">{pattern}</code>}
|
<div data-slot="permission-header-title">{props.t("notification.permission.title")}</div>
|
||||||
</For>
|
</div>
|
||||||
</div>
|
}
|
||||||
</Show>
|
footer={
|
||||||
<Show when={perm.permission === "doom_loop"}>
|
<>
|
||||||
<div class="text-12-regular text-text-weak pb-2 px-3">
|
<div />
|
||||||
{props.t("settings.permissions.tool.doom_loop.description")}
|
<div data-slot="permission-footer-actions">
|
||||||
</div>
|
<Button
|
||||||
</Show>
|
variant="ghost"
|
||||||
</BasicTool>
|
size="normal"
|
||||||
<div data-component="permission-prompt">
|
onClick={() => props.onDecide("reject")}
|
||||||
<div data-slot="permission-actions">
|
disabled={props.responding}
|
||||||
<Button
|
>
|
||||||
variant="ghost"
|
{props.t("ui.permission.deny")}
|
||||||
size="small"
|
</Button>
|
||||||
onClick={() => props.onDecide("reject")}
|
<Button
|
||||||
disabled={props.responding}
|
variant="secondary"
|
||||||
>
|
size="normal"
|
||||||
{props.t("ui.permission.deny")}
|
onClick={() => props.onDecide("always")}
|
||||||
</Button>
|
disabled={props.responding}
|
||||||
<Button
|
>
|
||||||
variant="secondary"
|
{props.t("ui.permission.allowAlways")}
|
||||||
size="small"
|
</Button>
|
||||||
onClick={() => props.onDecide("always")}
|
<Button
|
||||||
disabled={props.responding}
|
variant="primary"
|
||||||
>
|
size="normal"
|
||||||
{props.t("ui.permission.allowAlways")}
|
onClick={() => props.onDecide("once")}
|
||||||
</Button>
|
disabled={props.responding}
|
||||||
<Button
|
>
|
||||||
variant="primary"
|
{props.t("ui.permission.allowOnce")}
|
||||||
size="small"
|
</Button>
|
||||||
onClick={() => props.onDecide("once")}
|
</div>
|
||||||
disabled={props.responding}
|
</>
|
||||||
>
|
}
|
||||||
{props.t("ui.permission.allowOnce")}
|
>
|
||||||
</Button>
|
<Show when={toolDescription()}>
|
||||||
</div>
|
<div data-slot="permission-row">
|
||||||
|
<span data-slot="permission-spacer" aria-hidden="true" />
|
||||||
|
<div data-slot="permission-hint">{toolDescription()}</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={perm.patterns.length > 0}>
|
||||||
|
<div data-slot="permission-row">
|
||||||
|
<span data-slot="permission-spacer" aria-hidden="true" />
|
||||||
|
<div data-slot="permission-patterns">
|
||||||
|
<For each={perm.patterns}>
|
||||||
|
{(pattern) => <code class="text-12-regular text-text-base break-all">{pattern}</code>}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</DockPrompt>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)}
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={!props.blocked}>
|
<Show when={!props.blocked}>
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ describe("terminalWriter", () => {
|
|||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
const scheduled: VoidFunction[] = []
|
const scheduled: VoidFunction[] = []
|
||||||
const writer = terminalWriter(
|
const writer = terminalWriter(
|
||||||
(data) => calls.push(data),
|
(data, done) => {
|
||||||
|
calls.push(data)
|
||||||
|
done?.()
|
||||||
|
},
|
||||||
(flush) => scheduled.push(flush),
|
(flush) => scheduled.push(flush),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,10 +27,38 @@ describe("terminalWriter", () => {
|
|||||||
test("flush is a no-op when empty", () => {
|
test("flush is a no-op when empty", () => {
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
const writer = terminalWriter(
|
const writer = terminalWriter(
|
||||||
(data) => calls.push(data),
|
(data, done) => {
|
||||||
|
calls.push(data)
|
||||||
|
done?.()
|
||||||
|
},
|
||||||
(flush) => flush(),
|
(flush) => flush(),
|
||||||
)
|
)
|
||||||
writer.flush()
|
writer.flush()
|
||||||
expect(calls).toEqual([])
|
expect(calls).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("flush waits for pending write completion", () => {
|
||||||
|
const calls: string[] = []
|
||||||
|
let done: VoidFunction | undefined
|
||||||
|
const writer = terminalWriter(
|
||||||
|
(data, finish) => {
|
||||||
|
calls.push(data)
|
||||||
|
done = finish
|
||||||
|
},
|
||||||
|
(flush) => flush(),
|
||||||
|
)
|
||||||
|
|
||||||
|
writer.push("a")
|
||||||
|
|
||||||
|
let settled = false
|
||||||
|
writer.flush(() => {
|
||||||
|
settled = true
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(calls).toEqual(["a"])
|
||||||
|
expect(settled).toBe(false)
|
||||||
|
|
||||||
|
done?.()
|
||||||
|
expect(settled).toBe(true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,16 +1,42 @@
|
|||||||
export function terminalWriter(
|
export function terminalWriter(
|
||||||
write: (data: string) => void,
|
write: (data: string, done?: VoidFunction) => void,
|
||||||
schedule: (flush: VoidFunction) => void = queueMicrotask,
|
schedule: (flush: VoidFunction) => void = queueMicrotask,
|
||||||
) {
|
) {
|
||||||
let chunks: string[] | undefined
|
let chunks: string[] | undefined
|
||||||
|
let waits: VoidFunction[] | undefined
|
||||||
let scheduled = false
|
let scheduled = false
|
||||||
|
let writing = false
|
||||||
|
|
||||||
const flush = () => {
|
const settle = () => {
|
||||||
|
if (scheduled || writing || chunks?.length) return
|
||||||
|
const list = waits
|
||||||
|
if (!list?.length) return
|
||||||
|
waits = undefined
|
||||||
|
for (const fn of list) {
|
||||||
|
fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const run = () => {
|
||||||
|
if (writing) return
|
||||||
scheduled = false
|
scheduled = false
|
||||||
const items = chunks
|
const items = chunks
|
||||||
if (!items?.length) return
|
if (!items?.length) {
|
||||||
|
settle()
|
||||||
|
return
|
||||||
|
}
|
||||||
chunks = undefined
|
chunks = undefined
|
||||||
write(items.join(""))
|
writing = true
|
||||||
|
write(items.join(""), () => {
|
||||||
|
writing = false
|
||||||
|
if (chunks?.length) {
|
||||||
|
if (scheduled) return
|
||||||
|
scheduled = true
|
||||||
|
schedule(run)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
settle()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const push = (data: string) => {
|
const push = (data: string) => {
|
||||||
@@ -18,9 +44,21 @@ export function terminalWriter(
|
|||||||
if (chunks) chunks.push(data)
|
if (chunks) chunks.push(data)
|
||||||
else chunks = [data]
|
else chunks = [data]
|
||||||
|
|
||||||
if (scheduled) return
|
if (scheduled || writing) return
|
||||||
scheduled = true
|
scheduled = true
|
||||||
schedule(flush)
|
schedule(run)
|
||||||
|
}
|
||||||
|
|
||||||
|
const flush = (done?: VoidFunction) => {
|
||||||
|
if (!scheduled && !writing && !chunks?.length) {
|
||||||
|
done?.()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (done) {
|
||||||
|
if (waits) waits.push(done)
|
||||||
|
else waits = [done]
|
||||||
|
}
|
||||||
|
run()
|
||||||
}
|
}
|
||||||
|
|
||||||
return { push, flush }
|
return { push, flush }
|
||||||
|
|||||||
@@ -6,13 +6,17 @@ use process_wrap::tokio::ProcessGroup;
|
|||||||
use process_wrap::tokio::{JobObject, KillOnDrop};
|
use process_wrap::tokio::{JobObject, KillOnDrop};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::process::ExitStatusExt;
|
use std::os::unix::process::ExitStatusExt;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::{process::Stdio, time::Duration};
|
use std::{process::Stdio, time::Duration};
|
||||||
use tauri::{AppHandle, Manager, path::BaseDirectory};
|
use tauri::{AppHandle, Manager, path::BaseDirectory};
|
||||||
use tauri_plugin_store::StoreExt;
|
use tauri_plugin_store::StoreExt;
|
||||||
use tauri_specta::Event;
|
use tauri_specta::Event;
|
||||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
use tokio::{
|
||||||
use tokio::process::Command;
|
io::{AsyncBufRead, AsyncBufReadExt, BufReader},
|
||||||
use tokio::sync::{mpsc, oneshot};
|
process::Command,
|
||||||
|
sync::{mpsc, oneshot},
|
||||||
|
task::JoinHandle,
|
||||||
|
};
|
||||||
use tokio_stream::wrappers::ReceiverStream;
|
use tokio_stream::wrappers::ReceiverStream;
|
||||||
use tracing::Instrument;
|
use tracing::Instrument;
|
||||||
|
|
||||||
@@ -34,8 +38,8 @@ pub struct Config {
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum CommandEvent {
|
pub enum CommandEvent {
|
||||||
Stdout(Vec<u8>),
|
Stdout(String),
|
||||||
Stderr(Vec<u8>),
|
Stderr(String),
|
||||||
Error(String),
|
Error(String),
|
||||||
Terminated(TerminatedPayload),
|
Terminated(TerminatedPayload),
|
||||||
}
|
}
|
||||||
@@ -64,10 +68,11 @@ pub async fn get_config(app: &AppHandle) -> Option<Config> {
|
|||||||
|
|
||||||
events
|
events
|
||||||
.fold(String::new(), async |mut config_str, event| {
|
.fold(String::new(), async |mut config_str, event| {
|
||||||
if let CommandEvent::Stdout(stdout) = event
|
if let CommandEvent::Stdout(s) = &event {
|
||||||
&& let Ok(s) = str::from_utf8(&stdout)
|
config_str += s.as_str()
|
||||||
{
|
}
|
||||||
config_str += s
|
if let CommandEvent::Stderr(s) = &event {
|
||||||
|
config_str += s.as_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
config_str
|
config_str
|
||||||
@@ -317,9 +322,9 @@ pub fn spawn_command(
|
|||||||
cmd
|
cmd
|
||||||
};
|
};
|
||||||
|
|
||||||
cmd.stdin(Stdio::null());
|
|
||||||
cmd.stdout(Stdio::piped());
|
cmd.stdout(Stdio::piped());
|
||||||
cmd.stderr(Stdio::piped());
|
cmd.stderr(Stdio::piped());
|
||||||
|
cmd.stdin(Stdio::null());
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
cmd.creation_flags(0x0800_0000);
|
cmd.creation_flags(0x0800_0000);
|
||||||
@@ -337,32 +342,24 @@ pub fn spawn_command(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut child = wrap.spawn()?;
|
let mut child = wrap.spawn()?;
|
||||||
let stdout = child.stdout().take();
|
let guard = Arc::new(tokio::sync::RwLock::new(()));
|
||||||
let stderr = child.stderr().take();
|
|
||||||
let (tx, rx) = mpsc::channel(256);
|
let (tx, rx) = mpsc::channel(256);
|
||||||
let (kill_tx, mut kill_rx) = mpsc::channel(1);
|
let (kill_tx, mut kill_rx) = mpsc::channel(1);
|
||||||
|
|
||||||
if let Some(stdout) = stdout {
|
let stdout = spawn_pipe_reader(
|
||||||
let tx = tx.clone();
|
tx.clone(),
|
||||||
tokio::spawn(async move {
|
guard.clone(),
|
||||||
let mut lines = BufReader::new(stdout).lines();
|
BufReader::new(child.stdout().take().unwrap()),
|
||||||
while let Ok(Some(line)) = lines.next_line().await {
|
CommandEvent::Stdout,
|
||||||
let _ = tx.send(CommandEvent::Stdout(line.into_bytes())).await;
|
);
|
||||||
}
|
let stderr = spawn_pipe_reader(
|
||||||
});
|
tx.clone(),
|
||||||
}
|
guard.clone(),
|
||||||
|
BufReader::new(child.stderr().take().unwrap()),
|
||||||
|
CommandEvent::Stderr,
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(stderr) = stderr {
|
tokio::task::spawn(async move {
|
||||||
let tx = tx.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let mut lines = BufReader::new(stderr).lines();
|
|
||||||
while let Ok(Some(line)) = lines.next_line().await {
|
|
||||||
let _ = tx.send(CommandEvent::Stderr(line.into_bytes())).await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let mut kill_open = true;
|
let mut kill_open = true;
|
||||||
let status = loop {
|
let status = loop {
|
||||||
match child.try_wait() {
|
match child.try_wait() {
|
||||||
@@ -394,6 +391,9 @@ pub fn spawn_command(
|
|||||||
let _ = tx.send(CommandEvent::Error(err.to_string())).await;
|
let _ = tx.send(CommandEvent::Error(err.to_string())).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stdout.abort();
|
||||||
|
stderr.abort();
|
||||||
});
|
});
|
||||||
|
|
||||||
let event_stream = ReceiverStream::new(rx);
|
let event_stream = ReceiverStream::new(rx);
|
||||||
@@ -404,9 +404,7 @@ pub fn spawn_command(
|
|||||||
|
|
||||||
fn signal_from_status(status: std::process::ExitStatus) -> Option<i32> {
|
fn signal_from_status(status: std::process::ExitStatus) -> Option<i32> {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
return status.signal();
|
||||||
return status.signal();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
{
|
{
|
||||||
@@ -442,12 +440,10 @@ pub fn serve(
|
|||||||
events
|
events
|
||||||
.for_each(move |event| {
|
.for_each(move |event| {
|
||||||
match event {
|
match event {
|
||||||
CommandEvent::Stdout(line_bytes) => {
|
CommandEvent::Stdout(line) => {
|
||||||
let line = String::from_utf8_lossy(&line_bytes);
|
|
||||||
tracing::info!("{line}");
|
tracing::info!("{line}");
|
||||||
}
|
}
|
||||||
CommandEvent::Stderr(line_bytes) => {
|
CommandEvent::Stderr(line) => {
|
||||||
let line = String::from_utf8_lossy(&line_bytes);
|
|
||||||
tracing::info!("{line}");
|
tracing::info!("{line}");
|
||||||
}
|
}
|
||||||
CommandEvent::Error(err) => {
|
CommandEvent::Error(err) => {
|
||||||
@@ -499,11 +495,7 @@ pub mod sqlite_migration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
future::ready(match &event {
|
future::ready(match &event {
|
||||||
CommandEvent::Stdout(stdout) => {
|
CommandEvent::Stdout(s) | CommandEvent::Stderr(s) => {
|
||||||
let Ok(s) = str::from_utf8(stdout) else {
|
|
||||||
return future::ready(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(s) = s.strip_prefix("sqlite-migration:").map(|s| s.trim()) {
|
if let Some(s) = s.strip_prefix("sqlite-migration:").map(|s| s.trim()) {
|
||||||
if let Ok(progress) = s.parse::<u8>() {
|
if let Ok(progress) = s.parse::<u8>() {
|
||||||
let _ = SqliteMigrationProgress::InProgress(progress).emit(&app);
|
let _ = SqliteMigrationProgress::InProgress(progress).emit(&app);
|
||||||
@@ -522,3 +514,41 @@ pub mod sqlite_migration {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn spawn_pipe_reader<F: Fn(String) -> CommandEvent + Send + Copy + 'static>(
|
||||||
|
tx: mpsc::Sender<CommandEvent>,
|
||||||
|
guard: Arc<tokio::sync::RwLock<()>>,
|
||||||
|
pipe_reader: impl AsyncBufRead + Send + Unpin + 'static,
|
||||||
|
wrapper: F,
|
||||||
|
) -> JoinHandle<()> {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _lock = guard.read().await;
|
||||||
|
let reader = BufReader::new(pipe_reader);
|
||||||
|
|
||||||
|
read_line(reader, tx, wrapper).await;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_line<F: Fn(String) -> CommandEvent + Send + Copy + 'static>(
|
||||||
|
reader: BufReader<impl AsyncBufRead + Unpin>,
|
||||||
|
tx: mpsc::Sender<CommandEvent>,
|
||||||
|
wrapper: F,
|
||||||
|
) {
|
||||||
|
let mut lines = reader.lines();
|
||||||
|
loop {
|
||||||
|
let line = lines.next_line().await;
|
||||||
|
|
||||||
|
match line {
|
||||||
|
Ok(s) => {
|
||||||
|
if let Some(s) = s {
|
||||||
|
let _ = tx.clone().send(wrapper(s)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let tx_ = tx.clone();
|
||||||
|
let _ = tx_.send(CommandEvent::Error(e.to_string())).await;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -159,6 +159,38 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string):
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a deduplicated list of plugin-registered auth providers that are not
|
||||||
|
* already present in models.dev, respecting enabled/disabled provider lists.
|
||||||
|
* Pure function with no side effects; safe to test without mocking.
|
||||||
|
*/
|
||||||
|
export function resolvePluginProviders(input: {
|
||||||
|
hooks: Hooks[]
|
||||||
|
existingProviders: Record<string, unknown>
|
||||||
|
disabled: Set<string>
|
||||||
|
enabled?: Set<string>
|
||||||
|
providerNames: Record<string, string | undefined>
|
||||||
|
}): Array<{ id: string; name: string }> {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const result: Array<{ id: string; name: string }> = []
|
||||||
|
|
||||||
|
for (const hook of input.hooks) {
|
||||||
|
if (!hook.auth) continue
|
||||||
|
const id = hook.auth.provider
|
||||||
|
if (seen.has(id)) continue
|
||||||
|
seen.add(id)
|
||||||
|
if (Object.hasOwn(input.existingProviders, id)) continue
|
||||||
|
if (input.disabled.has(id)) continue
|
||||||
|
if (input.enabled && !input.enabled.has(id)) continue
|
||||||
|
result.push({
|
||||||
|
id,
|
||||||
|
name: input.providerNames[id] ?? id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
export const AuthCommand = cmd({
|
export const AuthCommand = cmd({
|
||||||
command: "auth",
|
command: "auth",
|
||||||
describe: "manage credentials",
|
describe: "manage credentials",
|
||||||
@@ -277,6 +309,13 @@ export const AuthLoginCommand = cmd({
|
|||||||
openrouter: 5,
|
openrouter: 5,
|
||||||
vercel: 6,
|
vercel: 6,
|
||||||
}
|
}
|
||||||
|
const pluginProviders = resolvePluginProviders({
|
||||||
|
hooks: await Plugin.list(),
|
||||||
|
existingProviders: providers,
|
||||||
|
disabled,
|
||||||
|
enabled,
|
||||||
|
providerNames: Object.fromEntries(Object.entries(config.provider ?? {}).map(([id, p]) => [id, p.name])),
|
||||||
|
})
|
||||||
let provider = await prompts.autocomplete({
|
let provider = await prompts.autocomplete({
|
||||||
message: "Select provider",
|
message: "Select provider",
|
||||||
maxItems: 8,
|
maxItems: 8,
|
||||||
@@ -298,6 +337,11 @@ export const AuthLoginCommand = cmd({
|
|||||||
}[x.id],
|
}[x.id],
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
|
...pluginProviders.map((x) => ({
|
||||||
|
label: x.name,
|
||||||
|
value: x.id,
|
||||||
|
hint: "plugin",
|
||||||
|
})),
|
||||||
{
|
{
|
||||||
value: "other",
|
value: "other",
|
||||||
label: "Other",
|
label: "Other",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const ExportCommand = cmd({
|
|||||||
handler: async (args) => {
|
handler: async (args) => {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
let sessionID = args.sessionID
|
let sessionID = args.sessionID
|
||||||
process.stderr.write(`Exporting session: ${sessionID ?? "latest"}`)
|
process.stderr.write(`Exporting session: ${sessionID ?? "latest"}\n`)
|
||||||
|
|
||||||
if (!sessionID) {
|
if (!sessionID) {
|
||||||
UI.empty()
|
UI.empty()
|
||||||
|
|||||||
@@ -64,12 +64,16 @@ function EditBody(props: { request: PermissionRequest }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<box flexDirection="column" gap={1}>
|
<box flexDirection="column" gap={1}>
|
||||||
<box flexDirection="row" gap={1} paddingLeft={1}>
|
|
||||||
<text fg={theme.textMuted}>{"→"}</text>
|
|
||||||
<text fg={theme.textMuted}>Edit {normalizePath(filepath())}</text>
|
|
||||||
</box>
|
|
||||||
<Show when={diff()}>
|
<Show when={diff()}>
|
||||||
<scrollbox height="100%">
|
<scrollbox
|
||||||
|
height="100%"
|
||||||
|
verticalScrollbarOptions={{
|
||||||
|
trackOptions: {
|
||||||
|
backgroundColor: theme.background,
|
||||||
|
foregroundColor: theme.borderActive,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
<diff
|
<diff
|
||||||
diff={diff()}
|
diff={diff()}
|
||||||
view={view()}
|
view={view()}
|
||||||
@@ -91,6 +95,11 @@ function EditBody(props: { request: PermissionRequest }) {
|
|||||||
/>
|
/>
|
||||||
</scrollbox>
|
</scrollbox>
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when={!diff()}>
|
||||||
|
<box paddingLeft={1}>
|
||||||
|
<text fg={theme.textMuted}>No diff provided</text>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -194,76 +203,233 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
|
|||||||
</Match>
|
</Match>
|
||||||
<Match when={store.stage === "permission"}>
|
<Match when={store.stage === "permission"}>
|
||||||
{(() => {
|
{(() => {
|
||||||
|
const info = () => {
|
||||||
|
const permission = props.request.permission
|
||||||
|
const data = input()
|
||||||
|
|
||||||
|
if (permission === "edit") {
|
||||||
|
const raw = props.request.metadata?.filepath
|
||||||
|
const filepath = typeof raw === "string" ? raw : ""
|
||||||
|
return {
|
||||||
|
icon: "→",
|
||||||
|
title: `Edit ${normalizePath(filepath)}`,
|
||||||
|
body: <EditBody request={props.request} />,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permission === "read") {
|
||||||
|
const raw = data.filePath
|
||||||
|
const filePath = typeof raw === "string" ? raw : ""
|
||||||
|
return {
|
||||||
|
icon: "→",
|
||||||
|
title: `Read ${normalizePath(filePath)}`,
|
||||||
|
body: (
|
||||||
|
<Show when={filePath}>
|
||||||
|
<box paddingLeft={1}>
|
||||||
|
<text fg={theme.textMuted}>{"Path: " + normalizePath(filePath)}</text>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permission === "glob") {
|
||||||
|
const pattern = typeof data.pattern === "string" ? data.pattern : ""
|
||||||
|
return {
|
||||||
|
icon: "✱",
|
||||||
|
title: `Glob "${pattern}"`,
|
||||||
|
body: (
|
||||||
|
<Show when={pattern}>
|
||||||
|
<box paddingLeft={1}>
|
||||||
|
<text fg={theme.textMuted}>{"Pattern: " + pattern}</text>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permission === "grep") {
|
||||||
|
const pattern = typeof data.pattern === "string" ? data.pattern : ""
|
||||||
|
return {
|
||||||
|
icon: "✱",
|
||||||
|
title: `Grep "${pattern}"`,
|
||||||
|
body: (
|
||||||
|
<Show when={pattern}>
|
||||||
|
<box paddingLeft={1}>
|
||||||
|
<text fg={theme.textMuted}>{"Pattern: " + pattern}</text>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permission === "list") {
|
||||||
|
const raw = data.path
|
||||||
|
const dir = typeof raw === "string" ? raw : ""
|
||||||
|
return {
|
||||||
|
icon: "→",
|
||||||
|
title: `List ${normalizePath(dir)}`,
|
||||||
|
body: (
|
||||||
|
<Show when={dir}>
|
||||||
|
<box paddingLeft={1}>
|
||||||
|
<text fg={theme.textMuted}>{"Path: " + normalizePath(dir)}</text>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permission === "bash") {
|
||||||
|
const title =
|
||||||
|
typeof data.description === "string" && data.description ? data.description : "Shell command"
|
||||||
|
const command = typeof data.command === "string" ? data.command : ""
|
||||||
|
return {
|
||||||
|
icon: "#",
|
||||||
|
title,
|
||||||
|
body: (
|
||||||
|
<Show when={command}>
|
||||||
|
<box paddingLeft={1}>
|
||||||
|
<text fg={theme.text}>{"$ " + command}</text>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permission === "task") {
|
||||||
|
const type = typeof data.subagent_type === "string" ? data.subagent_type : "Unknown"
|
||||||
|
const desc = typeof data.description === "string" ? data.description : ""
|
||||||
|
return {
|
||||||
|
icon: "#",
|
||||||
|
title: `${Locale.titlecase(type)} Task`,
|
||||||
|
body: (
|
||||||
|
<Show when={desc}>
|
||||||
|
<box paddingLeft={1}>
|
||||||
|
<text fg={theme.text}>{"◉ " + desc}</text>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permission === "webfetch") {
|
||||||
|
const url = typeof data.url === "string" ? data.url : ""
|
||||||
|
return {
|
||||||
|
icon: "%",
|
||||||
|
title: `WebFetch ${url}`,
|
||||||
|
body: (
|
||||||
|
<Show when={url}>
|
||||||
|
<box paddingLeft={1}>
|
||||||
|
<text fg={theme.textMuted}>{"URL: " + url}</text>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permission === "websearch") {
|
||||||
|
const query = typeof data.query === "string" ? data.query : ""
|
||||||
|
return {
|
||||||
|
icon: "◈",
|
||||||
|
title: `Exa Web Search "${query}"`,
|
||||||
|
body: (
|
||||||
|
<Show when={query}>
|
||||||
|
<box paddingLeft={1}>
|
||||||
|
<text fg={theme.textMuted}>{"Query: " + query}</text>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permission === "codesearch") {
|
||||||
|
const query = typeof data.query === "string" ? data.query : ""
|
||||||
|
return {
|
||||||
|
icon: "◇",
|
||||||
|
title: `Exa Code Search "${query}"`,
|
||||||
|
body: (
|
||||||
|
<Show when={query}>
|
||||||
|
<box paddingLeft={1}>
|
||||||
|
<text fg={theme.textMuted}>{"Query: " + query}</text>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permission === "external_directory") {
|
||||||
|
const meta = props.request.metadata ?? {}
|
||||||
|
const parent = typeof meta["parentDir"] === "string" ? meta["parentDir"] : undefined
|
||||||
|
const filepath = typeof meta["filepath"] === "string" ? meta["filepath"] : undefined
|
||||||
|
const pattern = props.request.patterns?.[0]
|
||||||
|
const derived =
|
||||||
|
typeof pattern === "string" ? (pattern.includes("*") ? path.dirname(pattern) : pattern) : undefined
|
||||||
|
|
||||||
|
const raw = parent ?? filepath ?? derived
|
||||||
|
const dir = normalizePath(raw)
|
||||||
|
const patterns = (props.request.patterns ?? []).filter((p): p is string => typeof p === "string")
|
||||||
|
|
||||||
|
return {
|
||||||
|
icon: "←",
|
||||||
|
title: `Access external directory ${dir}`,
|
||||||
|
body: (
|
||||||
|
<Show when={patterns.length > 0}>
|
||||||
|
<box paddingLeft={1} gap={1}>
|
||||||
|
<text fg={theme.textMuted}>Patterns</text>
|
||||||
|
<box>
|
||||||
|
<For each={patterns}>{(p) => <text fg={theme.text}>{"- " + p}</text>}</For>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permission === "doom_loop") {
|
||||||
|
return {
|
||||||
|
icon: "⟳",
|
||||||
|
title: "Continue after repeated failures",
|
||||||
|
body: (
|
||||||
|
<box paddingLeft={1}>
|
||||||
|
<text fg={theme.textMuted}>This keeps the session running despite repeated failures.</text>
|
||||||
|
</box>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
icon: "⚙",
|
||||||
|
title: `Call tool ${permission}`,
|
||||||
|
body: (
|
||||||
|
<box paddingLeft={1}>
|
||||||
|
<text fg={theme.textMuted}>{"Tool: " + permission}</text>
|
||||||
|
</box>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = info()
|
||||||
|
|
||||||
|
const header = () => (
|
||||||
|
<box flexDirection="column" gap={0}>
|
||||||
|
<box flexDirection="row" gap={1} flexShrink={0}>
|
||||||
|
<text fg={theme.warning}>{"△"}</text>
|
||||||
|
<text fg={theme.text}>Permission required</text>
|
||||||
|
</box>
|
||||||
|
<box flexDirection="row" gap={1} paddingLeft={2} flexShrink={0}>
|
||||||
|
<text fg={theme.textMuted} flexShrink={0}>
|
||||||
|
{current.icon}
|
||||||
|
</text>
|
||||||
|
<text fg={theme.text}>{current.title}</text>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
|
||||||
const body = (
|
const body = (
|
||||||
<Prompt
|
<Prompt
|
||||||
title="Permission required"
|
title="Permission required"
|
||||||
body={
|
header={header()}
|
||||||
<Switch>
|
body={current.body}
|
||||||
<Match when={props.request.permission === "edit"}>
|
|
||||||
<EditBody request={props.request} />
|
|
||||||
</Match>
|
|
||||||
<Match when={props.request.permission === "read"}>
|
|
||||||
<TextBody icon="→" title={`Read ` + normalizePath(input().filePath as string)} />
|
|
||||||
</Match>
|
|
||||||
<Match when={props.request.permission === "glob"}>
|
|
||||||
<TextBody icon="✱" title={`Glob "` + (input().pattern ?? "") + `"`} />
|
|
||||||
</Match>
|
|
||||||
<Match when={props.request.permission === "grep"}>
|
|
||||||
<TextBody icon="✱" title={`Grep "` + (input().pattern ?? "") + `"`} />
|
|
||||||
</Match>
|
|
||||||
<Match when={props.request.permission === "list"}>
|
|
||||||
<TextBody icon="→" title={`List ` + normalizePath(input().path as string)} />
|
|
||||||
</Match>
|
|
||||||
<Match when={props.request.permission === "bash"}>
|
|
||||||
<TextBody
|
|
||||||
icon="#"
|
|
||||||
title={(input().description as string) ?? ""}
|
|
||||||
description={("$ " + input().command) as string}
|
|
||||||
/>
|
|
||||||
</Match>
|
|
||||||
<Match when={props.request.permission === "task"}>
|
|
||||||
<TextBody
|
|
||||||
icon="#"
|
|
||||||
title={`${Locale.titlecase((input().subagent_type as string) ?? "Unknown")} Task`}
|
|
||||||
description={"◉ " + input().description}
|
|
||||||
/>
|
|
||||||
</Match>
|
|
||||||
<Match when={props.request.permission === "webfetch"}>
|
|
||||||
<TextBody icon="%" title={`WebFetch ` + (input().url ?? "")} />
|
|
||||||
</Match>
|
|
||||||
<Match when={props.request.permission === "websearch"}>
|
|
||||||
<TextBody icon="◈" title={`Exa Web Search "` + (input().query ?? "") + `"`} />
|
|
||||||
</Match>
|
|
||||||
<Match when={props.request.permission === "codesearch"}>
|
|
||||||
<TextBody icon="◇" title={`Exa Code Search "` + (input().query ?? "") + `"`} />
|
|
||||||
</Match>
|
|
||||||
<Match when={props.request.permission === "external_directory"}>
|
|
||||||
{(() => {
|
|
||||||
const meta = props.request.metadata ?? {}
|
|
||||||
const parent = typeof meta["parentDir"] === "string" ? meta["parentDir"] : undefined
|
|
||||||
const filepath = typeof meta["filepath"] === "string" ? meta["filepath"] : undefined
|
|
||||||
const pattern = props.request.patterns?.[0]
|
|
||||||
const derived =
|
|
||||||
typeof pattern === "string"
|
|
||||||
? pattern.includes("*")
|
|
||||||
? path.dirname(pattern)
|
|
||||||
: pattern
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const raw = parent ?? filepath ?? derived
|
|
||||||
const dir = normalizePath(raw)
|
|
||||||
|
|
||||||
return <TextBody icon="←" title={`Access external directory ` + dir} />
|
|
||||||
})()}
|
|
||||||
</Match>
|
|
||||||
<Match when={props.request.permission === "doom_loop"}>
|
|
||||||
<TextBody icon="⟳" title="Continue after repeated failures" />
|
|
||||||
</Match>
|
|
||||||
<Match when={true}>
|
|
||||||
<TextBody icon="⚙" title={`Call tool ` + props.request.permission} />
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
}
|
|
||||||
options={{ once: "Allow once", always: "Allow always", reject: "Reject" }}
|
options={{ once: "Allow once", always: "Allow always", reject: "Reject" }}
|
||||||
escapeKey="reject"
|
escapeKey="reject"
|
||||||
fullscreen
|
fullscreen
|
||||||
@@ -372,6 +538,7 @@ function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: (
|
|||||||
|
|
||||||
function Prompt<const T extends Record<string, string>>(props: {
|
function Prompt<const T extends Record<string, string>>(props: {
|
||||||
title: string
|
title: string
|
||||||
|
header?: JSX.Element
|
||||||
body: JSX.Element
|
body: JSX.Element
|
||||||
options: T
|
options: T
|
||||||
escapeKey?: keyof T
|
escapeKey?: keyof T
|
||||||
@@ -445,10 +612,19 @@ function Prompt<const T extends Record<string, string>>(props: {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1} flexGrow={1}>
|
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1} flexGrow={1}>
|
||||||
<box flexDirection="row" gap={1} paddingLeft={1} flexShrink={0}>
|
<Show
|
||||||
<text fg={theme.warning}>{"△"}</text>
|
when={props.header}
|
||||||
<text fg={theme.text}>{props.title}</text>
|
fallback={
|
||||||
</box>
|
<box flexDirection="row" gap={1} paddingLeft={1} flexShrink={0}>
|
||||||
|
<text fg={theme.warning}>{"△"}</text>
|
||||||
|
<text fg={theme.text}>{props.title}</text>
|
||||||
|
</box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<box paddingLeft={1} flexShrink={0}>
|
||||||
|
{props.header}
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
{props.body}
|
{props.body}
|
||||||
</box>
|
</box>
|
||||||
<box
|
<box
|
||||||
|
|||||||
@@ -80,7 +80,15 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
|
|||||||
paddingRight={2}
|
paddingRight={2}
|
||||||
position={props.overlay ? "absolute" : "relative"}
|
position={props.overlay ? "absolute" : "relative"}
|
||||||
>
|
>
|
||||||
<scrollbox flexGrow={1}>
|
<scrollbox
|
||||||
|
flexGrow={1}
|
||||||
|
verticalScrollbarOptions={{
|
||||||
|
trackOptions: {
|
||||||
|
backgroundColor: theme.background,
|
||||||
|
foregroundColor: theme.borderActive,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
<box flexShrink={0} gap={1} paddingRight={1}>
|
<box flexShrink={0} gap={1} paddingRight={1}>
|
||||||
<box paddingRight={1}>
|
<box paddingRight={1}>
|
||||||
<text fg={theme.text}>
|
<text fg={theme.text}>
|
||||||
|
|||||||
@@ -184,5 +184,6 @@ export const TuiThreadCommand = cmd({
|
|||||||
} finally {
|
} finally {
|
||||||
unguard?.()
|
unguard?.()
|
||||||
}
|
}
|
||||||
|
process.exit(0)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -137,7 +137,12 @@ export const rpc = {
|
|||||||
async shutdown() {
|
async shutdown() {
|
||||||
Log.Default.info("worker shutting down")
|
Log.Default.info("worker shutting down")
|
||||||
if (eventStream.abort) eventStream.abort.abort()
|
if (eventStream.abort) eventStream.abort.abort()
|
||||||
await Instance.disposeAll()
|
await Promise.race([
|
||||||
|
Instance.disposeAll(),
|
||||||
|
new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 5000)
|
||||||
|
}),
|
||||||
|
])
|
||||||
if (server) server.stop(true)
|
if (server) server.stop(true)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,6 +149,28 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|||||||
}
|
}
|
||||||
return entry.oauthState
|
return entry.oauthState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async invalidateCredentials(type: "all" | "client" | "tokens"): Promise<void> {
|
||||||
|
log.info("invalidating credentials", { mcpName: this.mcpName, type })
|
||||||
|
const entry = await McpAuth.get(this.mcpName)
|
||||||
|
if (!entry) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "all":
|
||||||
|
await McpAuth.remove(this.mcpName)
|
||||||
|
break
|
||||||
|
case "client":
|
||||||
|
delete entry.clientInfo
|
||||||
|
await McpAuth.set(this.mcpName, entry)
|
||||||
|
break
|
||||||
|
case "tokens":
|
||||||
|
delete entry.tokens
|
||||||
|
await McpAuth.set(this.mcpName, entry)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH }
|
export { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH }
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import * as fs from "fs/promises"
|
import * as fs from "fs/promises"
|
||||||
import { readFileSync } from "fs"
|
|
||||||
import { Log } from "../util/log"
|
import { Log } from "../util/log"
|
||||||
|
|
||||||
export namespace Patch {
|
export namespace Patch {
|
||||||
@@ -308,16 +307,18 @@ export namespace Patch {
|
|||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deriveNewContentsFromChunks(filePath: string, chunks: UpdateFileChunk[]): ApplyPatchFileUpdate {
|
export async function deriveNewContentsFromChunks(
|
||||||
// Read original file content
|
filePath: string,
|
||||||
let originalContent: string
|
chunks: UpdateFileChunk[],
|
||||||
try {
|
originalContent?: string,
|
||||||
originalContent = readFileSync(filePath, "utf-8")
|
): Promise<ApplyPatchFileUpdate> {
|
||||||
} catch (error) {
|
const content =
|
||||||
throw new Error(`Failed to read file ${filePath}: ${error}`)
|
originalContent ??
|
||||||
}
|
(await fs.readFile(filePath, "utf-8").catch((error) => {
|
||||||
|
throw new Error(`Failed to read file ${filePath}: ${error}`)
|
||||||
|
}))
|
||||||
|
|
||||||
let originalLines = originalContent.split("\n")
|
let originalLines = content.split("\n")
|
||||||
|
|
||||||
// Drop trailing empty element for consistent line counting
|
// Drop trailing empty element for consistent line counting
|
||||||
if (originalLines.length > 0 && originalLines[originalLines.length - 1] === "") {
|
if (originalLines.length > 0 && originalLines[originalLines.length - 1] === "") {
|
||||||
@@ -335,7 +336,7 @@ export namespace Patch {
|
|||||||
const newContent = newLines.join("\n")
|
const newContent = newLines.join("\n")
|
||||||
|
|
||||||
// Generate unified diff
|
// Generate unified diff
|
||||||
const unifiedDiff = generateUnifiedDiff(originalContent, newContent)
|
const unifiedDiff = generateUnifiedDiff(content, newContent)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
unified_diff: unifiedDiff,
|
unified_diff: unifiedDiff,
|
||||||
@@ -545,7 +546,7 @@ export namespace Patch {
|
|||||||
break
|
break
|
||||||
|
|
||||||
case "update":
|
case "update":
|
||||||
const fileUpdate = deriveNewContentsFromChunks(hunk.path, hunk.chunks)
|
const fileUpdate = await deriveNewContentsFromChunks(hunk.path, hunk.chunks)
|
||||||
|
|
||||||
if (hunk.move_path) {
|
if (hunk.move_path) {
|
||||||
// Handle file move
|
// Handle file move
|
||||||
@@ -641,7 +642,7 @@ export namespace Patch {
|
|||||||
case "update":
|
case "update":
|
||||||
const updatePath = path.resolve(effectiveCwd, hunk.path)
|
const updatePath = path.resolve(effectiveCwd, hunk.path)
|
||||||
try {
|
try {
|
||||||
const fileUpdate = deriveNewContentsFromChunks(updatePath, hunk.chunks)
|
const fileUpdate = await deriveNewContentsFromChunks(updatePath, hunk.chunks)
|
||||||
changes.set(resolvedPath, {
|
changes.set(resolvedPath, {
|
||||||
type: "update",
|
type: "update",
|
||||||
unified_diff: fileUpdate.unified_diff,
|
unified_diff: fileUpdate.unified_diff,
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export namespace ModelsDev {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Flag.OPENCODE_DISABLE_MODELS_FETCH) {
|
if (!Flag.OPENCODE_DISABLE_MODELS_FETCH && !process.argv.includes("--get-yargs-completions")) {
|
||||||
ModelsDev.refresh()
|
ModelsDev.refresh()
|
||||||
setInterval(
|
setInterval(
|
||||||
async () => {
|
async () => {
|
||||||
|
|||||||
@@ -578,6 +578,17 @@ export namespace Provider {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
kilo: async () => {
|
||||||
|
return {
|
||||||
|
autoload: false,
|
||||||
|
options: {
|
||||||
|
headers: {
|
||||||
|
"HTTP-Referer": "https://opencode.ai/",
|
||||||
|
"X-Title": "opencode",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Model = z
|
export const Model = z
|
||||||
|
|||||||
@@ -579,7 +579,7 @@ export namespace Session {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const updateMessage = fn(MessageV2.Info, async (msg) => {
|
export const updateMessage = fn(MessageV2.Info, async (msg) => {
|
||||||
const time_created = msg.role === "user" ? msg.time.created : msg.time.created
|
const time_created = msg.time.created
|
||||||
const { id, sessionID, ...data } = msg
|
const { id, sessionID, ...data } = msg
|
||||||
Database.use((db) => {
|
Database.use((db) => {
|
||||||
db.insert(MessageTable)
|
db.insert(MessageTable)
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
|
|||||||
|
|
||||||
// Apply the update chunks to get new content
|
// Apply the update chunks to get new content
|
||||||
try {
|
try {
|
||||||
const fileUpdate = Patch.deriveNewContentsFromChunks(filePath, hunk.chunks)
|
const fileUpdate = await Patch.deriveNewContentsFromChunks(filePath, hunk.chunks, oldContent)
|
||||||
newContent = fileUpdate.content
|
newContent = fileUpdate.content
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`apply_patch verification failed: ${error}`)
|
throw new Error(`apply_patch verification failed: ${error}`)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
import * as fs from "fs"
|
import { createReadStream } from "fs"
|
||||||
|
import * as fs from "fs/promises"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
|
import { createInterface } from "readline"
|
||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
import { LSP } from "../lsp"
|
import { LSP } from "../lsp"
|
||||||
import { FileTime } from "../file/time"
|
import { FileTime } from "../file/time"
|
||||||
@@ -11,7 +13,9 @@ import { InstructionPrompt } from "../session/instruction"
|
|||||||
|
|
||||||
const DEFAULT_READ_LIMIT = 2000
|
const DEFAULT_READ_LIMIT = 2000
|
||||||
const MAX_LINE_LENGTH = 2000
|
const MAX_LINE_LENGTH = 2000
|
||||||
|
const MAX_LINE_SUFFIX = `... (line truncated to ${MAX_LINE_LENGTH} chars)`
|
||||||
const MAX_BYTES = 50 * 1024
|
const MAX_BYTES = 50 * 1024
|
||||||
|
const MAX_BYTES_LABEL = `${MAX_BYTES / 1024} KB`
|
||||||
|
|
||||||
export const ReadTool = Tool.define("read", {
|
export const ReadTool = Tool.define("read", {
|
||||||
description: DESCRIPTION,
|
description: DESCRIPTION,
|
||||||
@@ -49,14 +53,18 @@ export const ReadTool = Tool.define("read", {
|
|||||||
const dir = path.dirname(filepath)
|
const dir = path.dirname(filepath)
|
||||||
const base = path.basename(filepath)
|
const base = path.basename(filepath)
|
||||||
|
|
||||||
const dirEntries = fs.readdirSync(dir)
|
const suggestions = await fs
|
||||||
const suggestions = dirEntries
|
.readdir(dir)
|
||||||
.filter(
|
.then((entries) =>
|
||||||
(entry) =>
|
entries
|
||||||
entry.toLowerCase().includes(base.toLowerCase()) || base.toLowerCase().includes(entry.toLowerCase()),
|
.filter(
|
||||||
|
(entry) =>
|
||||||
|
entry.toLowerCase().includes(base.toLowerCase()) || base.toLowerCase().includes(entry.toLowerCase()),
|
||||||
|
)
|
||||||
|
.map((entry) => path.join(dir, entry))
|
||||||
|
.slice(0, 3),
|
||||||
)
|
)
|
||||||
.map((entry) => path.join(dir, entry))
|
.catch(() => [])
|
||||||
.slice(0, 3)
|
|
||||||
|
|
||||||
if (suggestions.length > 0) {
|
if (suggestions.length > 0) {
|
||||||
throw new Error(`File not found: ${filepath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`)
|
throw new Error(`File not found: ${filepath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`)
|
||||||
@@ -66,12 +74,12 @@ export const ReadTool = Tool.define("read", {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
const dirents = await fs.promises.readdir(filepath, { withFileTypes: true })
|
const dirents = await fs.readdir(filepath, { withFileTypes: true })
|
||||||
const entries = await Promise.all(
|
const entries = await Promise.all(
|
||||||
dirents.map(async (dirent) => {
|
dirents.map(async (dirent) => {
|
||||||
if (dirent.isDirectory()) return dirent.name + "/"
|
if (dirent.isDirectory()) return dirent.name + "/"
|
||||||
if (dirent.isSymbolicLink()) {
|
if (dirent.isSymbolicLink()) {
|
||||||
const target = await fs.promises.stat(path.join(filepath, dirent.name)).catch(() => undefined)
|
const target = await fs.stat(path.join(filepath, dirent.name)).catch(() => undefined)
|
||||||
if (target?.isDirectory()) return dirent.name + "/"
|
if (target?.isDirectory()) return dirent.name + "/"
|
||||||
}
|
}
|
||||||
return dirent.name
|
return dirent.name
|
||||||
@@ -134,27 +142,53 @@ export const ReadTool = Tool.define("read", {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isBinary = await isBinaryFile(filepath, file)
|
const isBinary = await isBinaryFile(filepath, stat.size)
|
||||||
if (isBinary) throw new Error(`Cannot read binary file: ${filepath}`)
|
if (isBinary) throw new Error(`Cannot read binary file: ${filepath}`)
|
||||||
|
|
||||||
|
const stream = createReadStream(filepath, { encoding: "utf8" })
|
||||||
|
const rl = createInterface({
|
||||||
|
input: stream,
|
||||||
|
// Note: we use the crlfDelay option to recognize all instances of CR LF
|
||||||
|
// ('\r\n') in file as a single line break.
|
||||||
|
crlfDelay: Infinity,
|
||||||
|
})
|
||||||
|
|
||||||
const limit = params.limit ?? DEFAULT_READ_LIMIT
|
const limit = params.limit ?? DEFAULT_READ_LIMIT
|
||||||
const offset = params.offset ?? 1
|
const offset = params.offset ?? 1
|
||||||
const start = offset - 1
|
const start = offset - 1
|
||||||
const lines = await file.text().then((text) => text.split("\n"))
|
|
||||||
if (start >= lines.length) throw new Error(`Offset ${offset} is out of range for this file (${lines.length} lines)`)
|
|
||||||
|
|
||||||
const raw: string[] = []
|
const raw: string[] = []
|
||||||
let bytes = 0
|
let bytes = 0
|
||||||
|
let lines = 0
|
||||||
let truncatedByBytes = false
|
let truncatedByBytes = false
|
||||||
for (let i = start; i < Math.min(lines.length, start + limit); i++) {
|
let hasMoreLines = false
|
||||||
const line = lines[i].length > MAX_LINE_LENGTH ? lines[i].substring(0, MAX_LINE_LENGTH) + "..." : lines[i]
|
try {
|
||||||
const size = Buffer.byteLength(line, "utf-8") + (raw.length > 0 ? 1 : 0)
|
for await (const text of rl) {
|
||||||
if (bytes + size > MAX_BYTES) {
|
lines += 1
|
||||||
truncatedByBytes = true
|
if (lines <= start) continue
|
||||||
break
|
|
||||||
|
if (raw.length >= limit) {
|
||||||
|
hasMoreLines = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const line = text.length > MAX_LINE_LENGTH ? text.substring(0, MAX_LINE_LENGTH) + MAX_LINE_SUFFIX : text
|
||||||
|
const size = Buffer.byteLength(line, "utf-8") + (raw.length > 0 ? 1 : 0)
|
||||||
|
if (bytes + size > MAX_BYTES) {
|
||||||
|
truncatedByBytes = true
|
||||||
|
hasMoreLines = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
raw.push(line)
|
||||||
|
bytes += size
|
||||||
}
|
}
|
||||||
raw.push(line)
|
} finally {
|
||||||
bytes += size
|
rl.close()
|
||||||
|
stream.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lines < offset && !(lines === 0 && offset === 1)) {
|
||||||
|
throw new Error(`Offset ${offset} is out of range for this file (${lines} lines)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = raw.map((line, index) => {
|
const content = raw.map((line, index) => {
|
||||||
@@ -165,15 +199,15 @@ export const ReadTool = Tool.define("read", {
|
|||||||
let output = [`<path>${filepath}</path>`, `<type>file</type>`, "<content>"].join("\n")
|
let output = [`<path>${filepath}</path>`, `<type>file</type>`, "<content>"].join("\n")
|
||||||
output += content.join("\n")
|
output += content.join("\n")
|
||||||
|
|
||||||
const totalLines = lines.length
|
const totalLines = lines
|
||||||
const lastReadLine = offset + raw.length - 1
|
const lastReadLine = offset + raw.length - 1
|
||||||
const hasMoreLines = totalLines > lastReadLine
|
const nextOffset = lastReadLine + 1
|
||||||
const truncated = hasMoreLines || truncatedByBytes
|
const truncated = hasMoreLines || truncatedByBytes
|
||||||
|
|
||||||
if (truncatedByBytes) {
|
if (truncatedByBytes) {
|
||||||
output += `\n\n(Output truncated at ${MAX_BYTES} bytes. Use 'offset' parameter to read beyond line ${lastReadLine})`
|
output += `\n\n(Output capped at ${MAX_BYTES_LABEL}. Showing lines ${offset}-${lastReadLine}. Use offset=${nextOffset} to continue.)`
|
||||||
} else if (hasMoreLines) {
|
} else if (hasMoreLines) {
|
||||||
output += `\n\n(File has more lines. Use 'offset' parameter to read beyond line ${lastReadLine})`
|
output += `\n\n(Showing lines ${offset}-${lastReadLine} of ${totalLines}. Use offset=${nextOffset} to continue.)`
|
||||||
} else {
|
} else {
|
||||||
output += `\n\n(End of file - total ${totalLines} lines)`
|
output += `\n\n(End of file - total ${totalLines} lines)`
|
||||||
}
|
}
|
||||||
@@ -199,7 +233,7 @@ export const ReadTool = Tool.define("read", {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
async function isBinaryFile(filepath: string, file: Bun.BunFile): Promise<boolean> {
|
async function isBinaryFile(filepath: string, fileSize: number): Promise<boolean> {
|
||||||
const ext = path.extname(filepath).toLowerCase()
|
const ext = path.extname(filepath).toLowerCase()
|
||||||
// binary check for common non-text extensions
|
// binary check for common non-text extensions
|
||||||
switch (ext) {
|
switch (ext) {
|
||||||
@@ -236,22 +270,25 @@ async function isBinaryFile(filepath: string, file: Bun.BunFile): Promise<boolea
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
const stat = await file.stat()
|
|
||||||
const fileSize = stat.size
|
|
||||||
if (fileSize === 0) return false
|
if (fileSize === 0) return false
|
||||||
|
|
||||||
const bufferSize = Math.min(4096, fileSize)
|
const fh = await fs.open(filepath, "r")
|
||||||
const buffer = await file.arrayBuffer()
|
try {
|
||||||
if (buffer.byteLength === 0) return false
|
const sampleSize = Math.min(4096, fileSize)
|
||||||
const bytes = new Uint8Array(buffer.slice(0, bufferSize))
|
const bytes = Buffer.alloc(sampleSize)
|
||||||
|
const result = await fh.read(bytes, 0, sampleSize, 0)
|
||||||
|
if (result.bytesRead === 0) return false
|
||||||
|
|
||||||
let nonPrintableCount = 0
|
let nonPrintableCount = 0
|
||||||
for (let i = 0; i < bytes.length; i++) {
|
for (let i = 0; i < result.bytesRead; i++) {
|
||||||
if (bytes[i] === 0) return true
|
if (bytes[i] === 0) return true
|
||||||
if (bytes[i] < 9 || (bytes[i] > 13 && bytes[i] < 32)) {
|
if (bytes[i] < 9 || (bytes[i] > 13 && bytes[i] < 32)) {
|
||||||
nonPrintableCount++
|
nonPrintableCount++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// If >30% non-printable characters, consider it binary
|
||||||
|
return nonPrintableCount / result.bytesRead > 0.3
|
||||||
|
} finally {
|
||||||
|
await fh.close()
|
||||||
}
|
}
|
||||||
// If >30% non-printable characters, consider it binary
|
|
||||||
return nonPrintableCount / bytes.length > 0.3
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { test, expect, describe } from "bun:test"
|
||||||
|
import { resolvePluginProviders } from "../../src/cli/cmd/auth"
|
||||||
|
import type { Hooks } from "@opencode-ai/plugin"
|
||||||
|
|
||||||
|
function hookWithAuth(provider: string): Hooks {
|
||||||
|
return {
|
||||||
|
auth: {
|
||||||
|
provider,
|
||||||
|
methods: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hookWithoutAuth(): Hooks {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("resolvePluginProviders", () => {
|
||||||
|
test("returns plugin providers not in models.dev", () => {
|
||||||
|
const result = resolvePluginProviders({
|
||||||
|
hooks: [hookWithAuth("portkey")],
|
||||||
|
existingProviders: {},
|
||||||
|
disabled: new Set(),
|
||||||
|
providerNames: {},
|
||||||
|
})
|
||||||
|
expect(result).toEqual([{ id: "portkey", name: "portkey" }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("skips providers already in models.dev", () => {
|
||||||
|
const result = resolvePluginProviders({
|
||||||
|
hooks: [hookWithAuth("anthropic")],
|
||||||
|
existingProviders: { anthropic: {} },
|
||||||
|
disabled: new Set(),
|
||||||
|
providerNames: {},
|
||||||
|
})
|
||||||
|
expect(result).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("deduplicates across plugins", () => {
|
||||||
|
const result = resolvePluginProviders({
|
||||||
|
hooks: [hookWithAuth("portkey"), hookWithAuth("portkey")],
|
||||||
|
existingProviders: {},
|
||||||
|
disabled: new Set(),
|
||||||
|
providerNames: {},
|
||||||
|
})
|
||||||
|
expect(result).toEqual([{ id: "portkey", name: "portkey" }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("respects disabled_providers", () => {
|
||||||
|
const result = resolvePluginProviders({
|
||||||
|
hooks: [hookWithAuth("portkey")],
|
||||||
|
existingProviders: {},
|
||||||
|
disabled: new Set(["portkey"]),
|
||||||
|
providerNames: {},
|
||||||
|
})
|
||||||
|
expect(result).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("respects enabled_providers when provider is absent", () => {
|
||||||
|
const result = resolvePluginProviders({
|
||||||
|
hooks: [hookWithAuth("portkey")],
|
||||||
|
existingProviders: {},
|
||||||
|
disabled: new Set(),
|
||||||
|
enabled: new Set(["anthropic"]),
|
||||||
|
providerNames: {},
|
||||||
|
})
|
||||||
|
expect(result).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("includes provider when in enabled set", () => {
|
||||||
|
const result = resolvePluginProviders({
|
||||||
|
hooks: [hookWithAuth("portkey")],
|
||||||
|
existingProviders: {},
|
||||||
|
disabled: new Set(),
|
||||||
|
enabled: new Set(["portkey"]),
|
||||||
|
providerNames: {},
|
||||||
|
})
|
||||||
|
expect(result).toEqual([{ id: "portkey", name: "portkey" }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("resolves name from providerNames", () => {
|
||||||
|
const result = resolvePluginProviders({
|
||||||
|
hooks: [hookWithAuth("portkey")],
|
||||||
|
existingProviders: {},
|
||||||
|
disabled: new Set(),
|
||||||
|
providerNames: { portkey: "Portkey AI" },
|
||||||
|
})
|
||||||
|
expect(result).toEqual([{ id: "portkey", name: "Portkey AI" }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("falls back to id when no name configured", () => {
|
||||||
|
const result = resolvePluginProviders({
|
||||||
|
hooks: [hookWithAuth("portkey")],
|
||||||
|
existingProviders: {},
|
||||||
|
disabled: new Set(),
|
||||||
|
providerNames: {},
|
||||||
|
})
|
||||||
|
expect(result).toEqual([{ id: "portkey", name: "portkey" }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("skips hooks without auth", () => {
|
||||||
|
const result = resolvePluginProviders({
|
||||||
|
hooks: [hookWithoutAuth(), hookWithAuth("portkey"), hookWithoutAuth()],
|
||||||
|
existingProviders: {},
|
||||||
|
disabled: new Set(),
|
||||||
|
providerNames: {},
|
||||||
|
})
|
||||||
|
expect(result).toEqual([{ id: "portkey", name: "portkey" }])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns empty for no hooks", () => {
|
||||||
|
const result = resolvePluginProviders({
|
||||||
|
hooks: [],
|
||||||
|
existingProviders: {},
|
||||||
|
disabled: new Set(),
|
||||||
|
providerNames: {},
|
||||||
|
})
|
||||||
|
expect(result).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -211,8 +211,8 @@ describe("tool.read truncation", () => {
|
|||||||
const read = await ReadTool.init()
|
const read = await ReadTool.init()
|
||||||
const result = await read.execute({ filePath: path.join(tmp.path, "large.json") }, ctx)
|
const result = await read.execute({ filePath: path.join(tmp.path, "large.json") }, ctx)
|
||||||
expect(result.metadata.truncated).toBe(true)
|
expect(result.metadata.truncated).toBe(true)
|
||||||
expect(result.output).toContain("Output truncated at")
|
expect(result.output).toContain("Output capped at")
|
||||||
expect(result.output).toContain("bytes")
|
expect(result.output).toContain("Use offset=")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -230,7 +230,8 @@ describe("tool.read truncation", () => {
|
|||||||
const read = await ReadTool.init()
|
const read = await ReadTool.init()
|
||||||
const result = await read.execute({ filePath: path.join(tmp.path, "many-lines.txt"), limit: 10 }, ctx)
|
const result = await read.execute({ filePath: path.join(tmp.path, "many-lines.txt"), limit: 10 }, ctx)
|
||||||
expect(result.metadata.truncated).toBe(true)
|
expect(result.metadata.truncated).toBe(true)
|
||||||
expect(result.output).toContain("File has more lines")
|
expect(result.output).toContain("Showing lines 1-10 of 100")
|
||||||
|
expect(result.output).toContain("Use offset=11")
|
||||||
expect(result.output).toContain("line0")
|
expect(result.output).toContain("line0")
|
||||||
expect(result.output).toContain("line9")
|
expect(result.output).toContain("line9")
|
||||||
expect(result.output).not.toContain("line10")
|
expect(result.output).not.toContain("line10")
|
||||||
@@ -267,6 +268,10 @@ describe("tool.read truncation", () => {
|
|||||||
fn: async () => {
|
fn: async () => {
|
||||||
const read = await ReadTool.init()
|
const read = await ReadTool.init()
|
||||||
const result = await read.execute({ filePath: path.join(tmp.path, "offset.txt"), offset: 10, limit: 5 }, ctx)
|
const result = await read.execute({ filePath: path.join(tmp.path, "offset.txt"), offset: 10, limit: 5 }, ctx)
|
||||||
|
expect(result.output).toContain("10: line10")
|
||||||
|
expect(result.output).toContain("14: line14")
|
||||||
|
expect(result.output).not.toContain("9: line10")
|
||||||
|
expect(result.output).not.toContain("15: line15")
|
||||||
expect(result.output).toContain("line10")
|
expect(result.output).toContain("line10")
|
||||||
expect(result.output).toContain("line14")
|
expect(result.output).toContain("line14")
|
||||||
expect(result.output).not.toContain("line0")
|
expect(result.output).not.toContain("line0")
|
||||||
@@ -293,6 +298,40 @@ describe("tool.read truncation", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("allows reading empty file at default offset", async () => {
|
||||||
|
await using tmp = await tmpdir({
|
||||||
|
init: async (dir) => {
|
||||||
|
await Bun.write(path.join(dir, "empty.txt"), "")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
fn: async () => {
|
||||||
|
const read = await ReadTool.init()
|
||||||
|
const result = await read.execute({ filePath: path.join(tmp.path, "empty.txt") }, ctx)
|
||||||
|
expect(result.metadata.truncated).toBe(false)
|
||||||
|
expect(result.output).toContain("End of file - total 0 lines")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("throws when offset > 1 for empty file", async () => {
|
||||||
|
await using tmp = await tmpdir({
|
||||||
|
init: async (dir) => {
|
||||||
|
await Bun.write(path.join(dir, "empty.txt"), "")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
fn: async () => {
|
||||||
|
const read = await ReadTool.init()
|
||||||
|
await expect(read.execute({ filePath: path.join(tmp.path, "empty.txt"), offset: 2 }, ctx)).rejects.toThrow(
|
||||||
|
"Offset 2 is out of range for this file (0 lines)",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test("does not mark final directory page as truncated", async () => {
|
test("does not mark final directory page as truncated", async () => {
|
||||||
await using tmp = await tmpdir({
|
await using tmp = await tmpdir({
|
||||||
init: async (dir) => {
|
init: async (dir) => {
|
||||||
@@ -324,7 +363,7 @@ describe("tool.read truncation", () => {
|
|||||||
fn: async () => {
|
fn: async () => {
|
||||||
const read = await ReadTool.init()
|
const read = await ReadTool.init()
|
||||||
const result = await read.execute({ filePath: path.join(tmp.path, "long-line.txt") }, ctx)
|
const result = await read.execute({ filePath: path.join(tmp.path, "long-line.txt") }, ctx)
|
||||||
expect(result.output).toContain("...")
|
expect(result.output).toContain("(line truncated to 2000 chars)")
|
||||||
expect(result.output.length).toBeLessThan(3000)
|
expect(result.output.length).toBeLessThan(3000)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -425,3 +464,40 @@ describe("tool.read loaded instructions", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("tool.read binary detection", () => {
|
||||||
|
test("rejects text extension files with null bytes", async () => {
|
||||||
|
await using tmp = await tmpdir({
|
||||||
|
init: async (dir) => {
|
||||||
|
const bytes = Buffer.from([0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x77, 0x6f, 0x72, 0x6c, 0x64])
|
||||||
|
await Bun.write(path.join(dir, "null-byte.txt"), bytes)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
fn: async () => {
|
||||||
|
const read = await ReadTool.init()
|
||||||
|
await expect(read.execute({ filePath: path.join(tmp.path, "null-byte.txt") }, ctx)).rejects.toThrow(
|
||||||
|
"Cannot read binary file",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("rejects known binary extensions", async () => {
|
||||||
|
await using tmp = await tmpdir({
|
||||||
|
init: async (dir) => {
|
||||||
|
await Bun.write(path.join(dir, "module.wasm"), "not really wasm")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await Instance.provide({
|
||||||
|
directory: tmp.path,
|
||||||
|
fn: async () => {
|
||||||
|
const read = await ReadTool.init()
|
||||||
|
await expect(read.execute({ filePath: path.join(tmp.path, "module.wasm") }, ctx)).rejects.toThrow(
|
||||||
|
"Cannot read binary file",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&[data-size="small"] {
|
&[data-size="small"] {
|
||||||
height: 22px;
|
height: 24px;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
&[data-icon] {
|
&[data-icon] {
|
||||||
padding: 0 12px 0 4px;
|
padding: 0 12px 0 4px;
|
||||||
@@ -129,8 +129,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&[data-size="normal"] {
|
&[data-size="normal"] {
|
||||||
height: 24px;
|
height: 28px;
|
||||||
line-height: 24px;
|
line-height: 28px;
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
&[data-icon] {
|
&[data-icon] {
|
||||||
padding: 0 12px 0 4px;
|
padding: 0 12px 0 4px;
|
||||||
@@ -179,6 +179,10 @@
|
|||||||
background-color: var(--surface-base-active);
|
background-color: var(--surface-base-active);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-component="button"].titlebar-icon[data-variant="ghost"][aria-expanded="true"] [data-slot="icon-svg"] {
|
||||||
|
color: var(--icon-strong-base);
|
||||||
|
}
|
||||||
|
|
||||||
[data-component="button"].titlebar-icon[data-variant="ghost"][aria-expanded="true"]:hover:not(:disabled) {
|
[data-component="button"].titlebar-icon[data-variant="ghost"][aria-expanded="true"]:hover:not(:disabled) {
|
||||||
background-color: var(--surface-base-active);
|
background-color: var(--surface-base-active);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { JSX } from "solid-js"
|
||||||
|
|
||||||
|
export function DockPrompt(props: {
|
||||||
|
kind: "question" | "permission"
|
||||||
|
header: JSX.Element
|
||||||
|
children: JSX.Element
|
||||||
|
footer: JSX.Element
|
||||||
|
ref?: (el: HTMLDivElement) => void
|
||||||
|
}) {
|
||||||
|
const slot = (name: string) => `${props.kind}-${name}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-component="dock-prompt" data-kind={props.kind} ref={props.ref}>
|
||||||
|
<div data-slot={slot("body")}>
|
||||||
|
<div data-slot={slot("header")}>{props.header}</div>
|
||||||
|
<div data-slot={slot("content")}>{props.children}</div>
|
||||||
|
</div>
|
||||||
|
<div data-slot={slot("footer")}>{props.footer}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,3 +3,35 @@
|
|||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
File tree: show monochrome weak icons by default.
|
||||||
|
On hover, show the original file-type colors.
|
||||||
|
*/
|
||||||
|
[data-component="filetree"] .filetree-icon--mono {
|
||||||
|
color: var(--icon-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="filetree"] .filetree-iconpair {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="filetree"] .filetree-iconpair [data-component="file-icon"] {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="filetree"] .filetree-iconpair .filetree-icon--color {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="filetree"]:hover .filetree-iconpair .filetree-icon--color {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-component="filetree"]:hover .filetree-iconpair .filetree-icon--mono {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import type { Component, JSX } from "solid-js"
|
import type { Component, JSX } from "solid-js"
|
||||||
import { createMemo, splitProps } from "solid-js"
|
import { createMemo, splitProps, Show } from "solid-js"
|
||||||
import sprite from "./file-icons/sprite.svg"
|
import sprite from "./file-icons/sprite.svg"
|
||||||
import type { IconName } from "./file-icons/types"
|
import type { IconName } from "./file-icons/types"
|
||||||
|
|
||||||
|
let filter = 0
|
||||||
|
|
||||||
export type FileIconProps = JSX.GSVGAttributes<SVGSVGElement> & {
|
export type FileIconProps = JSX.GSVGAttributes<SVGSVGElement> & {
|
||||||
node: { path: string; type: "file" | "directory" }
|
node: { path: string; type: "file" | "directory" }
|
||||||
expanded?: boolean
|
expanded?: boolean
|
||||||
|
mono?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileIcon: Component<FileIconProps> = (props) => {
|
export const FileIcon: Component<FileIconProps> = (props) => {
|
||||||
const [local, rest] = splitProps(props, ["node", "class", "classList", "expanded"])
|
const [local, rest] = splitProps(props, ["node", "class", "classList", "expanded", "mono"])
|
||||||
const name = createMemo(() => chooseIconName(local.node.path, local.node.type, local.expanded || false))
|
const name = createMemo(() => chooseIconName(local.node.path, local.node.type, local.expanded || false))
|
||||||
|
const id = `file-icon-mono-${filter++}`
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
data-component="file-icon"
|
data-component="file-icon"
|
||||||
@@ -20,7 +24,15 @@ export const FileIcon: Component<FileIconProps> = (props) => {
|
|||||||
[local.class ?? ""]: !!local.class,
|
[local.class ?? ""]: !!local.class,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<use href={`${sprite}#${name()}`} />
|
<Show when={local.mono}>
|
||||||
|
<defs>
|
||||||
|
<filter id={id} color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-color="currentColor" result="flood" />
|
||||||
|
<feComposite in="flood" in2="SourceAlpha" operator="in" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</Show>
|
||||||
|
<use href={`${sprite}#${name()}`} filter={local.mono ? `url(#${id})` : undefined} />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,6 +176,10 @@
|
|||||||
background-color: var(--surface-base-active);
|
background-color: var(--surface-base-active);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-component="icon-button"].titlebar-icon[data-variant="ghost"][aria-expanded="true"] [data-slot="icon-svg"] {
|
||||||
|
color: var(--icon-strong-base);
|
||||||
|
}
|
||||||
|
|
||||||
[data-component="icon-button"].titlebar-icon[data-variant="ghost"][aria-expanded="true"]:hover:not(:disabled) {
|
[data-component="icon-button"].titlebar-icon[data-variant="ghost"][aria-expanded="true"]:hover:not(:disabled) {
|
||||||
background-color: var(--surface-base-active);
|
background-color: var(--surface-base-active);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const icons = {
|
|||||||
enter: `<path d="M5.83333 15.8334L2.5 12.5L5.83333 9.16671M3.33333 12.5H17.9167V4.58337H10" stroke="currentColor" stroke-linecap="square"/>`,
|
enter: `<path d="M5.83333 15.8334L2.5 12.5L5.83333 9.16671M3.33333 12.5H17.9167V4.58337H10" stroke="currentColor" stroke-linecap="square"/>`,
|
||||||
folder: `<path d="M2.08301 2.91675V16.2501H17.9163V5.41675H9.99967L8.33301 2.91675H2.08301Z" stroke="currentColor" stroke-linecap="round"/>`,
|
folder: `<path d="M2.08301 2.91675V16.2501H17.9163V5.41675H9.99967L8.33301 2.91675H2.08301Z" stroke="currentColor" stroke-linecap="round"/>`,
|
||||||
"file-tree": `<path d="M4.58203 16.6693L6.66536 9.58594H17.082M4.58203 16.6693H16.457L18.5404 9.58594H17.082M4.58203 16.6693H2.08203V3.33594H8.33203L9.9987 5.83594H17.082V9.58594" stroke="currentColor" stroke-linecap="round"/>`,
|
"file-tree": `<path d="M4.58203 16.6693L6.66536 9.58594H17.082M4.58203 16.6693H16.457L18.5404 9.58594H17.082M4.58203 16.6693H2.08203V3.33594H8.33203L9.9987 5.83594H17.082V9.58594" stroke="currentColor" stroke-linecap="round"/>`,
|
||||||
"file-tree-active": `<path d="M6.66536 9.58594L4.58203 16.6693H16.457L18.5404 9.58594H17.082H6.66536Z" fill="currentColor" fill-opacity="40%"/><path d="M4.58203 16.6693L6.66536 9.58594H17.082M4.58203 16.6693H16.457L18.5404 9.58594H17.082M4.58203 16.6693H2.08203V3.33594H8.33203L9.9987 5.83594H17.082V9.58594" stroke="currentColor" stroke-linecap="round"/>`,
|
"file-tree-active": `<path d="M6.66536 9.58594L4.58203 16.6693H16.457L18.5404 9.58594H17.082H6.66536Z" fill="currentColor" fill-opacity="16%"/><path d="M4.58203 16.6693L6.66536 9.58594H17.082M4.58203 16.6693H16.457L18.5404 9.58594H17.082M4.58203 16.6693H2.08203V3.33594H8.33203L9.9987 5.83594H17.082V9.58594" stroke="currentColor" stroke-linecap="round"/>`,
|
||||||
"magnifying-glass": `<path d="M13 13L10.6418 10.6418M11.9552 7.47761C11.9552 9.95053 9.95053 11.9552 7.47761 11.9552C5.0047 11.9552 3 9.95053 3 7.47761C3 5.0047 5.0047 3 7.47761 3C9.95053 3 11.9552 5.0047 11.9552 7.47761Z" stroke="currentColor" stroke-linecap="square" vector-effect="non-scaling-stroke"/>`,
|
"magnifying-glass": `<path d="M13 13L10.6418 10.6418M11.9552 7.47761C11.9552 9.95053 9.95053 11.9552 7.47761 11.9552C5.0047 11.9552 3 9.95053 3 7.47761C3 5.0047 5.0047 3 7.47761 3C9.95053 3 11.9552 5.0047 11.9552 7.47761Z" stroke="currentColor" stroke-linecap="square" vector-effect="non-scaling-stroke"/>`,
|
||||||
"plus-small": `<path d="M9.99984 5.41699V10.0003M9.99984 10.0003V14.5837M9.99984 10.0003H5.4165M9.99984 10.0003H14.5832" stroke="currentColor" stroke-linecap="square"/>`,
|
"plus-small": `<path d="M9.99984 5.41699V10.0003M9.99984 10.0003V14.5837M9.99984 10.0003H5.4165M9.99984 10.0003H14.5832" stroke="currentColor" stroke-linecap="square"/>`,
|
||||||
plus: `<path d="M9.9987 2.20703V9.9987M9.9987 9.9987V17.7904M9.9987 9.9987H2.20703M9.9987 9.9987H17.7904" stroke="currentColor" stroke-linecap="square"/>`,
|
plus: `<path d="M9.9987 2.20703V9.9987M9.9987 9.9987V17.7904M9.9987 9.9987H2.20703M9.9987 9.9987H17.7904" stroke="currentColor" stroke-linecap="square"/>`,
|
||||||
@@ -44,10 +44,10 @@ const icons = {
|
|||||||
task: `<path d="M9.99992 2.0835V17.9168M7.08325 3.75016H2.08325V16.2502H7.08325M12.9166 16.2502H17.9166V3.75016H12.9166" stroke="currentColor" stroke-linecap="square"/>`,
|
task: `<path d="M9.99992 2.0835V17.9168M7.08325 3.75016H2.08325V16.2502H7.08325M12.9166 16.2502H17.9166V3.75016H12.9166" stroke="currentColor" stroke-linecap="square"/>`,
|
||||||
stop: `<rect x="5" y="5" width="10" height="10" fill="currentColor"/>`,
|
stop: `<rect x="5" y="5" width="10" height="10" fill="currentColor"/>`,
|
||||||
"layout-left": `<path d="M2.91675 2.91699L2.91675 2.41699L2.41675 2.41699L2.41675 2.91699L2.91675 2.91699ZM17.0834 2.91699L17.5834 2.91699L17.5834 2.41699L17.0834 2.41699L17.0834 2.91699ZM17.0834 17.0837L17.0834 17.5837L17.5834 17.5837L17.5834 17.0837L17.0834 17.0837ZM2.91675 17.0837L2.41675 17.0837L2.41675 17.5837L2.91675 17.5837L2.91675 17.0837ZM7.41674 17.0837L7.41674 17.5837L8.41674 17.5837L8.41674 17.0837L7.91674 17.0837L7.41674 17.0837ZM8.41674 2.91699L8.41674 2.41699L7.41674 2.41699L7.41674 2.91699L7.91674 2.91699L8.41674 2.91699ZM2.91675 2.91699L2.91675 3.41699L17.0834 3.41699L17.0834 2.91699L17.0834 2.41699L2.91675 2.41699L2.91675 2.91699ZM17.0834 2.91699L16.5834 2.91699L16.5834 17.0837L17.0834 17.0837L17.5834 17.0837L17.5834 2.91699L17.0834 2.91699ZM17.0834 17.0837L17.0834 16.5837L2.91675 16.5837L2.91675 17.0837L2.91675 17.5837L17.0834 17.5837L17.0834 17.0837ZM2.91675 17.0837L3.41675 17.0837L3.41675 2.91699L2.91675 2.91699L2.41675 2.91699L2.41675 17.0837L2.91675 17.0837ZM7.91674 17.0837L8.41674 17.0837L8.41674 2.91699L7.91674 2.91699L7.41674 2.91699L7.41674 17.0837L7.91674 17.0837Z" fill="currentColor"/>`,
|
"layout-left": `<path d="M2.91675 2.91699L2.91675 2.41699L2.41675 2.41699L2.41675 2.91699L2.91675 2.91699ZM17.0834 2.91699L17.5834 2.91699L17.5834 2.41699L17.0834 2.41699L17.0834 2.91699ZM17.0834 17.0837L17.0834 17.5837L17.5834 17.5837L17.5834 17.0837L17.0834 17.0837ZM2.91675 17.0837L2.41675 17.0837L2.41675 17.5837L2.91675 17.5837L2.91675 17.0837ZM7.41674 17.0837L7.41674 17.5837L8.41674 17.5837L8.41674 17.0837L7.91674 17.0837L7.41674 17.0837ZM8.41674 2.91699L8.41674 2.41699L7.41674 2.41699L7.41674 2.91699L7.91674 2.91699L8.41674 2.91699ZM2.91675 2.91699L2.91675 3.41699L17.0834 3.41699L17.0834 2.91699L17.0834 2.41699L2.91675 2.41699L2.91675 2.91699ZM17.0834 2.91699L16.5834 2.91699L16.5834 17.0837L17.0834 17.0837L17.5834 17.0837L17.5834 2.91699L17.0834 2.91699ZM17.0834 17.0837L17.0834 16.5837L2.91675 16.5837L2.91675 17.0837L2.91675 17.5837L17.0834 17.5837L17.0834 17.0837ZM2.91675 17.0837L3.41675 17.0837L3.41675 2.91699L2.91675 2.91699L2.41675 2.91699L2.41675 17.0837L2.91675 17.0837ZM7.91674 17.0837L8.41674 17.0837L8.41674 2.91699L7.91674 2.91699L7.41674 2.91699L7.41674 17.0837L7.91674 17.0837Z" fill="currentColor"/>`,
|
||||||
"layout-left-partial": `<path d="M2.91732 2.91602L7.91732 2.91602L7.91732 17.0827H2.91732L2.91732 2.91602Z" fill="currentColor" fill-opacity="40%" /><path d="M2.91732 2.91602L17.084 2.91602M2.91732 2.91602L2.91732 17.0827M2.91732 2.91602L7.91732 2.91602M17.084 2.91602L17.084 17.0827M17.084 2.91602L7.91732 2.91602M17.084 17.0827L2.91732 17.0827M17.084 17.0827L7.91732 17.0827M2.91732 17.0827H7.91732M7.91732 17.0827L7.91732 2.91602" stroke="currentColor" stroke-linecap="square"/>`,
|
"layout-left-partial": `<path d="M2.91732 2.91602L7.91732 2.91602L7.91732 17.0827H2.91732L2.91732 2.91602Z" fill="currentColor" fill-opacity="16%" /><path d="M2.91732 2.91602L17.084 2.91602M2.91732 2.91602L2.91732 17.0827M2.91732 2.91602L7.91732 2.91602M17.084 2.91602L17.084 17.0827M17.084 2.91602L7.91732 2.91602M17.084 17.0827L2.91732 17.0827M17.084 17.0827L7.91732 17.0827M2.91732 17.0827H7.91732M7.91732 17.0827L7.91732 2.91602" stroke="currentColor" stroke-linecap="square"/>`,
|
||||||
"layout-left-full": `<path d="M2.91732 2.91602L7.91732 2.91602L7.91732 17.0827H2.91732L2.91732 2.91602Z" fill="currentColor"/><path d="M2.91732 2.91602L17.084 2.91602M2.91732 2.91602L2.91732 17.0827M2.91732 2.91602L7.91732 2.91602M17.084 2.91602L17.084 17.0827M17.084 2.91602L7.91732 2.91602M17.084 17.0827L2.91732 17.0827M17.084 17.0827L7.91732 17.0827M2.91732 17.0827H7.91732M7.91732 17.0827L7.91732 2.91602" stroke="currentColor" stroke-linecap="square"/>`,
|
"layout-left-full": `<path d="M2.91732 2.91602L7.91732 2.91602L7.91732 17.0827H2.91732L2.91732 2.91602Z" fill="currentColor"/><path d="M2.91732 2.91602L17.084 2.91602M2.91732 2.91602L2.91732 17.0827M2.91732 2.91602L7.91732 2.91602M17.084 2.91602L17.084 17.0827M17.084 2.91602L7.91732 2.91602M17.084 17.0827L2.91732 17.0827M17.084 17.0827L7.91732 17.0827M2.91732 17.0827H7.91732M7.91732 17.0827L7.91732 2.91602" stroke="currentColor" stroke-linecap="square"/>`,
|
||||||
"layout-right": `<path d="M2.91536 2.91406H2.36536V2.36406H2.91536V2.91406ZM2.91536 17.0807V17.6307H2.36536V17.0807H2.91536ZM17.082 17.0807H17.632V17.6307H17.082V17.0807ZM17.082 2.91406V2.36406H17.632V2.91406H17.082ZM6.9987 2.91406H6.4487V2.36406H6.9987V2.91406ZM6.9987 17.0807V17.6307H6.4487V17.0807H6.9987ZM2.91536 2.91406H3.46536V17.0807H2.91536H2.36536V2.91406H2.91536ZM2.91536 17.0807V16.5307H17.082V17.0807V17.6307H2.91536V17.0807ZM17.082 17.0807H16.532V2.91406H17.082H17.632V17.0807H17.082ZM17.082 2.91406V3.46406H2.91536V2.91406V2.36406H17.082V2.91406ZM6.9987 2.91406H7.5487V17.0807H6.9987H6.4487V2.91406H6.9987ZM17.082 17.0807L17.082 17.6307L6.9987 17.6307V17.0807V16.5307L17.082 16.5307L17.082 17.0807ZM6.9987 2.91406V2.36406H17.082V2.91406V3.46406H6.9987V2.91406Z" fill="currentColor"/>`,
|
"layout-right": `<path d="M2.91536 2.91406H2.36536V2.36406H2.91536V2.91406ZM2.91536 17.0807V17.6307H2.36536V17.0807H2.91536ZM17.082 17.0807H17.632V17.6307H17.082V17.0807ZM17.082 2.91406V2.36406H17.632V2.91406H17.082ZM6.9987 2.91406H6.4487V2.36406H6.9987V2.91406ZM6.9987 17.0807V17.6307H6.4487V17.0807H6.9987ZM2.91536 2.91406H3.46536V17.0807H2.91536H2.36536V2.91406H2.91536ZM2.91536 17.0807V16.5307H17.082V17.0807V17.6307H2.91536V17.0807ZM17.082 17.0807H16.532V2.91406H17.082H17.632V17.0807H17.082ZM17.082 2.91406V3.46406H2.91536V2.91406V2.36406H17.082V2.91406ZM6.9987 2.91406H7.5487V17.0807H6.9987H6.4487V2.91406H6.9987ZM17.082 17.0807L17.082 17.6307L6.9987 17.6307V17.0807V16.5307L17.082 16.5307L17.082 17.0807ZM6.9987 2.91406V2.36406H17.082V2.91406V3.46406H6.9987V2.91406Z" fill="currentColor"/>`,
|
||||||
"layout-right-partial": `<path d="M17.082 17.0807L6.9987 17.0807V2.91406H17.082V17.0807Z" fill="currentColor" fill-opacity="40%" /><path d="M2.91536 2.91406H2.36536V2.36406H2.91536V2.91406ZM2.91536 17.0807V17.6307H2.36536V17.0807H2.91536ZM17.082 17.0807H17.632V17.6307H17.082V17.0807ZM17.082 2.91406V2.36406H17.632V2.91406H17.082ZM6.9987 2.91406H6.4487V2.36406H6.9987V2.91406ZM6.9987 17.0807V17.6307H6.4487V17.0807H6.9987ZM2.91536 2.91406H3.46536V17.0807H2.91536H2.36536V2.91406H2.91536ZM2.91536 17.0807V16.5307H17.082V17.0807V17.6307H2.91536V17.0807ZM17.082 17.0807H16.532V2.91406H17.082H17.632V17.0807H17.082ZM17.082 2.91406V3.46406H2.91536V2.91406V2.36406H17.082V2.91406ZM6.9987 2.91406H7.5487V17.0807H6.9987H6.4487V2.91406H6.9987ZM17.082 17.0807L17.082 17.6307L6.9987 17.6307V17.0807V16.5307L17.082 16.5307L17.082 17.0807ZM6.9987 2.91406V2.36406H17.082V2.91406V3.46406H6.9987V2.91406Z" fill="currentColor" />`,
|
"layout-right-partial": `<path d="M17.082 17.0807L6.9987 17.0807V2.91406H17.082V17.0807Z" fill="currentColor" fill-opacity="16%" /><path d="M2.91536 2.91406H2.36536V2.36406H2.91536V2.91406ZM2.91536 17.0807V17.6307H2.36536V17.0807H2.91536ZM17.082 17.0807H17.632V17.6307H17.082V17.0807ZM17.082 2.91406V2.36406H17.632V2.91406H17.082ZM6.9987 2.91406H6.4487V2.36406H6.9987V2.91406ZM6.9987 17.0807V17.6307H6.4487V17.0807H6.9987ZM2.91536 2.91406H3.46536V17.0807H2.91536H2.36536V2.91406H2.91536ZM2.91536 17.0807V16.5307H17.082V17.0807V17.6307H2.91536V17.0807ZM17.082 17.0807H16.532V2.91406H17.082H17.632V17.0807H17.082ZM17.082 2.91406V3.46406H2.91536V2.91406V2.36406H17.082V2.91406ZM6.9987 2.91406H7.5487V17.0807H6.9987H6.4487V2.91406H6.9987ZM17.082 17.0807L17.082 17.6307L6.9987 17.6307V17.0807V16.5307L17.082 16.5307L17.082 17.0807ZM6.9987 2.91406V2.36406H17.082V2.91406V3.46406H6.9987V2.91406Z" fill="currentColor" />`,
|
||||||
"layout-right-full": `<path d="M17.082 17.0807L6.9987 17.0807V2.91406H17.082V17.0807Z" fill="currentColor" /><path d="M2.91536 2.91406H2.36536V2.36406H2.91536V2.91406ZM2.91536 17.0807V17.6307H2.36536V17.0807H2.91536ZM17.082 17.0807H17.632V17.6307H17.082V17.0807ZM17.082 2.91406V2.36406H17.632V2.91406H17.082ZM6.9987 2.91406H6.4487V2.36406H6.9987V2.91406ZM6.9987 17.0807V17.6307H6.4487V17.0807H6.9987ZM2.91536 2.91406H3.46536V17.0807H2.91536H2.36536V2.91406H2.91536ZM2.91536 17.0807V16.5307H17.082V17.0807V17.6307H2.91536V17.0807ZM17.082 17.0807H16.532V2.91406H17.082H17.632V17.0807H17.082ZM17.082 2.91406V3.46406H2.91536V2.91406V2.36406H17.082V2.91406ZM6.9987 2.91406H7.5487V17.0807H6.9987H6.4487V2.91406H6.9987ZM17.082 17.0807L17.082 17.6307L6.9987 17.6307V17.0807V16.5307L17.082 16.5307L17.082 17.0807ZM6.9987 2.91406V2.36406H17.082V2.91406V3.46406H6.9987V2.91406Z" fill="currentColor" />`,
|
"layout-right-full": `<path d="M17.082 17.0807L6.9987 17.0807V2.91406H17.082V17.0807Z" fill="currentColor" /><path d="M2.91536 2.91406H2.36536V2.36406H2.91536V2.91406ZM2.91536 17.0807V17.6307H2.36536V17.0807H2.91536ZM17.082 17.0807H17.632V17.6307H17.082V17.0807ZM17.082 2.91406V2.36406H17.632V2.91406H17.082ZM6.9987 2.91406H6.4487V2.36406H6.9987V2.91406ZM6.9987 17.0807V17.6307H6.4487V17.0807H6.9987ZM2.91536 2.91406H3.46536V17.0807H2.91536H2.36536V2.91406H2.91536ZM2.91536 17.0807V16.5307H17.082V17.0807V17.6307H2.91536V17.0807ZM17.082 17.0807H16.532V2.91406H17.082H17.632V17.0807H17.082ZM17.082 2.91406V3.46406H2.91536V2.91406V2.36406H17.082V2.91406ZM6.9987 2.91406H7.5487V17.0807H6.9987H6.4487V2.91406H6.9987ZM17.082 17.0807L17.082 17.6307L6.9987 17.6307V17.0807V16.5307L17.082 16.5307L17.082 17.0807ZM6.9987 2.91406V2.36406H17.082V2.91406V3.46406H6.9987V2.91406Z" fill="currentColor" />`,
|
||||||
"square-arrow-top-right": `<path d="M7.91675 2.9165H2.91675V17.0832H17.0834V12.0832M12.0834 2.9165H17.0834V7.9165M9.58342 10.4165L16.6667 3.33317" stroke="currentColor" stroke-linecap="square"/>`,
|
"square-arrow-top-right": `<path d="M7.91675 2.9165H2.91675V17.0832H17.0834V12.0832M12.0834 2.9165H17.0834V7.9165M9.58342 10.4165L16.6667 3.33317" stroke="currentColor" stroke-linecap="square"/>`,
|
||||||
"speech-bubble": `<path d="M18.3334 10.0003C18.3334 5.57324 15.0927 2.91699 10.0001 2.91699C4.90749 2.91699 1.66675 5.57324 1.66675 10.0003C1.66675 11.1497 2.45578 13.1016 2.5771 13.3949C2.5878 13.4207 2.59839 13.4444 2.60802 13.4706C2.69194 13.6996 3.04282 14.9364 1.66675 16.7684C3.5186 17.6538 5.48526 16.1982 5.48526 16.1982C6.84592 16.9202 8.46491 17.0837 10.0001 17.0837C15.0927 17.0837 18.3334 14.4274 18.3334 10.0003Z" stroke="currentColor" stroke-linecap="square"/>`,
|
"speech-bubble": `<path d="M18.3334 10.0003C18.3334 5.57324 15.0927 2.91699 10.0001 2.91699C4.90749 2.91699 1.66675 5.57324 1.66675 10.0003C1.66675 11.1497 2.45578 13.1016 2.5771 13.3949C2.5878 13.4207 2.59839 13.4444 2.60802 13.4706C2.69194 13.6996 3.04282 14.9364 1.66675 16.7684C3.5186 17.6538 5.48526 16.1982 5.48526 16.1982C6.84592 16.9202 8.46491 17.0837 10.0001 17.0837C15.0927 17.0837 18.3334 14.4274 18.3334 10.0003Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||||
@@ -56,7 +56,7 @@ const icons = {
|
|||||||
github: `<path d="M10.0001 1.62549C14.6042 1.62549 18.3334 5.35465 18.3334 9.95882C18.333 11.7049 17.785 13.4068 16.7666 14.8251C15.7482 16.2434 14.3107 17.3066 12.6563 17.8651C12.2397 17.9484 12.0834 17.688 12.0834 17.4692C12.0834 17.188 12.0938 16.2922 12.0938 15.1776C12.0938 14.3963 11.8334 13.8963 11.5313 13.6359C13.3855 13.4276 15.3334 12.7192 15.3334 9.52132C15.3334 8.60465 15.0105 7.86507 14.4792 7.28174C14.5626 7.0734 14.8542 6.21924 14.3959 5.0734C14.3959 5.0734 13.698 4.84424 12.1042 5.92757C11.4376 5.74007 10.7292 5.64632 10.0209 5.64632C9.31258 5.64632 8.60425 5.74007 7.93758 5.92757C6.34383 4.85465 5.64592 5.0734 5.64592 5.0734C5.18758 6.21924 5.47925 7.0734 5.56258 7.28174C5.03133 7.86507 4.70842 8.61507 4.70842 9.52132C4.70842 12.7088 6.64592 13.4276 8.50008 13.6359C8.2605 13.8442 8.04175 14.2088 7.96883 14.7505C7.48967 14.9692 6.29175 15.3234 5.54175 14.063C5.3855 13.813 4.91675 13.1984 4.2605 13.2088C3.56258 13.2192 3.97925 13.6047 4.27092 13.7609C4.62508 13.9588 5.03133 14.6984 5.12508 14.938C5.29175 15.4067 5.83342 16.3026 7.92717 15.9172C7.92717 16.6151 7.93758 17.2713 7.93758 17.4692C7.93758 17.688 7.78133 17.938 7.36467 17.8651C5.70491 17.3126 4.26126 16.2515 3.23851 14.8324C2.21576 13.4133 1.66583 11.7081 1.66675 9.95882C1.66675 5.35465 5.39592 1.62549 10.0001 1.62549Z" fill="currentColor"/>`,
|
github: `<path d="M10.0001 1.62549C14.6042 1.62549 18.3334 5.35465 18.3334 9.95882C18.333 11.7049 17.785 13.4068 16.7666 14.8251C15.7482 16.2434 14.3107 17.3066 12.6563 17.8651C12.2397 17.9484 12.0834 17.688 12.0834 17.4692C12.0834 17.188 12.0938 16.2922 12.0938 15.1776C12.0938 14.3963 11.8334 13.8963 11.5313 13.6359C13.3855 13.4276 15.3334 12.7192 15.3334 9.52132C15.3334 8.60465 15.0105 7.86507 14.4792 7.28174C14.5626 7.0734 14.8542 6.21924 14.3959 5.0734C14.3959 5.0734 13.698 4.84424 12.1042 5.92757C11.4376 5.74007 10.7292 5.64632 10.0209 5.64632C9.31258 5.64632 8.60425 5.74007 7.93758 5.92757C6.34383 4.85465 5.64592 5.0734 5.64592 5.0734C5.18758 6.21924 5.47925 7.0734 5.56258 7.28174C5.03133 7.86507 4.70842 8.61507 4.70842 9.52132C4.70842 12.7088 6.64592 13.4276 8.50008 13.6359C8.2605 13.8442 8.04175 14.2088 7.96883 14.7505C7.48967 14.9692 6.29175 15.3234 5.54175 14.063C5.3855 13.813 4.91675 13.1984 4.2605 13.2088C3.56258 13.2192 3.97925 13.6047 4.27092 13.7609C4.62508 13.9588 5.03133 14.6984 5.12508 14.938C5.29175 15.4067 5.83342 16.3026 7.92717 15.9172C7.92717 16.6151 7.93758 17.2713 7.93758 17.4692C7.93758 17.688 7.78133 17.938 7.36467 17.8651C5.70491 17.3126 4.26126 16.2515 3.23851 14.8324C2.21576 13.4133 1.66583 11.7081 1.66675 9.95882C1.66675 5.35465 5.39592 1.62549 10.0001 1.62549Z" fill="currentColor"/>`,
|
||||||
discord: `<path d="M16.0742 4.45014C14.9244 3.92097 13.7106 3.54556 12.4638 3.3335C12.2932 3.64011 12.1388 3.95557 12.0013 4.27856C10.6732 4.07738 9.32261 4.07738 7.99451 4.27856C7.85694 3.9556 7.70257 3.64014 7.53203 3.3335C6.28441 3.54735 5.06981 3.92365 3.91889 4.45291C1.63401 7.85128 1.01462 11.1652 1.32431 14.4322C2.6624 15.426 4.16009 16.1819 5.7523 16.6668C6.11082 16.1821 6.42806 15.6678 6.70066 15.1295C6.18289 14.9351 5.68315 14.6953 5.20723 14.4128C5.33249 14.3215 5.45499 14.2274 5.57336 14.136C6.95819 14.7907 8.46965 15.1302 9.99997 15.1302C11.5303 15.1302 13.0418 14.7907 14.4266 14.136C14.5463 14.2343 14.6688 14.3284 14.7927 14.4128C14.3159 14.6957 13.8152 14.9361 13.2965 15.1309C13.5688 15.669 13.8861 16.1828 14.2449 16.6668C15.8385 16.1838 17.3373 15.4283 18.6756 14.4335C19.039 10.645 18.0549 7.36145 16.0742 4.45014ZM7.09294 12.423C6.22992 12.423 5.51693 11.6357 5.51693 10.6671C5.51693 9.69852 6.20514 8.90427 7.09019 8.90427C7.97524 8.90427 8.68272 9.69852 8.66758 10.6671C8.65244 11.6357 7.97248 12.423 7.09294 12.423ZM12.907 12.423C12.0426 12.423 11.3324 11.6357 11.3324 10.6671C11.3324 9.69852 12.0206 8.90427 12.907 8.90427C13.7934 8.90427 14.4954 9.69852 14.4803 10.6671C14.4651 11.6357 13.7865 12.423 12.907 12.423Z" fill="currentColor"/>`,
|
discord: `<path d="M16.0742 4.45014C14.9244 3.92097 13.7106 3.54556 12.4638 3.3335C12.2932 3.64011 12.1388 3.95557 12.0013 4.27856C10.6732 4.07738 9.32261 4.07738 7.99451 4.27856C7.85694 3.9556 7.70257 3.64014 7.53203 3.3335C6.28441 3.54735 5.06981 3.92365 3.91889 4.45291C1.63401 7.85128 1.01462 11.1652 1.32431 14.4322C2.6624 15.426 4.16009 16.1819 5.7523 16.6668C6.11082 16.1821 6.42806 15.6678 6.70066 15.1295C6.18289 14.9351 5.68315 14.6953 5.20723 14.4128C5.33249 14.3215 5.45499 14.2274 5.57336 14.136C6.95819 14.7907 8.46965 15.1302 9.99997 15.1302C11.5303 15.1302 13.0418 14.7907 14.4266 14.136C14.5463 14.2343 14.6688 14.3284 14.7927 14.4128C14.3159 14.6957 13.8152 14.9361 13.2965 15.1309C13.5688 15.669 13.8861 16.1828 14.2449 16.6668C15.8385 16.1838 17.3373 15.4283 18.6756 14.4335C19.039 10.645 18.0549 7.36145 16.0742 4.45014ZM7.09294 12.423C6.22992 12.423 5.51693 11.6357 5.51693 10.6671C5.51693 9.69852 6.20514 8.90427 7.09019 8.90427C7.97524 8.90427 8.68272 9.69852 8.66758 10.6671C8.65244 11.6357 7.97248 12.423 7.09294 12.423ZM12.907 12.423C12.0426 12.423 11.3324 11.6357 11.3324 10.6671C11.3324 9.69852 12.0206 8.90427 12.907 8.90427C13.7934 8.90427 14.4954 9.69852 14.4803 10.6671C14.4651 11.6357 13.7865 12.423 12.907 12.423Z" fill="currentColor"/>`,
|
||||||
"layout-bottom": `<path d="M2.91699 17.0832L2.41699 17.0832L2.41699 17.5832L2.91699 17.5832L2.91699 17.0832ZM2.91699 2.91653L2.91699 2.41653L2.41699 2.41653L2.41699 2.91653L2.91699 2.91653ZM17.0837 2.91653L17.5837 2.91653L17.5837 2.41653L17.0837 2.41653L17.0837 2.91653ZM17.0837 17.0832L17.5837 17.0832L17.5837 17.5832L17.0837 17.5832L17.0837 17.0832ZM17.0837 12.5827L17.5837 12.5827L17.5837 11.5827L17.0837 11.5827L17.0837 12.0827L17.0837 12.5827ZM2.91699 11.5827L2.41699 11.5827L2.41699 12.5827L2.91699 12.5827L2.91699 12.0827L2.91699 11.5827ZM2.91699 17.0832L3.41699 17.0832L3.41699 2.91653L2.91699 2.91653L2.41699 2.91653L2.41699 17.0832L2.91699 17.0832ZM2.91699 2.91653L2.91699 3.41653L17.0837 3.41653L17.0837 2.91653L17.0837 2.41653L2.91699 2.41653L2.91699 2.91653ZM17.0837 2.91653L16.5837 2.91653L16.5837 17.0832L17.0837 17.0832L17.5837 17.0832L17.5837 2.91653L17.0837 2.91653ZM17.0837 17.0832L17.0837 16.5832L2.91699 16.5832L2.91699 17.0832L2.91699 17.5832L17.0837 17.5832L17.0837 17.0832ZM17.0837 12.0827L17.0837 11.5827L2.91699 11.5827L2.91699 12.0827L2.91699 12.5827L17.0837 12.5827L17.0837 12.0827Z" fill="currentColor"/>`,
|
"layout-bottom": `<path d="M2.91699 17.0832L2.41699 17.0832L2.41699 17.5832L2.91699 17.5832L2.91699 17.0832ZM2.91699 2.91653L2.91699 2.41653L2.41699 2.41653L2.41699 2.91653L2.91699 2.91653ZM17.0837 2.91653L17.5837 2.91653L17.5837 2.41653L17.0837 2.41653L17.0837 2.91653ZM17.0837 17.0832L17.5837 17.0832L17.5837 17.5832L17.0837 17.5832L17.0837 17.0832ZM17.0837 12.5827L17.5837 12.5827L17.5837 11.5827L17.0837 11.5827L17.0837 12.0827L17.0837 12.5827ZM2.91699 11.5827L2.41699 11.5827L2.41699 12.5827L2.91699 12.5827L2.91699 12.0827L2.91699 11.5827ZM2.91699 17.0832L3.41699 17.0832L3.41699 2.91653L2.91699 2.91653L2.41699 2.91653L2.41699 17.0832L2.91699 17.0832ZM2.91699 2.91653L2.91699 3.41653L17.0837 3.41653L17.0837 2.91653L17.0837 2.41653L2.91699 2.41653L2.91699 2.91653ZM17.0837 2.91653L16.5837 2.91653L16.5837 17.0832L17.0837 17.0832L17.5837 17.0832L17.5837 2.91653L17.0837 2.91653ZM17.0837 17.0832L17.0837 16.5832L2.91699 16.5832L2.91699 17.0832L2.91699 17.5832L17.0837 17.5832L17.0837 17.0832ZM17.0837 12.0827L17.0837 11.5827L2.91699 11.5827L2.91699 12.0827L2.91699 12.5827L17.0837 12.5827L17.0837 12.0827Z" fill="currentColor"/>`,
|
||||||
"layout-bottom-partial": `<path d="M2.91732 12.0827L17.084 12.0827L17.084 17.0827H2.91732L2.91732 12.0827Z" fill="currentColor" fill-opacity="40%" /><path d="M2.91732 2.91602L17.084 2.91602M2.91732 2.91602L2.91732 17.0827M17.084 2.91602L17.084 17.0827M17.084 17.0827L2.91732 17.0827M2.91732 12.0827L17.084 12.0827" stroke="currentColor" stroke-linecap="square"/>`,
|
"layout-bottom-partial": `<path d="M2.91732 12.0827L17.084 12.0827L17.084 17.0827H2.91732L2.91732 12.0827Z" fill="currentColor" fill-opacity="16%" /><path d="M2.91732 2.91602L17.084 2.91602M2.91732 2.91602L2.91732 17.0827M17.084 2.91602L17.084 17.0827M17.084 17.0827L2.91732 17.0827M2.91732 12.0827L17.084 12.0827" stroke="currentColor" stroke-linecap="square"/>`,
|
||||||
"layout-bottom-full": `<path d="M2.91732 12.0827L17.084 12.0827L17.084 17.0827H2.91732L2.91732 12.0827Z" fill="currentColor"/><path d="M2.91732 2.91602L17.084 2.91602M2.91732 2.91602L2.91732 17.0827M17.084 2.91602L17.084 17.0827M17.084 17.0827L2.91732 17.0827M2.91732 12.0827L17.084 12.0827" stroke="currentColor" stroke-linecap="square"/>`,
|
"layout-bottom-full": `<path d="M2.91732 12.0827L17.084 12.0827L17.084 17.0827H2.91732L2.91732 12.0827Z" fill="currentColor"/><path d="M2.91732 2.91602L17.084 2.91602M2.91732 2.91602L2.91732 17.0827M17.084 2.91602L17.084 17.0827M17.084 17.0827L2.91732 17.0827M2.91732 12.0827L17.084 12.0827" stroke="currentColor" stroke-linecap="square"/>`,
|
||||||
"dot-grid": `<path d="M2.08398 9.16602H3.75065V10.8327H2.08398V9.16602Z" fill="currentColor"/><path d="M10.834 9.16602H9.16732V10.8327H10.834V9.16602Z" fill="currentColor"/><path d="M16.2507 9.16602H17.9173V10.8327H16.2507V9.16602Z" fill="currentColor"/><path d="M2.08398 9.16602H3.75065V10.8327H2.08398V9.16602Z" stroke="currentColor"/><path d="M10.834 9.16602H9.16732V10.8327H10.834V9.16602Z" stroke="currentColor"/><path d="M16.2507 9.16602H17.9173V10.8327H16.2507V9.16602Z" stroke="currentColor"/>`,
|
"dot-grid": `<path d="M2.08398 9.16602H3.75065V10.8327H2.08398V9.16602Z" fill="currentColor"/><path d="M10.834 9.16602H9.16732V10.8327H10.834V9.16602Z" fill="currentColor"/><path d="M16.2507 9.16602H17.9173V10.8327H16.2507V9.16602Z" fill="currentColor"/><path d="M2.08398 9.16602H3.75065V10.8327H2.08398V9.16602Z" stroke="currentColor"/><path d="M10.834 9.16602H9.16732V10.8327H10.834V9.16602Z" stroke="currentColor"/><path d="M16.2507 9.16602H17.9173V10.8327H16.2507V9.16602Z" stroke="currentColor"/>`,
|
||||||
"circle-check": `<path d="M12.4987 7.91732L8.7487 12.5007L7.08203 10.834M17.9154 10.0007C17.9154 14.3729 14.371 17.9173 9.9987 17.9173C5.62644 17.9173 2.08203 14.3729 2.08203 10.0007C2.08203 5.6284 5.62644 2.08398 9.9987 2.08398C14.371 2.08398 17.9154 5.6284 17.9154 10.0007Z" stroke="currentColor" stroke-linecap="square"/>`,
|
"circle-check": `<path d="M12.4987 7.91732L8.7487 12.5007L7.08203 10.834M17.9154 10.0007C17.9154 14.3729 14.371 17.9173 9.9987 17.9173C5.62644 17.9173 2.08203 14.3729 2.08203 10.0007C2.08203 5.6284 5.62644 2.08398 9.9987 2.08398C14.371 2.08398 17.9154 5.6284 17.9154 10.0007Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||||
@@ -78,6 +78,7 @@ const icons = {
|
|||||||
keyboard: `<path d="M5.125 7.375V4.375H14.875V2.875M8.3125 13.9375H11.6875M8.125 13.9375H11.875M2.125 7.375H17.875V17.125H2.125V7.375ZM5.5 10.375H5.125V10.75H5.5V10.375ZM8.5 10.375H8.125V10.75H8.5V10.375ZM11.875 10.375H11.5V10.75H11.875V10.375ZM14.875 10.375H14.5V10.75H14.875V10.375ZM14.875 13.75H14.5V14.125H14.875V13.75ZM5.5 13.75H5.125V14.125H5.5V13.75Z" stroke="currentColor" stroke-linecap="square"/>`,
|
keyboard: `<path d="M5.125 7.375V4.375H14.875V2.875M8.3125 13.9375H11.6875M8.125 13.9375H11.875M2.125 7.375H17.875V17.125H2.125V7.375ZM5.5 10.375H5.125V10.75H5.5V10.375ZM8.5 10.375H8.125V10.75H8.5V10.375ZM11.875 10.375H11.5V10.75H11.875V10.375ZM14.875 10.375H14.5V10.75H14.875V10.375ZM14.875 13.75H14.5V14.125H14.875V13.75ZM5.5 13.75H5.125V14.125H5.5V13.75Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||||
selector: `<path d="M6.66626 12.5033L9.99959 15.8366L13.3329 12.5033M6.66626 7.50326L9.99959 4.16992L13.3329 7.50326" stroke="currentColor" stroke-linecap="square"/>`,
|
selector: `<path d="M6.66626 12.5033L9.99959 15.8366L13.3329 12.5033M6.66626 7.50326L9.99959 4.16992L13.3329 7.50326" stroke="currentColor" stroke-linecap="square"/>`,
|
||||||
"arrow-down-to-line": `<path d="M15.2083 11.6667L10 16.875L4.79167 11.6667M10 16.25V3.125" stroke="currentColor" stroke-width="1.25" stroke-linecap="square"/>`,
|
"arrow-down-to-line": `<path d="M15.2083 11.6667L10 16.875L4.79167 11.6667M10 16.25V3.125" stroke="currentColor" stroke-width="1.25" stroke-linecap="square"/>`,
|
||||||
|
warning: `<path d="M10 7.91667V11.6667M10 13.7417V13.75M10 2.5L1.875 16.25H18.125L10 2.5Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||||
link: `<path d="M2.08334 12.0833L1.72979 11.7298L1.37624 12.0833L1.72979 12.4369L2.08334 12.0833ZM7.91668 17.9167L7.56312 18.2702L7.91668 18.6238L8.27023 18.2702L7.91668 17.9167ZM17.9167 7.91666L18.2702 8.27022L18.6238 7.91666L18.2702 7.56311L17.9167 7.91666ZM12.0833 2.08333L12.4369 1.72977L12.0833 1.37622L11.7298 1.72977L12.0833 2.08333ZM8.39646 5.06311L8.0429 5.41666L8.75001 6.12377L9.10356 5.77021L8.75001 5.41666L8.39646 5.06311ZM5.77023 9.10355L6.12378 8.74999L5.41668 8.04289L5.06312 8.39644L5.41668 8.74999L5.77023 9.10355ZM14.2298 10.8964L13.8762 11.25L14.5833 11.9571L14.9369 11.6035L14.5833 11.25L14.2298 10.8964ZM11.6036 14.9369L11.9571 14.5833L11.25 13.8762L10.8965 14.2298L11.25 14.5833L11.6036 14.9369ZM7.14646 12.1464L6.7929 12.5L7.50001 13.2071L7.85356 12.8535L7.50001 12.5L7.14646 12.1464ZM12.8536 7.85355L13.2071 7.49999L12.5 6.79289L12.1465 7.14644L12.5 7.49999L12.8536 7.85355ZM2.08334 12.0833L1.72979 12.4369L7.56312 18.2702L7.91668 17.9167L8.27023 17.5631L2.4369 11.7298L2.08334 12.0833ZM17.9167 7.91666L18.2702 7.56311L12.4369 1.72977L12.0833 2.08333L11.7298 2.43688L17.5631 8.27022L17.9167 7.91666ZM12.0833 2.08333L11.7298 1.72977L8.39646 5.06311L8.75001 5.41666L9.10356 5.77021L12.4369 2.43688L12.0833 2.08333ZM5.41668 8.74999L5.06312 8.39644L1.72979 11.7298L2.08334 12.0833L2.4369 12.4369L5.77023 9.10355L5.41668 8.74999ZM14.5833 11.25L14.9369 11.6035L18.2702 8.27022L17.9167 7.91666L17.5631 7.56311L14.2298 10.8964L14.5833 11.25ZM7.91668 17.9167L8.27023 18.2702L11.6036 14.9369L11.25 14.5833L10.8965 14.2298L7.56312 17.5631L7.91668 17.9167ZM7.50001 12.5L7.85356 12.8535L12.8536 7.85355L12.5 7.49999L12.1465 7.14644L7.14646 12.1464L7.50001 12.5Z" fill="currentColor"/>`,
|
link: `<path d="M2.08334 12.0833L1.72979 11.7298L1.37624 12.0833L1.72979 12.4369L2.08334 12.0833ZM7.91668 17.9167L7.56312 18.2702L7.91668 18.6238L8.27023 18.2702L7.91668 17.9167ZM17.9167 7.91666L18.2702 8.27022L18.6238 7.91666L18.2702 7.56311L17.9167 7.91666ZM12.0833 2.08333L12.4369 1.72977L12.0833 1.37622L11.7298 1.72977L12.0833 2.08333ZM8.39646 5.06311L8.0429 5.41666L8.75001 6.12377L9.10356 5.77021L8.75001 5.41666L8.39646 5.06311ZM5.77023 9.10355L6.12378 8.74999L5.41668 8.04289L5.06312 8.39644L5.41668 8.74999L5.77023 9.10355ZM14.2298 10.8964L13.8762 11.25L14.5833 11.9571L14.9369 11.6035L14.5833 11.25L14.2298 10.8964ZM11.6036 14.9369L11.9571 14.5833L11.25 13.8762L10.8965 14.2298L11.25 14.5833L11.6036 14.9369ZM7.14646 12.1464L6.7929 12.5L7.50001 13.2071L7.85356 12.8535L7.50001 12.5L7.14646 12.1464ZM12.8536 7.85355L13.2071 7.49999L12.5 6.79289L12.1465 7.14644L12.5 7.49999L12.8536 7.85355ZM2.08334 12.0833L1.72979 12.4369L7.56312 18.2702L7.91668 17.9167L8.27023 17.5631L2.4369 11.7298L2.08334 12.0833ZM17.9167 7.91666L18.2702 7.56311L12.4369 1.72977L12.0833 2.08333L11.7298 2.43688L17.5631 8.27022L17.9167 7.91666ZM12.0833 2.08333L11.7298 1.72977L8.39646 5.06311L8.75001 5.41666L9.10356 5.77021L12.4369 2.43688L12.0833 2.08333ZM5.41668 8.74999L5.06312 8.39644L1.72979 11.7298L2.08334 12.0833L2.4369 12.4369L5.77023 9.10355L5.41668 8.74999ZM14.5833 11.25L14.9369 11.6035L18.2702 8.27022L17.9167 7.91666L17.5631 7.56311L14.2298 10.8964L14.5833 11.25ZM7.91668 17.9167L8.27023 18.2702L11.6036 14.9369L11.25 14.5833L10.8965 14.2298L7.56312 17.5631L7.91668 17.9167ZM7.50001 12.5L7.85356 12.8535L12.8536 7.85355L12.5 7.49999L12.1465 7.14644L7.14646 12.1464L7.50001 12.5Z" fill="currentColor"/>`,
|
||||||
providers: `<path d="M10.0001 4.37562V2.875M13 4.37793V2.87793M7.00014 4.37793V2.875M10 17.1279V15.6279M13 17.1279V15.6279M7 17.1279V15.6279M15.625 13.0029H17.125M15.625 7.00293H17.125M15.625 10.0029H17.125M2.875 10.0029H4.375M2.875 13.0029H4.375M2.875 7.00293H4.375M4.375 4.37793H15.625V15.6279H4.375V4.37793ZM12.6241 10.0022C12.6241 11.4519 11.4488 12.6272 9.99908 12.6272C8.54934 12.6272 7.37408 11.4519 7.37408 10.0022C7.37408 8.55245 8.54934 7.3772 9.99908 7.3772C11.4488 7.3772 12.6241 8.55245 12.6241 10.0022Z" stroke="currentColor" stroke-linecap="square"/>`,
|
providers: `<path d="M10.0001 4.37562V2.875M13 4.37793V2.87793M7.00014 4.37793V2.875M10 17.1279V15.6279M13 17.1279V15.6279M7 17.1279V15.6279M15.625 13.0029H17.125M15.625 7.00293H17.125M15.625 10.0029H17.125M2.875 10.0029H4.375M2.875 13.0029H4.375M2.875 7.00293H4.375M4.375 4.37793H15.625V15.6279H4.375V4.37793ZM12.6241 10.0022C12.6241 11.4519 11.4488 12.6272 9.99908 12.6272C8.54934 12.6272 7.37408 11.4519 7.37408 10.0022C7.37408 8.55245 8.54934 7.3772 9.99908 7.3772C11.4488 7.3772 12.6241 8.55245 12.6241 10.0022Z" stroke="currentColor" stroke-linecap="square"/>`,
|
||||||
models: `<path fill-rule="evenodd" clip-rule="evenodd" d="M17.5 10C12.2917 10 10 12.2917 10 17.5C10 12.2917 7.70833 10 2.5 10C7.70833 10 10 7.70833 10 2.5C10 7.70833 12.2917 10 17.5 10Z" stroke="currentColor"/>`,
|
models: `<path fill-rule="evenodd" clip-rule="evenodd" d="M17.5 10C12.2917 10 10 12.2917 10 17.5C10 12.2917 7.70833 10 2.5 10C7.70833 10 10 7.70833 10 2.5C10 7.70833 12.2917 10 17.5 10Z" stroke="currentColor"/>`,
|
||||||
|
|||||||
@@ -745,10 +745,139 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
[data-component="button"] {
|
||||||
|
padding-left: 12px;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-component="question-prompt"] {
|
[data-component="dock-prompt"][data-kind="permission"] {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: 100dvh;
|
||||||
|
|
||||||
|
[data-slot="permission-body"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 12px 12px 0;
|
||||||
|
background-color: var(--surface-raised-stronger-non-alpha);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: var(--shadow-xs-border);
|
||||||
|
overflow: clip;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="permission-header"] {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="permission-row"] {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 20px 1fr;
|
||||||
|
column-gap: 8px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="permission-row"][data-variant="header"] {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="permission-icon"] {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="permission-icon"] [data-component="icon"] {
|
||||||
|
color: var(--icon-warning-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="permission-header-title"] {
|
||||||
|
font-family: var(--font-family-sans);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
line-height: var(--line-height-large);
|
||||||
|
color: var(--text-strong);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="permission-content"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="permission-hint"] {
|
||||||
|
font-family: var(--font-family-sans);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
line-height: var(--line-height-large);
|
||||||
|
color: var(--text-weak);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="permission-patterns"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 1px 0 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: var(--line-height-large);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="permission-footer"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 32px 8px 8px;
|
||||||
|
background-color: var(--background-base);
|
||||||
|
border: 1px solid var(--border-weak-base);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: clip;
|
||||||
|
margin-top: -24px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="permission-footer-actions"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
[data-component="button"] {
|
||||||
|
padding-left: 12px;
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:is([data-component="question-prompt"], [data-component="dock-prompt"][data-kind="question"]) {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ function createThrottledValue(getValue: () => string) {
|
|||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const next = getValue()
|
const next = getValue()
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
|
|
||||||
const remaining = TEXT_RENDER_THROTTLE_MS - (now - last)
|
const remaining = TEXT_RENDER_THROTTLE_MS - (now - last)
|
||||||
if (remaining <= 0) {
|
if (remaining <= 0) {
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
@@ -250,6 +251,126 @@ export function getToolInfo(tool: string, input: any = {}): ToolInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CONTEXT_GROUP_TOOLS = new Set(["read", "glob", "grep", "list"])
|
const CONTEXT_GROUP_TOOLS = new Set(["read", "glob", "grep", "list"])
|
||||||
|
const HIDDEN_TOOLS = new Set(["todowrite", "todoread"])
|
||||||
|
|
||||||
|
function list<T>(value: T[] | undefined | null, fallback: T[]) {
|
||||||
|
if (Array.isArray(value)) return value
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderable(part: PartType) {
|
||||||
|
if (part.type === "tool") {
|
||||||
|
if (HIDDEN_TOOLS.has(part.tool)) return false
|
||||||
|
if (part.tool === "question") return part.state.status !== "pending" && part.state.status !== "running"
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (part.type === "text") return !!part.text?.trim()
|
||||||
|
if (part.type === "reasoning") return !!part.text?.trim()
|
||||||
|
return !!PART_MAPPING[part.type]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AssistantParts(props: {
|
||||||
|
messages: AssistantMessage[]
|
||||||
|
showAssistantCopyPartID?: string | null
|
||||||
|
working?: boolean
|
||||||
|
}) {
|
||||||
|
const data = useData()
|
||||||
|
const emptyParts: PartType[] = []
|
||||||
|
|
||||||
|
const grouped = createMemo(() => {
|
||||||
|
const keys: string[] = []
|
||||||
|
const items: Record<
|
||||||
|
string,
|
||||||
|
{ type: "part"; part: PartType; message: AssistantMessage } | { type: "context"; parts: ToolPart[] }
|
||||||
|
> = {}
|
||||||
|
const push = (
|
||||||
|
key: string,
|
||||||
|
item: { type: "part"; part: PartType; message: AssistantMessage } | { type: "context"; parts: ToolPart[] },
|
||||||
|
) => {
|
||||||
|
keys.push(key)
|
||||||
|
items[key] = item
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = props.messages.flatMap((message) =>
|
||||||
|
list(data.store.part?.[message.id], emptyParts)
|
||||||
|
.filter(renderable)
|
||||||
|
.map((part) => ({ message, part })),
|
||||||
|
)
|
||||||
|
|
||||||
|
let start = -1
|
||||||
|
|
||||||
|
const flush = (end: number) => {
|
||||||
|
if (start < 0) return
|
||||||
|
const first = parts[start]
|
||||||
|
const last = parts[end]
|
||||||
|
if (!first || !last) {
|
||||||
|
start = -1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
push(`context:${first.part.id}`, {
|
||||||
|
type: "context",
|
||||||
|
parts: parts
|
||||||
|
.slice(start, end + 1)
|
||||||
|
.map((x) => x.part)
|
||||||
|
.filter((part): part is ToolPart => isContextGroupTool(part)),
|
||||||
|
})
|
||||||
|
start = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.forEach((item, index) => {
|
||||||
|
if (isContextGroupTool(item.part)) {
|
||||||
|
if (start < 0) start = index
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
flush(index - 1)
|
||||||
|
push(`part:${item.message.id}:${item.part.id}`, { type: "part", part: item.part, message: item.message })
|
||||||
|
})
|
||||||
|
|
||||||
|
flush(parts.length - 1)
|
||||||
|
|
||||||
|
return { keys, items }
|
||||||
|
})
|
||||||
|
|
||||||
|
const last = createMemo(() => grouped().keys.at(-1))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<For each={grouped().keys}>
|
||||||
|
{(key) => {
|
||||||
|
const item = createMemo(() => grouped().items[key])
|
||||||
|
const ctx = createMemo(() => {
|
||||||
|
const value = item()
|
||||||
|
if (!value) return
|
||||||
|
if (value.type !== "context") return
|
||||||
|
return value
|
||||||
|
})
|
||||||
|
const part = createMemo(() => {
|
||||||
|
const value = item()
|
||||||
|
if (!value) return
|
||||||
|
if (value.type !== "part") return
|
||||||
|
return value
|
||||||
|
})
|
||||||
|
const tail = createMemo(() => last() === key)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Show when={ctx()}>
|
||||||
|
{(entry) => <ContextToolGroup parts={entry().parts} busy={props.working && tail()} />}
|
||||||
|
</Show>
|
||||||
|
<Show when={part()}>
|
||||||
|
{(entry) => (
|
||||||
|
<Part
|
||||||
|
part={entry().part}
|
||||||
|
message={entry().message}
|
||||||
|
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function isContextGroupTool(part: PartType): part is ToolPart {
|
function isContextGroupTool(part: PartType): part is ToolPart {
|
||||||
return part.type === "tool" && CONTEXT_GROUP_TOOLS.has(part.tool)
|
return part.type === "tool" && CONTEXT_GROUP_TOOLS.has(part.tool)
|
||||||
@@ -390,6 +511,8 @@ export function AssistantMessageDisplay(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parts.forEach((part, index) => {
|
parts.forEach((part, index) => {
|
||||||
|
if (!renderable(part)) return
|
||||||
|
|
||||||
if (isContextGroupTool(part)) {
|
if (isContextGroupTool(part)) {
|
||||||
if (start < 0) start = index
|
if (start < 0) start = index
|
||||||
return
|
return
|
||||||
@@ -408,31 +531,43 @@ export function AssistantMessageDisplay(props: {
|
|||||||
<For each={grouped().keys}>
|
<For each={grouped().keys}>
|
||||||
{(key) => {
|
{(key) => {
|
||||||
const item = createMemo(() => grouped().items[key])
|
const item = createMemo(() => grouped().items[key])
|
||||||
|
const ctx = createMemo(() => {
|
||||||
|
const value = item()
|
||||||
|
if (!value) return
|
||||||
|
if (value.type !== "context") return
|
||||||
|
return value
|
||||||
|
})
|
||||||
|
const part = createMemo(() => {
|
||||||
|
const value = item()
|
||||||
|
if (!value) return
|
||||||
|
if (value.type !== "part") return
|
||||||
|
return value
|
||||||
|
})
|
||||||
return (
|
return (
|
||||||
<Show when={item()}>
|
<>
|
||||||
{(value) => {
|
<Show when={ctx()}>{(entry) => <ContextToolGroup parts={entry().parts} />}</Show>
|
||||||
const entry = value()
|
<Show when={part()}>
|
||||||
if (entry.type === "context") return <ContextToolGroup parts={entry.parts} />
|
{(entry) => (
|
||||||
return (
|
|
||||||
<Part
|
<Part
|
||||||
part={entry.part}
|
part={entry().part}
|
||||||
message={props.message}
|
message={props.message}
|
||||||
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
||||||
/>
|
/>
|
||||||
)
|
)}
|
||||||
}}
|
</Show>
|
||||||
</Show>
|
</>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextToolGroup(props: { parts: ToolPart[] }) {
|
function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) {
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const [open, setOpen] = createSignal(false)
|
const [open, setOpen] = createSignal(false)
|
||||||
const pending = createMemo(() =>
|
const pending = createMemo(
|
||||||
props.parts.some((part) => part.state.status === "pending" || part.state.status === "running"),
|
() =>
|
||||||
|
!!props.busy || props.parts.some((part) => part.state.status === "pending" || part.state.status === "running"),
|
||||||
)
|
)
|
||||||
const summary = createMemo(() => contextToolSummary(props.parts))
|
const summary = createMemo(() => contextToolSummary(props.parts))
|
||||||
const details = createMemo(() => summary().join(", "))
|
const details = createMemo(() => summary().join(", "))
|
||||||
@@ -445,7 +580,7 @@ function ContextToolGroup(props: { parts: ToolPart[] }) {
|
|||||||
when={pending()}
|
when={pending()}
|
||||||
fallback={
|
fallback={
|
||||||
<span data-slot="context-tool-group-title">
|
<span data-slot="context-tool-group-title">
|
||||||
<span data-slot="context-tool-group-label">Gathered context</span>
|
<span data-slot="context-tool-group-label">{i18n.t("ui.sessionTurn.status.gatheredContext")}</span>
|
||||||
<Show when={details().length}>
|
<Show when={details().length}>
|
||||||
<span data-slot="context-tool-group-summary">{details()}</span>
|
<span data-slot="context-tool-group-summary">{details()}</span>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -891,13 +1026,13 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
|||||||
<Show when={showPermission() && permission()}>
|
<Show when={showPermission() && permission()}>
|
||||||
<div data-component="permission-prompt">
|
<div data-component="permission-prompt">
|
||||||
<div data-slot="permission-actions">
|
<div data-slot="permission-actions">
|
||||||
<Button variant="ghost" size="small" onClick={() => respond("reject")}>
|
<Button variant="ghost" size="normal" onClick={() => respond("reject")}>
|
||||||
{i18n.t("ui.permission.deny")}
|
{i18n.t("ui.permission.deny")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" size="small" onClick={() => respond("always")}>
|
<Button variant="secondary" size="normal" onClick={() => respond("always")}>
|
||||||
{i18n.t("ui.permission.allowAlways")}
|
{i18n.t("ui.permission.allowAlways")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" size="small" onClick={() => respond("once")}>
|
<Button variant="primary" size="normal" onClick={() => respond("once")}>
|
||||||
{i18n.t("ui.permission.allowOnce")}
|
{i18n.t("ui.permission.allowOnce")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,85 +1,121 @@
|
|||||||
[data-component="radio-group"] {
|
[data-component="radio-group"] {
|
||||||
display: flex;
|
--radio-group-height: 28px;
|
||||||
flex-direction: column;
|
--radio-group-gap: 4px;
|
||||||
gap: calc(var(--spacing) * 2);
|
--radio-group-padding: 2px;
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
|
||||||
[data-slot="radio-group-wrapper"] {
|
[data-slot="radio-group-wrapper"] {
|
||||||
all: unset;
|
all: unset;
|
||||||
background-color: var(--surface-base);
|
background-color: var(--surface-inset-base);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-sm);
|
||||||
box-shadow: var(--shadow-xs-border);
|
box-shadow: var(--shadow-xxs-border);
|
||||||
|
display: inline-flex;
|
||||||
|
height: var(--radio-group-height);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
overflow: visible;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[data-fill] [data-slot="radio-group-wrapper"] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
[data-slot="radio-group-items"] {
|
[data-slot="radio-group-items"] {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
list-style: none;
|
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
gap: var(--radio-group-gap);
|
||||||
|
height: 100%;
|
||||||
|
list-style: none;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-fill] [data-slot="radio-group-items"] {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="radio-group-indicator"] {
|
[data-slot="radio-group-indicator"] {
|
||||||
background: var(--button-secondary-base);
|
background: var(--surface-raised-stronger-non-alpha);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-sm);
|
||||||
box-shadow: var(--shadow-xs-border);
|
box-shadow: var(--shadow-xs-border);
|
||||||
content: "";
|
content: "";
|
||||||
opacity: var(--indicator-opacity, 1);
|
opacity: var(--indicator-opacity, 1);
|
||||||
|
pointer-events: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
transition:
|
transition:
|
||||||
opacity 300ms ease-in-out,
|
opacity 200ms ease-out,
|
||||||
box-shadow 100ms ease-in-out,
|
box-shadow 100ms ease-in-out,
|
||||||
width 150ms ease,
|
width 200ms ease-out,
|
||||||
height 150ms ease,
|
height 200ms ease-out,
|
||||||
transform 150ms ease;
|
transform 200ms ease-out;
|
||||||
|
will-change: transform;
|
||||||
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="radio-group-item"] {
|
[data-slot="radio-group-item"] {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Separator between items */
|
&[data-fill] [data-slot="radio-group-item"] {
|
||||||
[data-slot="radio-group-item"]:not(:first-of-type)::before {
|
flex: 1;
|
||||||
background: var(--border-weak-base);
|
|
||||||
border-radius: var(--radius-xs);
|
|
||||||
content: "";
|
|
||||||
inset: 6px 0;
|
|
||||||
position: absolute;
|
|
||||||
transition: opacity 150ms ease;
|
|
||||||
width: 1px;
|
|
||||||
transform: translateX(-0.5px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide separator when item or previous item is checked */
|
[data-slot="radio-group-item-input"] {
|
||||||
[data-slot="radio-group-item"]:has([data-slot="radio-group-item-input"][data-checked])::before,
|
border-width: 0;
|
||||||
[data-slot="radio-group-item"]:has([data-slot="radio-group-item-input"][data-checked])
|
clip: rect(0 0 0 0);
|
||||||
+ [data-slot="radio-group-item"]::before {
|
height: 1px;
|
||||||
opacity: 0;
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="radio-group-item-label"] {
|
[data-slot="radio-group-item-label"] {
|
||||||
color: var(--text-weak);
|
color: var(--text-weak);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
font-family: var(--font-family-sans);
|
font-family: var(--font-family-sans);
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
border-radius: var(--radius-md);
|
flex: 1;
|
||||||
cursor: pointer;
|
height: 100%;
|
||||||
display: flex;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
gap: calc(var(--spacing) * 1);
|
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
padding: 6px 12px;
|
padding: var(--radio-group-padding);
|
||||||
place-content: center;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
transition-duration: 150ms;
|
transition:
|
||||||
transition-property: color, opacity;
|
color 200ms ease-out,
|
||||||
transition-timing-function: ease-in-out;
|
opacity 200ms ease-out;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="radio-group-item-input"] {
|
[data-slot="radio-group-item-control"] {
|
||||||
all: unset;
|
align-items: center;
|
||||||
|
border-radius: var(--radius-xs);
|
||||||
|
display: inline-flex;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 0;
|
||||||
|
padding: var(--radio-group-control-padding, 0 10px);
|
||||||
|
transition: background-color 200ms ease-out;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-pad="none"] {
|
||||||
|
--radio-group-control-padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-pad="normal"] {
|
||||||
|
--radio-group-control-padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Checked state */
|
/* Checked state */
|
||||||
@@ -87,28 +123,26 @@
|
|||||||
color: var(--text-strong);
|
color: var(--text-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hover state: match the inset background (adds subtle density) */
|
||||||
|
[data-slot="radio-group-item-input"]:not([data-checked], [data-disabled])
|
||||||
|
+ [data-slot="radio-group-item-label"]:hover
|
||||||
|
[data-slot="radio-group-item-control"] {
|
||||||
|
background-color: var(--surface-inset-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Do not overlay hover on the active segment */
|
||||||
|
[data-slot="radio-group-item-input"][data-checked]
|
||||||
|
+ [data-slot="radio-group-item-label"]:hover
|
||||||
|
[data-slot="radio-group-item-control"] {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
/* Disabled state */
|
/* Disabled state */
|
||||||
[data-slot="radio-group-item-input"][data-disabled] + [data-slot="radio-group-item-label"] {
|
[data-slot="radio-group-item-input"][data-disabled] + [data-slot="radio-group-item-label"] {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hover state for unchecked, enabled items */
|
|
||||||
[data-slot="radio-group-item-input"]:not([data-checked], [data-disabled]) + [data-slot="radio-group-item-label"] {
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="radio-group-item-input"]:not([data-checked], [data-disabled])
|
|
||||||
+ [data-slot="radio-group-item-label"]:hover {
|
|
||||||
color: var(--text-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-slot="radio-group-item-input"]:not([data-checked], [data-disabled])
|
|
||||||
+ [data-slot="radio-group-item-label"]:active {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Focus state */
|
/* Focus state */
|
||||||
[data-slot="radio-group-wrapper"]:has([data-slot="radio-group-item-input"]:focus-visible)
|
[data-slot="radio-group-wrapper"]:has([data-slot="radio-group-item-input"]:focus-visible)
|
||||||
[data-slot="radio-group-indicator"] {
|
[data-slot="radio-group-indicator"] {
|
||||||
@@ -126,27 +160,23 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[aria-orientation="vertical"] [data-slot="radio-group-item"]:not(:first-of-type)::before {
|
|
||||||
height: 1px;
|
|
||||||
width: auto;
|
|
||||||
inset: 0 6px;
|
|
||||||
transform: translateY(-0.5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Small size variant */
|
/* Small size variant */
|
||||||
&[data-size="small"] {
|
&[data-size="small"] {
|
||||||
|
--radio-group-height: 24px;
|
||||||
|
--radio-group-gap: 3px;
|
||||||
|
--radio-group-padding: 2px;
|
||||||
|
|
||||||
[data-slot="radio-group-item-label"] {
|
[data-slot="radio-group-item-label"] {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 4px 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="radio-group-item"]:not(:first-of-type)::before {
|
[data-slot="radio-group-item-control"] {
|
||||||
inset: 4px 0;
|
padding: var(--radio-group-control-padding, 0 8px);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&[aria-orientation="vertical"] [data-slot="radio-group-item"]:not(:first-of-type)::before {
|
&[data-size="small"][data-pad="normal"] {
|
||||||
inset: 0 4px;
|
--radio-group-control-padding: 0 8px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Disabled root state */
|
/* Disabled root state */
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export type RadioGroupProps<T> = Omit<
|
|||||||
class?: ComponentProps<"div">["class"]
|
class?: ComponentProps<"div">["class"]
|
||||||
classList?: ComponentProps<"div">["classList"]
|
classList?: ComponentProps<"div">["classList"]
|
||||||
size?: "small" | "medium"
|
size?: "small" | "medium"
|
||||||
|
fill?: boolean
|
||||||
|
pad?: "none" | "normal"
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RadioGroup<T>(props: RadioGroupProps<T>) {
|
export function RadioGroup<T>(props: RadioGroupProps<T>) {
|
||||||
@@ -28,6 +30,8 @@ export function RadioGroup<T>(props: RadioGroupProps<T>) {
|
|||||||
"label",
|
"label",
|
||||||
"onSelect",
|
"onSelect",
|
||||||
"size",
|
"size",
|
||||||
|
"fill",
|
||||||
|
"pad",
|
||||||
])
|
])
|
||||||
|
|
||||||
const getValue = (item: T): string => {
|
const getValue = (item: T): string => {
|
||||||
@@ -49,6 +53,8 @@ export function RadioGroup<T>(props: RadioGroupProps<T>) {
|
|||||||
{...others}
|
{...others}
|
||||||
data-component="radio-group"
|
data-component="radio-group"
|
||||||
data-size={local.size ?? "medium"}
|
data-size={local.size ?? "medium"}
|
||||||
|
data-fill={local.fill ? "" : undefined}
|
||||||
|
data-pad={local.pad ?? "normal"}
|
||||||
classList={{
|
classList={{
|
||||||
...(local.classList ?? {}),
|
...(local.classList ?? {}),
|
||||||
[local.class ?? ""]: !!local.class,
|
[local.class ?? ""]: !!local.class,
|
||||||
@@ -62,9 +68,11 @@ export function RadioGroup<T>(props: RadioGroupProps<T>) {
|
|||||||
<div role="presentation" data-slot="radio-group-items">
|
<div role="presentation" data-slot="radio-group-items">
|
||||||
<For each={local.options}>
|
<For each={local.options}>
|
||||||
{(option) => (
|
{(option) => (
|
||||||
<Kobalte.Item value={getValue(option)} data-slot="radio-group-item">
|
<Kobalte.Item value={getValue(option)} data-slot="radio-group-item" data-value={getValue(option)}>
|
||||||
<Kobalte.ItemInput data-slot="radio-group-item-input" />
|
<Kobalte.ItemInput data-slot="radio-group-item-input" />
|
||||||
<Kobalte.ItemLabel data-slot="radio-group-item-label">{getLabel(option)}</Kobalte.ItemLabel>
|
<Kobalte.ItemLabel data-slot="radio-group-item-label">
|
||||||
|
<span data-slot="radio-group-item-control">{getLabel(option)}</span>
|
||||||
|
</Kobalte.ItemLabel>
|
||||||
</Kobalte.Item>
|
</Kobalte.Item>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Binary } from "@opencode-ai/util/binary"
|
|||||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||||
import { createMemo, createSignal, For, ParentProps, Show } from "solid-js"
|
import { createMemo, createSignal, For, ParentProps, Show } from "solid-js"
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
import { Message } from "./message-part"
|
import { AssistantParts, Message } from "./message-part"
|
||||||
import { Card } from "./card"
|
import { Card } from "./card"
|
||||||
import { Collapsible } from "./collapsible"
|
import { Collapsible } from "./collapsible"
|
||||||
import { DiffChanges } from "./diff-changes"
|
import { DiffChanges } from "./diff-changes"
|
||||||
@@ -91,13 +91,6 @@ function visible(part: PartType) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
function AssistantMessageItem(props: { message: AssistantMessage; showAssistantCopyPartID?: string | null }) {
|
|
||||||
const data = useData()
|
|
||||||
const emptyParts: PartType[] = []
|
|
||||||
const msgParts = createMemo(() => list(data.store.part?.[props.message.id], emptyParts))
|
|
||||||
return <Message message={props.message} parts={msgParts()} showAssistantCopyPartID={props.showAssistantCopyPartID} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SessionTurn(
|
export function SessionTurn(
|
||||||
props: ParentProps<{
|
props: ParentProps<{
|
||||||
sessionID: string
|
sessionID: string
|
||||||
@@ -237,8 +230,7 @@ export function SessionTurn(
|
|||||||
const working = createMemo(() => status().type !== "idle" && isLastUserMessage())
|
const working = createMemo(() => status().type !== "idle" && isLastUserMessage())
|
||||||
|
|
||||||
const assistantCopyPartID = createMemo(() => {
|
const assistantCopyPartID = createMemo(() => {
|
||||||
if (!isLastUserMessage()) return null
|
if (working()) return null
|
||||||
if (status().type !== "idle") return null
|
|
||||||
return showAssistantCopyPartID() ?? null
|
return showAssistantCopyPartID() ?? null
|
||||||
})
|
})
|
||||||
const assistantVisible = createMemo(() =>
|
const assistantVisible = createMemo(() =>
|
||||||
@@ -281,14 +273,11 @@ export function SessionTurn(
|
|||||||
</Show>
|
</Show>
|
||||||
<Show when={assistantMessages().length > 0}>
|
<Show when={assistantMessages().length > 0}>
|
||||||
<div data-slot="session-turn-assistant-content" aria-hidden={working()}>
|
<div data-slot="session-turn-assistant-content" aria-hidden={working()}>
|
||||||
<For each={assistantMessages()}>
|
<AssistantParts
|
||||||
{(assistantMessage) => (
|
messages={assistantMessages()}
|
||||||
<AssistantMessageItem
|
showAssistantCopyPartID={assistantCopyPartID()}
|
||||||
message={assistantMessage}
|
working={working()}
|
||||||
showAssistantCopyPartID={assistantCopyPartID()}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={edited() > 0}>
|
<Show when={edited() > 0}>
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ export const dict = {
|
|||||||
|
|
||||||
"ui.sessionTurn.status.delegating": "تفويض العمل",
|
"ui.sessionTurn.status.delegating": "تفويض العمل",
|
||||||
"ui.sessionTurn.status.planning": "تخطيط الخطوات التالية",
|
"ui.sessionTurn.status.planning": "تخطيط الخطوات التالية",
|
||||||
"ui.sessionTurn.status.gatheringContext": "جمع السياق",
|
"ui.sessionTurn.status.gatheringContext": "استكشاف...",
|
||||||
|
"ui.sessionTurn.status.gatheredContext": "تم الاستكشاف",
|
||||||
"ui.sessionTurn.status.searchingCodebase": "البحث في قاعدة التعليمات البرمجية",
|
"ui.sessionTurn.status.searchingCodebase": "البحث في قاعدة التعليمات البرمجية",
|
||||||
"ui.sessionTurn.status.searchingWeb": "البحث في الويب",
|
"ui.sessionTurn.status.searchingWeb": "البحث في الويب",
|
||||||
"ui.sessionTurn.status.makingEdits": "إجراء تعديلات",
|
"ui.sessionTurn.status.makingEdits": "إجراء تعديلات",
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ export const dict = {
|
|||||||
|
|
||||||
"ui.sessionTurn.status.delegating": "Delegando trabalho",
|
"ui.sessionTurn.status.delegating": "Delegando trabalho",
|
||||||
"ui.sessionTurn.status.planning": "Planejando próximos passos",
|
"ui.sessionTurn.status.planning": "Planejando próximos passos",
|
||||||
"ui.sessionTurn.status.gatheringContext": "Coletando contexto",
|
"ui.sessionTurn.status.gatheringContext": "Explorando...",
|
||||||
|
"ui.sessionTurn.status.gatheredContext": "Explorado",
|
||||||
"ui.sessionTurn.status.searchingCodebase": "Pesquisando no código",
|
"ui.sessionTurn.status.searchingCodebase": "Pesquisando no código",
|
||||||
"ui.sessionTurn.status.searchingWeb": "Pesquisando na web",
|
"ui.sessionTurn.status.searchingWeb": "Pesquisando na web",
|
||||||
"ui.sessionTurn.status.makingEdits": "Fazendo edições",
|
"ui.sessionTurn.status.makingEdits": "Fazendo edições",
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ export const dict = {
|
|||||||
|
|
||||||
"ui.sessionTurn.status.delegating": "Delegiranje posla",
|
"ui.sessionTurn.status.delegating": "Delegiranje posla",
|
||||||
"ui.sessionTurn.status.planning": "Planiranje sljedećih koraka",
|
"ui.sessionTurn.status.planning": "Planiranje sljedećih koraka",
|
||||||
"ui.sessionTurn.status.gatheringContext": "Prikupljanje konteksta",
|
"ui.sessionTurn.status.gatheringContext": "Istraživanje...",
|
||||||
|
"ui.sessionTurn.status.gatheredContext": "Istraženo",
|
||||||
"ui.sessionTurn.status.searchingCodebase": "Pretraživanje baze koda",
|
"ui.sessionTurn.status.searchingCodebase": "Pretraživanje baze koda",
|
||||||
"ui.sessionTurn.status.searchingWeb": "Pretraživanje weba",
|
"ui.sessionTurn.status.searchingWeb": "Pretraživanje weba",
|
||||||
"ui.sessionTurn.status.makingEdits": "Pravljenje izmjena",
|
"ui.sessionTurn.status.makingEdits": "Pravljenje izmjena",
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export const dict = {
|
|||||||
|
|
||||||
"ui.sessionTurn.status.delegating": "Delegerer arbejde",
|
"ui.sessionTurn.status.delegating": "Delegerer arbejde",
|
||||||
"ui.sessionTurn.status.planning": "Planlægger næste trin",
|
"ui.sessionTurn.status.planning": "Planlægger næste trin",
|
||||||
"ui.sessionTurn.status.gatheringContext": "Indsamler kontekst",
|
"ui.sessionTurn.status.gatheringContext": "Udforsker...",
|
||||||
|
"ui.sessionTurn.status.gatheredContext": "Udforsket",
|
||||||
"ui.sessionTurn.status.searchingCodebase": "Søger i koden",
|
"ui.sessionTurn.status.searchingCodebase": "Søger i koden",
|
||||||
"ui.sessionTurn.status.searchingWeb": "Søger på nettet",
|
"ui.sessionTurn.status.searchingWeb": "Søger på nettet",
|
||||||
"ui.sessionTurn.status.makingEdits": "Laver ændringer",
|
"ui.sessionTurn.status.makingEdits": "Laver ændringer",
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ export const dict = {
|
|||||||
|
|
||||||
"ui.sessionTurn.status.delegating": "Arbeit delegieren",
|
"ui.sessionTurn.status.delegating": "Arbeit delegieren",
|
||||||
"ui.sessionTurn.status.planning": "Nächste Schritte planen",
|
"ui.sessionTurn.status.planning": "Nächste Schritte planen",
|
||||||
"ui.sessionTurn.status.gatheringContext": "Kontext sammeln",
|
"ui.sessionTurn.status.gatheringContext": "Erkunden...",
|
||||||
|
"ui.sessionTurn.status.gatheredContext": "Erkundet",
|
||||||
"ui.sessionTurn.status.searchingCodebase": "Codebasis durchsuchen",
|
"ui.sessionTurn.status.searchingCodebase": "Codebasis durchsuchen",
|
||||||
"ui.sessionTurn.status.searchingWeb": "Web durchsuchen",
|
"ui.sessionTurn.status.searchingWeb": "Web durchsuchen",
|
||||||
"ui.sessionTurn.status.makingEdits": "Änderungen vornehmen",
|
"ui.sessionTurn.status.makingEdits": "Änderungen vornehmen",
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ export const dict = {
|
|||||||
|
|
||||||
"ui.sessionTurn.status.delegating": "Delegating work",
|
"ui.sessionTurn.status.delegating": "Delegating work",
|
||||||
"ui.sessionTurn.status.planning": "Planning next steps",
|
"ui.sessionTurn.status.planning": "Planning next steps",
|
||||||
"ui.sessionTurn.status.gatheringContext": "Gathering context",
|
"ui.sessionTurn.status.gatheringContext": "Exploring...",
|
||||||
|
"ui.sessionTurn.status.gatheredContext": "Explored",
|
||||||
"ui.sessionTurn.status.searchingCodebase": "Searching the codebase",
|
"ui.sessionTurn.status.searchingCodebase": "Searching the codebase",
|
||||||
"ui.sessionTurn.status.searchingWeb": "Searching the web",
|
"ui.sessionTurn.status.searchingWeb": "Searching the web",
|
||||||
"ui.sessionTurn.status.makingEdits": "Making edits",
|
"ui.sessionTurn.status.makingEdits": "Making edits",
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ export const dict = {
|
|||||||
|
|
||||||
"ui.sessionTurn.status.delegating": "Delegando trabajo",
|
"ui.sessionTurn.status.delegating": "Delegando trabajo",
|
||||||
"ui.sessionTurn.status.planning": "Planificando siguientes pasos",
|
"ui.sessionTurn.status.planning": "Planificando siguientes pasos",
|
||||||
"ui.sessionTurn.status.gatheringContext": "Recopilando contexto",
|
"ui.sessionTurn.status.gatheringContext": "Explorando...",
|
||||||
|
"ui.sessionTurn.status.gatheredContext": "Explorado",
|
||||||
"ui.sessionTurn.status.searchingCodebase": "Buscando en la base de código",
|
"ui.sessionTurn.status.searchingCodebase": "Buscando en la base de código",
|
||||||
"ui.sessionTurn.status.searchingWeb": "Buscando en la web",
|
"ui.sessionTurn.status.searchingWeb": "Buscando en la web",
|
||||||
"ui.sessionTurn.status.makingEdits": "Realizando ediciones",
|
"ui.sessionTurn.status.makingEdits": "Realizando ediciones",
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ export const dict = {
|
|||||||
|
|
||||||
"ui.sessionTurn.status.delegating": "Délégation du travail",
|
"ui.sessionTurn.status.delegating": "Délégation du travail",
|
||||||
"ui.sessionTurn.status.planning": "Planification des prochaines étapes",
|
"ui.sessionTurn.status.planning": "Planification des prochaines étapes",
|
||||||
"ui.sessionTurn.status.gatheringContext": "Collecte du contexte",
|
"ui.sessionTurn.status.gatheringContext": "Exploration...",
|
||||||
|
"ui.sessionTurn.status.gatheredContext": "Exploré",
|
||||||
"ui.sessionTurn.status.searchingCodebase": "Recherche dans la base de code",
|
"ui.sessionTurn.status.searchingCodebase": "Recherche dans la base de code",
|
||||||
"ui.sessionTurn.status.searchingWeb": "Recherche sur le web",
|
"ui.sessionTurn.status.searchingWeb": "Recherche sur le web",
|
||||||
"ui.sessionTurn.status.makingEdits": "Application des modifications",
|
"ui.sessionTurn.status.makingEdits": "Application des modifications",
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export const dict = {
|
|||||||
|
|
||||||
"ui.sessionTurn.status.delegating": "作業を委任中",
|
"ui.sessionTurn.status.delegating": "作業を委任中",
|
||||||
"ui.sessionTurn.status.planning": "次のステップを計画中",
|
"ui.sessionTurn.status.planning": "次のステップを計画中",
|
||||||
"ui.sessionTurn.status.gatheringContext": "コンテキストを収集中",
|
"ui.sessionTurn.status.gatheringContext": "探索中...",
|
||||||
|
"ui.sessionTurn.status.gatheredContext": "探索済み",
|
||||||
"ui.sessionTurn.status.searchingCodebase": "コードベースを検索中",
|
"ui.sessionTurn.status.searchingCodebase": "コードベースを検索中",
|
||||||
"ui.sessionTurn.status.searchingWeb": "ウェブを検索中",
|
"ui.sessionTurn.status.searchingWeb": "ウェブを検索中",
|
||||||
"ui.sessionTurn.status.makingEdits": "編集を実行中",
|
"ui.sessionTurn.status.makingEdits": "編集を実行中",
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ export const dict = {
|
|||||||
|
|
||||||
"ui.sessionTurn.status.delegating": "작업 위임 중",
|
"ui.sessionTurn.status.delegating": "작업 위임 중",
|
||||||
"ui.sessionTurn.status.planning": "다음 단계 계획 중",
|
"ui.sessionTurn.status.planning": "다음 단계 계획 중",
|
||||||
"ui.sessionTurn.status.gatheringContext": "컨텍스트 수집 중",
|
"ui.sessionTurn.status.gatheringContext": "탐색 중...",
|
||||||
|
"ui.sessionTurn.status.gatheredContext": "탐색됨",
|
||||||
"ui.sessionTurn.status.searchingCodebase": "코드베이스 검색 중",
|
"ui.sessionTurn.status.searchingCodebase": "코드베이스 검색 중",
|
||||||
"ui.sessionTurn.status.searchingWeb": "웹 검색 중",
|
"ui.sessionTurn.status.searchingWeb": "웹 검색 중",
|
||||||
"ui.sessionTurn.status.makingEdits": "편집 수행 중",
|
"ui.sessionTurn.status.makingEdits": "편집 수행 중",
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ export const dict: Record<Keys, string> = {
|
|||||||
|
|
||||||
"ui.sessionTurn.status.delegating": "Delegerer arbeid",
|
"ui.sessionTurn.status.delegating": "Delegerer arbeid",
|
||||||
"ui.sessionTurn.status.planning": "Planlegger neste trinn",
|
"ui.sessionTurn.status.planning": "Planlegger neste trinn",
|
||||||
"ui.sessionTurn.status.gatheringContext": "Samler inn kontekst",
|
"ui.sessionTurn.status.gatheringContext": "Utforsker...",
|
||||||
|
"ui.sessionTurn.status.gatheredContext": "Utforsket",
|
||||||
"ui.sessionTurn.status.searchingCodebase": "Søker i kodebasen",
|
"ui.sessionTurn.status.searchingCodebase": "Søker i kodebasen",
|
||||||
"ui.sessionTurn.status.searchingWeb": "Søker på nettet",
|
"ui.sessionTurn.status.searchingWeb": "Søker på nettet",
|
||||||
"ui.sessionTurn.status.makingEdits": "Gjør endringer",
|
"ui.sessionTurn.status.makingEdits": "Gjør endringer",
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export const dict = {
|
|||||||
|
|
||||||
"ui.sessionTurn.status.delegating": "Delegowanie pracy",
|
"ui.sessionTurn.status.delegating": "Delegowanie pracy",
|
||||||
"ui.sessionTurn.status.planning": "Planowanie kolejnych kroków",
|
"ui.sessionTurn.status.planning": "Planowanie kolejnych kroków",
|
||||||
"ui.sessionTurn.status.gatheringContext": "Zbieranie kontekstu",
|
"ui.sessionTurn.status.gatheringContext": "Eksplorowanie...",
|
||||||
|
"ui.sessionTurn.status.gatheredContext": "Wyeksplorowano",
|
||||||
"ui.sessionTurn.status.searchingCodebase": "Przeszukiwanie bazy kodu",
|
"ui.sessionTurn.status.searchingCodebase": "Przeszukiwanie bazy kodu",
|
||||||
"ui.sessionTurn.status.searchingWeb": "Przeszukiwanie sieci",
|
"ui.sessionTurn.status.searchingWeb": "Przeszukiwanie sieci",
|
||||||
"ui.sessionTurn.status.makingEdits": "Wprowadzanie zmian",
|
"ui.sessionTurn.status.makingEdits": "Wprowadzanie zmian",
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export const dict = {
|
|||||||
|
|
||||||
"ui.sessionTurn.status.delegating": "Делегирование работы",
|
"ui.sessionTurn.status.delegating": "Делегирование работы",
|
||||||
"ui.sessionTurn.status.planning": "Планирование следующих шагов",
|
"ui.sessionTurn.status.planning": "Планирование следующих шагов",
|
||||||
"ui.sessionTurn.status.gatheringContext": "Сбор контекста",
|
"ui.sessionTurn.status.gatheringContext": "Исследование...",
|
||||||
|
"ui.sessionTurn.status.gatheredContext": "Исследовано",
|
||||||
"ui.sessionTurn.status.searchingCodebase": "Поиск в кодовой базе",
|
"ui.sessionTurn.status.searchingCodebase": "Поиск в кодовой базе",
|
||||||
"ui.sessionTurn.status.searchingWeb": "Поиск в интернете",
|
"ui.sessionTurn.status.searchingWeb": "Поиск в интернете",
|
||||||
"ui.sessionTurn.status.makingEdits": "Внесение изменений",
|
"ui.sessionTurn.status.makingEdits": "Внесение изменений",
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ export const dict = {
|
|||||||
|
|
||||||
"ui.sessionTurn.status.delegating": "มอบหมายงาน",
|
"ui.sessionTurn.status.delegating": "มอบหมายงาน",
|
||||||
"ui.sessionTurn.status.planning": "วางแผนขั้นตอนถัดไป",
|
"ui.sessionTurn.status.planning": "วางแผนขั้นตอนถัดไป",
|
||||||
"ui.sessionTurn.status.gatheringContext": "รวบรวมบริบท",
|
"ui.sessionTurn.status.gatheringContext": "กำลังสำรวจ...",
|
||||||
|
"ui.sessionTurn.status.gatheredContext": "สำรวจแล้ว",
|
||||||
"ui.sessionTurn.status.searchingCodebase": "กำลังค้นหาโค้ดเบส",
|
"ui.sessionTurn.status.searchingCodebase": "กำลังค้นหาโค้ดเบส",
|
||||||
"ui.sessionTurn.status.searchingWeb": "กำลังค้นหาบนเว็บ",
|
"ui.sessionTurn.status.searchingWeb": "กำลังค้นหาบนเว็บ",
|
||||||
"ui.sessionTurn.status.makingEdits": "กำลังแก้ไข",
|
"ui.sessionTurn.status.makingEdits": "กำลังแก้ไข",
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ export const dict = {
|
|||||||
|
|
||||||
"ui.sessionTurn.status.delegating": "正在委派工作",
|
"ui.sessionTurn.status.delegating": "正在委派工作",
|
||||||
"ui.sessionTurn.status.planning": "正在规划下一步",
|
"ui.sessionTurn.status.planning": "正在规划下一步",
|
||||||
"ui.sessionTurn.status.gatheringContext": "正在收集上下文",
|
"ui.sessionTurn.status.gatheringContext": "正在探索...",
|
||||||
|
"ui.sessionTurn.status.gatheredContext": "已探索",
|
||||||
"ui.sessionTurn.status.searchingCodebase": "正在搜索代码库",
|
"ui.sessionTurn.status.searchingCodebase": "正在搜索代码库",
|
||||||
"ui.sessionTurn.status.searchingWeb": "正在搜索网页",
|
"ui.sessionTurn.status.searchingWeb": "正在搜索网页",
|
||||||
"ui.sessionTurn.status.makingEdits": "正在修改",
|
"ui.sessionTurn.status.makingEdits": "正在修改",
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ export const dict = {
|
|||||||
|
|
||||||
"ui.sessionTurn.status.delegating": "正在委派工作",
|
"ui.sessionTurn.status.delegating": "正在委派工作",
|
||||||
"ui.sessionTurn.status.planning": "正在規劃下一步",
|
"ui.sessionTurn.status.planning": "正在規劃下一步",
|
||||||
"ui.sessionTurn.status.gatheringContext": "正在收集上下文",
|
"ui.sessionTurn.status.gatheringContext": "正在探索...",
|
||||||
|
"ui.sessionTurn.status.gatheredContext": "已探索",
|
||||||
"ui.sessionTurn.status.searchingCodebase": "正在搜尋程式碼庫",
|
"ui.sessionTurn.status.searchingCodebase": "正在搜尋程式碼庫",
|
||||||
"ui.sessionTurn.status.searchingWeb": "正在搜尋網頁",
|
"ui.sessionTurn.status.searchingWeb": "正在搜尋網頁",
|
||||||
"ui.sessionTurn.status.makingEdits": "正在修改",
|
"ui.sessionTurn.status.makingEdits": "正在修改",
|
||||||
|
|||||||
@@ -19,12 +19,35 @@ export const virtualMetrics: Partial<VirtualFileMetrics> = {
|
|||||||
fileGap: 0,
|
fileGap: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scrollable(value: string) {
|
||||||
|
return value === "auto" || value === "scroll" || value === "overlay"
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollRoot(container: HTMLElement) {
|
||||||
|
let node = container.parentElement
|
||||||
|
while (node) {
|
||||||
|
const style = getComputedStyle(node)
|
||||||
|
if (scrollable(style.overflowY)) return node
|
||||||
|
node = node.parentElement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function target(container: HTMLElement): Target | undefined {
|
function target(container: HTMLElement): Target | undefined {
|
||||||
if (typeof document === "undefined") return
|
if (typeof document === "undefined") return
|
||||||
|
|
||||||
const root = container.closest("[data-component='session-review']")
|
const review = container.closest("[data-component='session-review']")
|
||||||
if (root instanceof HTMLElement) {
|
if (review instanceof HTMLElement) {
|
||||||
const content = root.querySelector("[data-slot='session-review-container']")
|
const content = review.querySelector("[data-slot='session-review-container']")
|
||||||
|
return {
|
||||||
|
key: review,
|
||||||
|
root: review,
|
||||||
|
content: content instanceof HTMLElement ? content : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = scrollRoot(container)
|
||||||
|
if (root) {
|
||||||
|
const content = root.querySelector("[role='log']")
|
||||||
return {
|
return {
|
||||||
key: root,
|
key: root,
|
||||||
root,
|
root,
|
||||||
|
|||||||
@@ -253,7 +253,7 @@
|
|||||||
--icon-success-base: var(--apple-light-7);
|
--icon-success-base: var(--apple-light-7);
|
||||||
--icon-success-hover: var(--apple-light-8);
|
--icon-success-hover: var(--apple-light-8);
|
||||||
--icon-success-active: var(--apple-light-11);
|
--icon-success-active: var(--apple-light-11);
|
||||||
--icon-warning-base: var(--amber-light-7);
|
--icon-warning-base: var(--amber-light-9);
|
||||||
--icon-warning-hover: var(--amber-light-8);
|
--icon-warning-hover: var(--amber-light-8);
|
||||||
--icon-warning-active: var(--amber-light-11);
|
--icon-warning-active: var(--amber-light-11);
|
||||||
--icon-critical-base: var(--ember-light-10);
|
--icon-critical-base: var(--ember-light-10);
|
||||||
@@ -287,7 +287,7 @@
|
|||||||
--icon-diff-add-active: var(--mint-light-12);
|
--icon-diff-add-active: var(--mint-light-12);
|
||||||
--icon-diff-delete-base: var(--ember-light-10);
|
--icon-diff-delete-base: var(--ember-light-10);
|
||||||
--icon-diff-delete-hover: var(--ember-light-11);
|
--icon-diff-delete-hover: var(--ember-light-11);
|
||||||
--icon-diff-modified-base: var(--icon-warning-base);
|
--icon-diff-modified-base: #ff8c00;
|
||||||
--syntax-comment: var(--text-weak);
|
--syntax-comment: var(--text-weak);
|
||||||
--syntax-regexp: var(--text-base);
|
--syntax-regexp: var(--text-base);
|
||||||
--syntax-string: #006656;
|
--syntax-string: #006656;
|
||||||
@@ -507,7 +507,7 @@
|
|||||||
--icon-strong-disabled: var(--smoke-dark-8);
|
--icon-strong-disabled: var(--smoke-dark-8);
|
||||||
--icon-strong-focus: #fdfcfc;
|
--icon-strong-focus: #fdfcfc;
|
||||||
--icon-brand-base: var(--white);
|
--icon-brand-base: var(--white);
|
||||||
--icon-interactive-base: var(--cobalt-dark-9);
|
--icon-interactive-base: var(--cobalt-dark-11);
|
||||||
--icon-success-base: var(--apple-dark-7);
|
--icon-success-base: var(--apple-dark-7);
|
||||||
--icon-success-hover: var(--apple-dark-8);
|
--icon-success-hover: var(--apple-dark-8);
|
||||||
--icon-success-active: var(--apple-dark-11);
|
--icon-success-active: var(--apple-dark-11);
|
||||||
@@ -545,7 +545,7 @@
|
|||||||
--icon-diff-add-active: var(--mint-dark-11);
|
--icon-diff-add-active: var(--mint-dark-11);
|
||||||
--icon-diff-delete-base: var(--ember-dark-9);
|
--icon-diff-delete-base: var(--ember-dark-9);
|
||||||
--icon-diff-delete-hover: var(--ember-dark-10);
|
--icon-diff-delete-hover: var(--ember-dark-10);
|
||||||
--icon-diff-modified-base: var(--icon-warning-base);
|
--icon-diff-modified-base: #ffba92;
|
||||||
--syntax-comment: var(--text-weak);
|
--syntax-comment: var(--text-weak);
|
||||||
--syntax-regexp: var(--text-base);
|
--syntax-regexp: var(--text-base);
|
||||||
--syntax-string: #00ceb9;
|
--syntax-string: #00ceb9;
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ export function resolveThemeVariant(variant: ThemeVariant, isDark: boolean): Res
|
|||||||
tokens["icon-diff-add-active"] = diffAdd[isDark ? 10 : 11]
|
tokens["icon-diff-add-active"] = diffAdd[isDark ? 10 : 11]
|
||||||
tokens["icon-diff-delete-base"] = diffDelete[isDark ? 8 : 9]
|
tokens["icon-diff-delete-base"] = diffDelete[isDark ? 8 : 9]
|
||||||
tokens["icon-diff-delete-hover"] = diffDelete[isDark ? 9 : 10]
|
tokens["icon-diff-delete-hover"] = diffDelete[isDark ? 9 : 10]
|
||||||
tokens["icon-diff-modified-base"] = tokens["icon-warning-base"]
|
tokens["icon-diff-modified-base"] = isDark ? "#ffba92" : "#FF8C00"
|
||||||
|
|
||||||
tokens["syntax-comment"] = "var(--text-weak)"
|
tokens["syntax-comment"] = "var(--text-weak)"
|
||||||
tokens["syntax-regexp"] = "var(--text-base)"
|
tokens["syntax-regexp"] = "var(--text-base)"
|
||||||
|
|||||||
@@ -440,7 +440,7 @@
|
|||||||
"icon-strong-disabled": "var(--smoke-dark-8)",
|
"icon-strong-disabled": "var(--smoke-dark-8)",
|
||||||
"icon-strong-focus": "#fdfcfc",
|
"icon-strong-focus": "#fdfcfc",
|
||||||
"icon-brand-base": "var(--white)",
|
"icon-brand-base": "var(--white)",
|
||||||
"icon-interactive-base": "var(--cobalt-dark-9)",
|
"icon-interactive-base": "var(--cobalt-dark-11)",
|
||||||
"icon-success-base": "var(--apple-dark-9)",
|
"icon-success-base": "var(--apple-dark-9)",
|
||||||
"icon-success-hover": "var(--apple-dark-10)",
|
"icon-success-hover": "var(--apple-dark-10)",
|
||||||
"icon-success-active": "var(--apple-dark-11)",
|
"icon-success-active": "var(--apple-dark-11)",
|
||||||
|
|||||||
@@ -73,13 +73,14 @@ You can also access our models through the following API endpoints.
|
|||||||
| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||||
| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||||
| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
| GPT 5 Nano | gpt-5-nano | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
|
||||||
|
| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
|
| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
|
| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
|
| Claude Sonnet 4.6 | claude-sonnet-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
| Claude Sonnet 4.5 | claude-sonnet-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
| Claude Haiku 4.5 | claude-haiku-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
||||||
| Claude Opus 4.6 | claude-opus-4-6 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
|
||||||
| Claude Opus 4.5 | claude-opus-4-5 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
|
||||||
| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
|
|
||||||
| Gemini 3 Pro | gemini-3-pro | `https://opencode.ai/zen/v1/models/gemini-3-pro` | `@ai-sdk/google` |
|
| Gemini 3 Pro | gemini-3-pro | `https://opencode.ai/zen/v1/models/gemini-3-pro` | `@ai-sdk/google` |
|
||||||
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
| Gemini 3 Flash | gemini-3-flash | `https://opencode.ai/zen/v1/models/gemini-3-flash` | `@ai-sdk/google` |
|
||||||
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
| MiniMax M2.5 | minimax-m2.5 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
|
||||||
@@ -131,16 +132,18 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
|
|||||||
| Kimi K2 Thinking | $0.40 | $2.50 | - | - |
|
| Kimi K2 Thinking | $0.40 | $2.50 | - | - |
|
||||||
| Kimi K2 | $0.40 | $2.50 | - | - |
|
| Kimi K2 | $0.40 | $2.50 | - | - |
|
||||||
| Qwen3 Coder 480B | $0.45 | $1.50 | - | - |
|
| Qwen3 Coder 480B | $0.45 | $1.50 | - | - |
|
||||||
|
| Claude Opus 4.6 (≤ 200K tokens) | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||||
|
| Claude Opus 4.6 (> 200K tokens) | $10.00 | $37.50 | $1.00 | $12.50 |
|
||||||
|
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
|
||||||
|
| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 |
|
||||||
|
| Claude Sonnet 4.6 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
|
||||||
|
| Claude Sonnet 4.6 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
|
||||||
| Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
|
| Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
|
||||||
| Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
|
| Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
|
||||||
| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
|
| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
|
||||||
| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
|
| Claude Sonnet 4 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
|
||||||
| Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 |
|
| Claude Haiku 4.5 | $1.00 | $5.00 | $0.10 | $1.25 |
|
||||||
| Claude Haiku 3.5 | $0.80 | $4.00 | $0.08 | $1.00 |
|
| Claude Haiku 3.5 | $0.80 | $4.00 | $0.08 | $1.00 |
|
||||||
| Claude Opus 4.6 (≤ 200K tokens) | $5.00 | $25.00 | $0.50 | $6.25 |
|
|
||||||
| Claude Opus 4.6 (> 200K tokens) | $10.00 | $37.50 | $1.00 | $12.50 |
|
|
||||||
| Claude Opus 4.5 | $5.00 | $25.00 | $0.50 | $6.25 |
|
|
||||||
| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 |
|
|
||||||
| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
| Gemini 3 Pro (≤ 200K tokens) | $2.00 | $12.00 | $0.20 | - |
|
||||||
| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
| Gemini 3 Pro (> 200K tokens) | $4.00 | $18.00 | $0.40 | - |
|
||||||
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
|
| Gemini 3 Flash | $0.50 | $3.00 | $0.05 | - |
|
||||||
|
|||||||
@@ -1,234 +0,0 @@
|
|||||||
From 90904222b6f8c86a6d0a8ebed9661950f632a4e8 Mon Sep 17 00:00:00 2001
|
|
||||||
From: OpenCode Bot <opencode@sst.dev>
|
|
||||||
Date: Wed, 11 Feb 2026 18:44:27 +0000
|
|
||||||
Subject: [PATCH] add square logo variants to brand page
|
|
||||||
|
|
||||||
---
|
|
||||||
.../asset/brand/opencode-logo-dark-square.png | Bin 0 -> 697 bytes
|
|
||||||
.../asset/brand/opencode-logo-dark-square.svg | 18 ++++++
|
|
||||||
.../brand/opencode-logo-light-square.png | Bin 0 -> 697 bytes
|
|
||||||
.../brand/opencode-logo-light-square.svg | 18 ++++++
|
|
||||||
.../preview-opencode-logo-dark-square.png | Bin 0 -> 1477 bytes
|
|
||||||
.../preview-opencode-logo-light-square.png | Bin 0 -> 1467 bytes
|
|
||||||
.../console/app/src/routes/brand/index.tsx | 60 ++++++++++++++++++
|
|
||||||
7 files changed, 96 insertions(+)
|
|
||||||
create mode 100644 packages/console/app/src/asset/brand/opencode-logo-dark-square.png
|
|
||||||
create mode 100644 packages/console/app/src/asset/brand/opencode-logo-dark-square.svg
|
|
||||||
create mode 100644 packages/console/app/src/asset/brand/opencode-logo-light-square.png
|
|
||||||
create mode 100644 packages/console/app/src/asset/brand/opencode-logo-light-square.svg
|
|
||||||
create mode 100644 packages/console/app/src/asset/brand/preview-opencode-logo-dark-square.png
|
|
||||||
create mode 100644 packages/console/app/src/asset/brand/preview-opencode-logo-light-square.png
|
|
||||||
|
|
||||||
diff --git a/packages/console/app/src/asset/brand/opencode-logo-dark-square.png b/packages/console/app/src/asset/brand/opencode-logo-dark-square.png
|
|
||||||
new file mode 100644
|
|
||||||
index 0000000000000000000000000000000000000000..673c7e3a20f917fae56719ed1c35b4614ecd5f53
|
|
||||||
GIT binary patch
|
|
||||||
literal 697
|
|
||||||
zcmeAS@N?(olHy`uVBq!ia0y~yV2S`?CT5_>VU7ZSAjOjI=<CS9u(6-}Pa-RjuaN8!
|
|
||||||
z<jcTNrN+R}(89p*3n<j^f`OsbfPvvv0t1893<d`Af;qbaZGaLy0X`wFK>FjGH{Nb;
|
|
||||||
zK*kCWpQS*Gu_VYZn8D%MjWiG^$=lt9p@UV{1IXbl@Q5sCU=ULUVMfm&l@CBc_7YED
|
|
||||||
zSN2y-+(O#2cjOjy1NC%xx;TbZ+<JS?ke5M0fMtVmr)P-K<_$G=ESI!0Hc3x^;^R#P
|
|
||||||
z@VYyUYYSCCTI00A1HzVGb*Dn;c)vb-jcFpozT1*PW*Wd~QY~?fC`m~yNwrEYN(E93
|
|
||||||
zMg~S^x&}tNhK3=A7FH&PRz^nJ1_o9J26?+;7NKa!%}>cptHiBA{`nI*paup{S3j3^
|
|
||||||
HP6<r_YMlsn
|
|
||||||
|
|
||||||
literal 0
|
|
||||||
HcmV?d00001
|
|
||||||
|
|
||||||
diff --git a/packages/console/app/src/asset/brand/opencode-logo-dark-square.svg b/packages/console/app/src/asset/brand/opencode-logo-dark-square.svg
|
|
||||||
new file mode 100644
|
|
||||||
index 0000000..6a67f62
|
|
||||||
--- /dev/null
|
|
||||||
+++ b/packages/console/app/src/asset/brand/opencode-logo-dark-square.svg
|
|
||||||
@@ -0,0 +1,18 @@
|
|
||||||
+<svg width="300" height="300" viewBox="0 0 300 300" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
+<g transform="translate(30, 0)">
|
|
||||||
+<g clip-path="url(#clip0_1401_86283)">
|
|
||||||
+<mask id="mask0_1401_86283" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="240" height="300">
|
|
||||||
+<path d="M240 0H0V300H240V0Z" fill="white"/>
|
|
||||||
+</mask>
|
|
||||||
+<g mask="url(#mask0_1401_86283)">
|
|
||||||
+<path d="M180 240H60V120H180V240Z" fill="#4B4646"/>
|
|
||||||
+<path d="M180 60H60V240H180V60ZM240 300H0V0H240V300Z" fill="#F1ECEC"/>
|
|
||||||
+</g>
|
|
||||||
+</g>
|
|
||||||
+</g>
|
|
||||||
+<defs>
|
|
||||||
+<clipPath id="clip0_1401_86283">
|
|
||||||
+<rect width="240" height="300" fill="white"/>
|
|
||||||
+</clipPath>
|
|
||||||
+</defs>
|
|
||||||
+</svg>
|
|
||||||
diff --git a/packages/console/app/src/asset/brand/opencode-logo-light-square.png b/packages/console/app/src/asset/brand/opencode-logo-light-square.png
|
|
||||||
new file mode 100644
|
|
||||||
index 0000000000000000000000000000000000000000..5c710474abc4668504cb9678da1de6ad33458af9
|
|
||||||
GIT binary patch
|
|
||||||
literal 697
|
|
||||||
zcmeAS@N?(olHy`uVBq!ia0y~yV2S`?CT5_>VU7ZSAjOjI=<CS9u(6-}Pa-RjuaN8!
|
|
||||||
z<jcTNrN+R}(89p*3n<j^f`OsbfPvvv0t1893<d`Af;qbaZGaLy0X`wFKw42w?)<s4
|
|
||||||
zK*kyVm4AQ~V@Z%-FoVOh8)+a;lDE4HLkFv@2av;A;1OBOz#ygy!i=6lDj$G?>?NMQ
|
|
||||||
zuI#UvxP`Q3@5n9a2I}eXba4!+xb^m&Auof10LupBPR|gd%^Pa$ST1R0Y?7Y-#K)To
|
|
||||||
z;B|Kx*A}XPw8m+J2ZSxX>Q05w@qT^w8q-9EeYYip%rt<}q*~${QIe8al4_M)lnSI6
|
|
||||||
zj0}v-bPbGj4GlvKEv!rot&EJc4GgRd4DxoxEJD$co1c=IR*74K{PQPrKn)C@u6{1-
|
|
||||||
HoD!M<*)j+`
|
|
||||||
|
|
||||||
literal 0
|
|
||||||
HcmV?d00001
|
|
||||||
|
|
||||||
diff --git a/packages/console/app/src/asset/brand/opencode-logo-light-square.svg b/packages/console/app/src/asset/brand/opencode-logo-light-square.svg
|
|
||||||
new file mode 100644
|
|
||||||
index 0000000..a738ad8
|
|
||||||
--- /dev/null
|
|
||||||
+++ b/packages/console/app/src/asset/brand/opencode-logo-light-square.svg
|
|
||||||
@@ -0,0 +1,18 @@
|
|
||||||
+<svg width="300" height="300" viewBox="0 0 300 300" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
+<g transform="translate(30, 0)">
|
|
||||||
+<g clip-path="url(#clip0_1401_86274)">
|
|
||||||
+<mask id="mask0_1401_86274" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="240" height="300">
|
|
||||||
+<path d="M240 0H0V300H240V0Z" fill="white"/>
|
|
||||||
+</mask>
|
|
||||||
+<g mask="url(#mask0_1401_86274)">
|
|
||||||
+<path d="M180 240H60V120H180V240Z" fill="#CFCECD"/>
|
|
||||||
+<path d="M180 60H60V240H180V60ZM240 300H0V0H240V300Z" fill="#211E1E"/>
|
|
||||||
+</g>
|
|
||||||
+</g>
|
|
||||||
+</g>
|
|
||||||
+<defs>
|
|
||||||
+<clipPath id="clip0_1401_86274">
|
|
||||||
+<rect width="240" height="300" fill="white"/>
|
|
||||||
+</clipPath>
|
|
||||||
+</defs>
|
|
||||||
+</svg>
|
|
||||||
diff --git a/packages/console/app/src/asset/brand/preview-opencode-logo-dark-square.png b/packages/console/app/src/asset/brand/preview-opencode-logo-dark-square.png
|
|
||||||
new file mode 100644
|
|
||||||
index 0000000000000000000000000000000000000000..604ad7aa7a87c71d4a3972f18da4044e53f745fe
|
|
||||||
GIT binary patch
|
|
||||||
literal 1477
|
|
||||||
zcmeAS@N?(olHy`uVBq!ia0y~yV4MKL9LzwG?TN?!0V$SrM_)$<hK>E)e-c@Ne1&9>
|
|
||||||
zAYTTCDm4a%h86~fUqGRT7Yq!g1`G_Z5*Qe)W-u^_7tGleXakf83GfMV1=1hiyun28
|
|
||||||
zU%gs1cdnz2jklZIqq}#b!@|5=U4d+%%7=IEM1_VzxRD_to-Qsx1sTc7KrJ6$zXlo@
|
|
||||||
zFuxY$Hi42LzhDLii5Gug>4^i8#NSshUI=lk@B-x+lf2zs7&=&GJ%Aj}0*}aI1_m)z
|
|
||||||
z5N7lYQuzQBWH0gbb!C6W#4V()m9m%>=ouDUPZ!6Kid%0la{4g^2(UVS^9^9Ux#z#*
|
|
||||||
zUM9{NA)j{pe@M$Rs$BIl=U=S8$$a1WzrRdo{gl+0z}h6r5vC9^6c`^Bx}VO;+bO}5
|
|
||||||
zvNP)ZgKMjwCMdj@`|<2Y_0Pt}1ZL)gY-~-uJS@@@9A*XrISC3k4mfNWo)RazbGPm0
|
|
||||||
z-*LChSmMOL4?kJISKi=fE3%ne|KQy6WQCxi5SK$J*`YY~T7$y$*OKq5Bzd2d?Vs~N
|
|
||||||
z@B8fph5p+U-+hzde)4_q^MCH|=dNhx2_G663hFce=0A)TEv>6k&v^?1%NErV*NBpo
|
|
||||||
z#FA92<f2p{#b9J$WTtCiq-$sxVrXGyVr*q(scm3jWnhruaU&H)LvDUbW?Cg~4U(b>
|
|
||||||
RH-Q=$JYD@<);T3K0RY|)#{mEU
|
|
||||||
|
|
||||||
literal 0
|
|
||||||
HcmV?d00001
|
|
||||||
|
|
||||||
diff --git a/packages/console/app/src/asset/brand/preview-opencode-logo-light-square.png b/packages/console/app/src/asset/brand/preview-opencode-logo-light-square.png
|
|
||||||
new file mode 100644
|
|
||||||
index 0000000000000000000000000000000000000000..3964d8528440323730053e56f9d957539937b99d
|
|
||||||
GIT binary patch
|
|
||||||
literal 1467
|
|
||||||
zcmeAS@N?(olHy`uVBq!ia0y~yV4MKL9LzwG?TN?!0V$SrM_)$<hK>E)e-c@Ne1&9>
|
|
||||||
zAYTTCDm4a%h86~fUqGRT7Yq!g1`G_Z5*Qe)W-u^_7tGleXakh+3-AeX1=5Oga+pX-
|
|
||||||
zUcR`n@W%CP=g*zh*VWm(dpD2)RHh&+YpAEUZ|@!;cmKY<7tWv4*U?$OZY@wf(5P=q
|
|
||||||
zw@e07{3Stt!3+!%FaEyL69*!Rzpq|wUamVGD8-oM?e4<R!7A$k<Zu>vL>4nJh^c}w
|
|
||||||
zqi2xH2cRH(iKnkC`zt1HA#JUc#jHSIuvmGzIEGZ*dV8@wmnlGi)v?HEMncg4sk@lD
|
|
||||||
zIaT_<lxtU%8_(QxrNqqsn)KQ9%h&DxCN=JfRETIyU~LlR2pb9vn-fWv%cJ$!m?PI+
|
|
||||||
zGv4tv%TnURa`rlle{ppQ3O5coY-nsuU}iqZ#@58k!y+xgVP+tZGdv|e#VtQu_MLV6
|
|
||||||
z?L5Ea#y9-;OWOY?C_F#SfA^^jN9a(9$sv^JP@HNicjD`}`}Y4=-!3(o@cVI9<8Hfm
|
|
||||||
z&5bti(|7$Y)|v3Q^Zn-UpA;4kk?aKV*|pOO`<V}&_gU>d_YAOLQ7v(eC`m~yNwrEY
|
|
||||||
zN(E93Mg~S^x&}tNhK3=A7FH(4Rz{ZE1_o9J1{oeVQc*PI=BH$)RpQnlDVlH-sDZ)L
|
|
||||||
L)z4*}Q$iB}B`L3|
|
|
||||||
|
|
||||||
literal 0
|
|
||||||
HcmV?d00001
|
|
||||||
|
|
||||||
diff --git a/packages/console/app/src/routes/brand/index.tsx b/packages/console/app/src/routes/brand/index.tsx
|
|
||||||
index eda3c84..9140462 100644
|
|
||||||
--- a/packages/console/app/src/routes/brand/index.tsx
|
|
||||||
+++ b/packages/console/app/src/routes/brand/index.tsx
|
|
||||||
@@ -7,18 +7,24 @@ import { useI18n } from "~/context/i18n"
|
|
||||||
import { LocaleLinks } from "~/component/locale-links"
|
|
||||||
import previewLogoLight from "../../asset/brand/preview-opencode-logo-light.png"
|
|
||||||
import previewLogoDark from "../../asset/brand/preview-opencode-logo-dark.png"
|
|
||||||
+import previewLogoLightSquare from "../../asset/brand/preview-opencode-logo-light-square.png"
|
|
||||||
+import previewLogoDarkSquare from "../../asset/brand/preview-opencode-logo-dark-square.png"
|
|
||||||
import previewWordmarkLight from "../../asset/brand/preview-opencode-wordmark-light.png"
|
|
||||||
import previewWordmarkDark from "../../asset/brand/preview-opencode-wordmark-dark.png"
|
|
||||||
import previewWordmarkSimpleLight from "../../asset/brand/preview-opencode-wordmark-simple-light.png"
|
|
||||||
import previewWordmarkSimpleDark from "../../asset/brand/preview-opencode-wordmark-simple-dark.png"
|
|
||||||
import logoLightPng from "../../asset/brand/opencode-logo-light.png"
|
|
||||||
import logoDarkPng from "../../asset/brand/opencode-logo-dark.png"
|
|
||||||
+import logoLightSquarePng from "../../asset/brand/opencode-logo-light-square.png"
|
|
||||||
+import logoDarkSquarePng from "../../asset/brand/opencode-logo-dark-square.png"
|
|
||||||
import wordmarkLightPng from "../../asset/brand/opencode-wordmark-light.png"
|
|
||||||
import wordmarkDarkPng from "../../asset/brand/opencode-wordmark-dark.png"
|
|
||||||
import wordmarkSimpleLightPng from "../../asset/brand/opencode-wordmark-simple-light.png"
|
|
||||||
import wordmarkSimpleDarkPng from "../../asset/brand/opencode-wordmark-simple-dark.png"
|
|
||||||
import logoLightSvg from "../../asset/brand/opencode-logo-light.svg"
|
|
||||||
import logoDarkSvg from "../../asset/brand/opencode-logo-dark.svg"
|
|
||||||
+import logoLightSquareSvg from "../../asset/brand/opencode-logo-light-square.svg"
|
|
||||||
+import logoDarkSquareSvg from "../../asset/brand/opencode-logo-dark-square.svg"
|
|
||||||
import wordmarkLightSvg from "../../asset/brand/opencode-wordmark-light.svg"
|
|
||||||
import wordmarkDarkSvg from "../../asset/brand/opencode-wordmark-dark.svg"
|
|
||||||
import wordmarkSimpleLightSvg from "../../asset/brand/opencode-wordmark-simple-light.svg"
|
|
||||||
@@ -135,6 +141,60 @@ export default function Brand() {
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
+ <div>
|
|
||||||
+ <img src={previewLogoLightSquare} alt="OpenCode brand guidelines" />
|
|
||||||
+ <div data-component="actions">
|
|
||||||
+ <button onClick={() => downloadFile(logoLightSquarePng, "opencode-logo-light-square.png")}>
|
|
||||||
+ PNG
|
|
||||||
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
+ <path
|
|
||||||
+ d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
|
|
||||||
+ stroke="currentColor"
|
|
||||||
+ stroke-width="1.5"
|
|
||||||
+ stroke-linecap="square"
|
|
||||||
+ />
|
|
||||||
+ </svg>
|
|
||||||
+ </button>
|
|
||||||
+ <button onClick={() => downloadFile(logoLightSquareSvg, "opencode-logo-light-square.svg")}>
|
|
||||||
+ SVG
|
|
||||||
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
+ <path
|
|
||||||
+ d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
|
|
||||||
+ stroke="currentColor"
|
|
||||||
+ stroke-width="1.5"
|
|
||||||
+ stroke-linecap="square"
|
|
||||||
+ />
|
|
||||||
+ </svg>
|
|
||||||
+ </button>
|
|
||||||
+ </div>
|
|
||||||
+ </div>
|
|
||||||
+ <div>
|
|
||||||
+ <img src={previewLogoDarkSquare} alt="OpenCode brand guidelines" />
|
|
||||||
+ <div data-component="actions">
|
|
||||||
+ <button onClick={() => downloadFile(logoDarkSquarePng, "opencode-logo-dark-square.png")}>
|
|
||||||
+ PNG
|
|
||||||
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
+ <path
|
|
||||||
+ d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
|
|
||||||
+ stroke="currentColor"
|
|
||||||
+ stroke-width="1.5"
|
|
||||||
+ stroke-linecap="square"
|
|
||||||
+ />
|
|
||||||
+ </svg>
|
|
||||||
+ </button>
|
|
||||||
+ <button onClick={() => downloadFile(logoDarkSquareSvg, "opencode-logo-dark-square.svg")}>
|
|
||||||
+ SVG
|
|
||||||
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
+ <path
|
|
||||||
+ d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
|
|
||||||
+ stroke="currentColor"
|
|
||||||
+ stroke-width="1.5"
|
|
||||||
+ stroke-linecap="square"
|
|
||||||
+ />
|
|
||||||
+ </svg>
|
|
||||||
+ </button>
|
|
||||||
+ </div>
|
|
||||||
+ </div>
|
|
||||||
<div>
|
|
||||||
<img src={previewWordmarkLight} alt="OpenCode brand guidelines" />
|
|
||||||
<div data-component="actions">
|
|
||||||
--
|
|
||||||
2.39.5
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user