mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-18 14:54:17 +00:00
Compare commits
20 Commits
production
...
optimize-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8abfd71684 | ||
|
|
0ca75544ab | ||
|
|
572a037e5d | ||
|
|
ad92181fa7 | ||
|
|
c56f4aa5d8 | ||
|
|
a344a766fd | ||
|
|
bca793d064 | ||
|
|
ad3c192837 | ||
|
|
5512231ca8 | ||
|
|
bad394cd49 | ||
|
|
3b97580621 | ||
|
|
cb88fe26aa | ||
|
|
e345b89ce5 | ||
|
|
26c7b240ba | ||
|
|
d327a2b1cf | ||
|
|
c1b03b728a | ||
|
|
2a2437bf22 | ||
|
|
4ccb82e81a | ||
|
|
92912219df | ||
|
|
bab3124e8b |
@@ -69,6 +69,10 @@ Examples:
|
||||
- Provider integration issues
|
||||
- New, broken, or poor-quality models
|
||||
|
||||
#### acp
|
||||
|
||||
If the issue mentions acp support, assign acp label.
|
||||
|
||||
#### docs
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { createFocusSignal } from "@solid-primitives/active-element"
|
||||
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 { IconButton } from "@opencode-ai/ui/icon-button"
|
||||
import { Select } from "@opencode-ai/ui/select"
|
||||
import { RadioGroup } from "@opencode-ai/ui/radio-group"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { ModelSelectorPopover } from "@/components/dialog-select-model"
|
||||
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
|
||||
@@ -108,6 +109,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
let slashPopoverRef!: HTMLDivElement
|
||||
|
||||
const mirror = { input: false }
|
||||
const inset = 44
|
||||
|
||||
const scrollCursorIntoView = () => {
|
||||
const container = scrollRef
|
||||
@@ -117,7 +119,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
const range = selection.getRangeAt(0)
|
||||
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
|
||||
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
@@ -130,8 +139,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (bottom > container.scrollTop + container.clientHeight - padding) {
|
||||
container.scrollTop = bottom - container.clientHeight + padding
|
||||
if (bottom > container.scrollTop + container.clientHeight - inset) {
|
||||
container.scrollTop = bottom - container.clientHeight + inset
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +250,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
return messages.some((m) => m.role === "user")
|
||||
})
|
||||
|
||||
const MAX_HISTORY = 100
|
||||
const [history, setHistory] = persisted(
|
||||
Persist.global("prompt-history", ["prompt-history.v1"]),
|
||||
createStore<{
|
||||
@@ -311,6 +319,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
requestAnimationFrame(() => editorRef?.focus())
|
||||
}
|
||||
|
||||
const shellModeKey = "mod+shift+x"
|
||||
const normalModeKey = "mod+shift+e"
|
||||
|
||||
command.register("prompt-input", () => [
|
||||
{
|
||||
id: "file.attach",
|
||||
@@ -320,6 +331,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
disabled: store.mode !== "normal",
|
||||
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)
|
||||
@@ -1059,8 +1086,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
removeLabel={language.t("prompt.attachment.remove")}
|
||||
/>
|
||||
<div
|
||||
class="relative max-h-[240px] overflow-y-auto"
|
||||
ref={(el) => (scrollRef = el)}
|
||||
class="relative"
|
||||
onMouseDown={(e) => {
|
||||
const target = e.target
|
||||
if (!(target instanceof HTMLElement)) return
|
||||
@@ -1074,40 +1100,42 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
editorRef?.focus()
|
||||
}}
|
||||
>
|
||||
<div
|
||||
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 class="relative max-h-[240px] overflow-y-auto no-scrollbar" ref={(el) => (scrollRef = el)}>
|
||||
<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"
|
||||
classList={{ "font-mono!": store.mode === "shell" }}
|
||||
>
|
||||
{placeholder()}
|
||||
</div>
|
||||
</Show>
|
||||
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-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">
|
||||
<input
|
||||
@@ -1330,59 +1358,35 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
</TooltipKeybind>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0">
|
||||
<div
|
||||
data-component="prompt-mode-toggle"
|
||||
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"
|
||||
>
|
||||
<div
|
||||
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"
|
||||
style={{
|
||||
transform: store.mode === "shell" ? "translateX(0px)" : "translateX(calc(100% + 4px))",
|
||||
}}
|
||||
/>
|
||||
<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" }}
|
||||
<div class="shrink-0" data-component="prompt-mode-toggle">
|
||||
<RadioGroup
|
||||
options={["shell", "normal"] as const}
|
||||
current={store.mode}
|
||||
value={(mode) => mode}
|
||||
label={(mode) => (
|
||||
<TooltipKeybind
|
||||
placement="top"
|
||||
gutter={4}
|
||||
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"
|
||||
>
|
||||
<Icon
|
||||
name="console"
|
||||
name={mode === "shell" ? "console" : "prompt"}
|
||||
class="size-[18px]"
|
||||
classList={{
|
||||
"text-icon-strong-base": store.mode === "shell",
|
||||
"text-icon-weak": store.mode !== "shell",
|
||||
"text-icon-strong-base": mode === "shell" && store.mode === "shell",
|
||||
"text-icon-interactive-base": mode === "normal" && store.mode === "normal",
|
||||
"text-icon-weak": store.mode !== mode,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="relative z-10 flex-1 h-full p-0.5 flex items-center justify-center"
|
||||
aria-pressed={store.mode === "normal"}
|
||||
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>
|
||||
</TooltipKeybind>
|
||||
)}
|
||||
onSelect={(mode) => mode && setMode(mode)}
|
||||
fill
|
||||
pad="none"
|
||||
class="w-[68px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -63,6 +63,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "التبديل إلى الوكيل السابق",
|
||||
"command.model.variant.cycle": "تغيير جهد التفكير",
|
||||
"command.model.variant.cycle.description": "التبديل إلى مستوى الجهد التالي",
|
||||
"command.prompt.mode.shell": "التبديل إلى وضع Shell",
|
||||
"command.prompt.mode.normal": "التبديل إلى وضع Prompt",
|
||||
"command.permissions.autoaccept.enable": "قبول التعديلات تلقائيًا",
|
||||
"command.permissions.autoaccept.disable": "إيقاف قبول التعديلات تلقائيًا",
|
||||
"command.workspace.toggle": "تبديل مساحات العمل",
|
||||
@@ -210,6 +212,7 @@ export const dict = {
|
||||
"prompt.placeholder.summarizeComments": "لخّص التعليقات…",
|
||||
"prompt.placeholder.summarizeComment": "لخّص التعليق…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.normal": "Prompt",
|
||||
"prompt.mode.shell.exit": "esc للخروج",
|
||||
"prompt.example.1": "إصلاح TODO في قاعدة التعليمات البرمجية",
|
||||
"prompt.example.2": "ما هو المكدس التقني لهذا المشروع؟",
|
||||
|
||||
@@ -63,6 +63,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "Mudar para o agente anterior",
|
||||
"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.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.disable": "Parar de aceitar edições automaticamente",
|
||||
"command.workspace.toggle": "Alternar espaços de trabalho",
|
||||
@@ -210,6 +212,7 @@ export const dict = {
|
||||
"prompt.placeholder.summarizeComments": "Resumir comentários…",
|
||||
"prompt.placeholder.summarizeComment": "Resumir comentário…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.normal": "Prompt",
|
||||
"prompt.mode.shell.exit": "esc para sair",
|
||||
"prompt.example.1": "Corrigir um TODO no código",
|
||||
"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.model.variant.cycle": "Promijeni nivo razmišljanja",
|
||||
"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.disable": "Zaustavi automatsko prihvatanje izmjena",
|
||||
"command.workspace.toggle": "Prikaži/sakrij radne prostore",
|
||||
@@ -228,6 +230,7 @@ export const dict = {
|
||||
"prompt.placeholder.summarizeComments": "Sažmi komentare…",
|
||||
"prompt.placeholder.summarizeComment": "Sažmi komentar…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.normal": "Prompt",
|
||||
"prompt.mode.shell.exit": "esc za izlaz",
|
||||
|
||||
"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.model.variant.cycle": "Skift tænkeindsats",
|
||||
"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.disable": "Stop automatisk accept af ændringer",
|
||||
"command.workspace.toggle": "Skift arbejdsområder",
|
||||
@@ -226,6 +228,7 @@ export const dict = {
|
||||
"prompt.placeholder.summarizeComments": "Opsummér kommentarer…",
|
||||
"prompt.placeholder.summarizeComment": "Opsummér kommentar…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.normal": "Prompt",
|
||||
"prompt.mode.shell.exit": "esc for at afslutte",
|
||||
|
||||
"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.model.variant.cycle": "Denkaufwand 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.disable": "Automatische Annahme von Änderungen stoppen",
|
||||
"command.workspace.toggle": "Arbeitsbereiche umschalten",
|
||||
@@ -215,6 +217,7 @@ export const dict = {
|
||||
"prompt.placeholder.summarizeComments": "Kommentare zusammenfassen…",
|
||||
"prompt.placeholder.summarizeComment": "Kommentar zusammenfassen…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.normal": "Prompt",
|
||||
"prompt.mode.shell.exit": "esc zum Verlassen",
|
||||
"prompt.example.1": "Ein TODO in der Codebasis beheben",
|
||||
"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.model.variant.cycle": "Cycle thinking effort",
|
||||
"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.disable": "Stop auto-accepting edits",
|
||||
"command.workspace.toggle": "Toggle workspaces",
|
||||
@@ -228,6 +230,7 @@ export const dict = {
|
||||
"prompt.placeholder.summarizeComments": "Summarize comments…",
|
||||
"prompt.placeholder.summarizeComment": "Summarize comment…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.normal": "Prompt",
|
||||
"prompt.mode.shell.exit": "esc to exit",
|
||||
|
||||
"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.model.variant.cycle": "Alternar esfuerzo de pensamiento",
|
||||
"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.disable": "Dejar de aceptar ediciones automáticamente",
|
||||
"command.workspace.toggle": "Alternar espacios de trabajo",
|
||||
@@ -227,6 +229,7 @@ export const dict = {
|
||||
"prompt.placeholder.summarizeComments": "Resumir comentarios…",
|
||||
"prompt.placeholder.summarizeComment": "Resumir comentario…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.normal": "Prompt",
|
||||
"prompt.mode.shell.exit": "esc para salir",
|
||||
|
||||
"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.model.variant.cycle": "Changer l'effort de réflexion",
|
||||
"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.disable": "Arrêter l'acceptation automatique des modifications",
|
||||
"command.workspace.toggle": "Basculer les espaces de travail",
|
||||
@@ -210,6 +212,7 @@ export const dict = {
|
||||
"prompt.placeholder.summarizeComments": "Résumer les commentaires…",
|
||||
"prompt.placeholder.summarizeComment": "Résumer le commentaire…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.normal": "Prompt",
|
||||
"prompt.mode.shell.exit": "esc pour quitter",
|
||||
"prompt.example.1": "Corriger un TODO dans la base de code",
|
||||
"prompt.example.2": "Quelle est la pile technique de ce projet ?",
|
||||
|
||||
@@ -63,6 +63,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "前のエージェントに切り替え",
|
||||
"command.model.variant.cycle": "思考レベルの切り替え",
|
||||
"command.model.variant.cycle.description": "次の思考レベルに切り替え",
|
||||
"command.prompt.mode.shell": "シェルモードに切り替える",
|
||||
"command.prompt.mode.normal": "プロンプトモードに切り替える",
|
||||
"command.permissions.autoaccept.enable": "編集を自動承認",
|
||||
"command.permissions.autoaccept.disable": "編集の自動承認を停止",
|
||||
"command.workspace.toggle": "ワークスペースを切り替え",
|
||||
@@ -209,6 +211,7 @@ export const dict = {
|
||||
"prompt.placeholder.summarizeComments": "コメントを要約…",
|
||||
"prompt.placeholder.summarizeComment": "コメントを要約…",
|
||||
"prompt.mode.shell": "シェル",
|
||||
"prompt.mode.normal": "プロンプト",
|
||||
"prompt.mode.shell.exit": "escで終了",
|
||||
"prompt.example.1": "コードベースのTODOを修正",
|
||||
"prompt.example.2": "このプロジェクトの技術スタックは何ですか?",
|
||||
|
||||
@@ -67,6 +67,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "이전 에이전트로 전환",
|
||||
"command.model.variant.cycle": "생각 수준 순환",
|
||||
"command.model.variant.cycle.description": "다음 생각 수준으로 전환",
|
||||
"command.prompt.mode.shell": "셸 모드로 전환",
|
||||
"command.prompt.mode.normal": "프롬프트 모드로 전환",
|
||||
"command.permissions.autoaccept.enable": "편집 자동 수락",
|
||||
"command.permissions.autoaccept.disable": "편집 자동 수락 중지",
|
||||
"command.workspace.toggle": "작업 공간 전환",
|
||||
@@ -213,6 +215,7 @@ export const dict = {
|
||||
"prompt.placeholder.summarizeComments": "댓글 요약…",
|
||||
"prompt.placeholder.summarizeComment": "댓글 요약…",
|
||||
"prompt.mode.shell": "셸",
|
||||
"prompt.mode.normal": "프롬프트",
|
||||
"prompt.mode.shell.exit": "종료하려면 esc",
|
||||
"prompt.example.1": "코드베이스의 TODO 수정",
|
||||
"prompt.example.2": "이 프로젝트의 기술 스택이 무엇인가요?",
|
||||
|
||||
@@ -72,6 +72,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "Bytt til forrige agent",
|
||||
"command.model.variant.cycle": "Bytt tenkeinnsats",
|
||||
"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.disable": "Slutt å godta endringer automatisk",
|
||||
"command.workspace.toggle": "Veksle arbeidsområder",
|
||||
@@ -230,6 +232,7 @@ export const dict = {
|
||||
"prompt.placeholder.summarizeComments": "Oppsummer kommentarer…",
|
||||
"prompt.placeholder.summarizeComment": "Oppsummer kommentar…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.normal": "Prompt",
|
||||
"prompt.mode.shell.exit": "ESC for å avslutte",
|
||||
|
||||
"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.model.variant.cycle": "Przełącz wysiłek myślowy",
|
||||
"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.disable": "Zatrzymaj automatyczne akceptowanie edycji",
|
||||
"command.workspace.toggle": "Przełącz przestrzenie robocze",
|
||||
@@ -211,6 +213,7 @@ export const dict = {
|
||||
"prompt.placeholder.summarizeComments": "Podsumuj komentarze…",
|
||||
"prompt.placeholder.summarizeComment": "Podsumuj komentarz…",
|
||||
"prompt.mode.shell": "Terminal",
|
||||
"prompt.mode.normal": "Prompt",
|
||||
"prompt.mode.shell.exit": "esc aby wyjść",
|
||||
"prompt.example.1": "Napraw TODO w bazie kodu",
|
||||
"prompt.example.2": "Jaki jest stos technologiczny tego projektu?",
|
||||
|
||||
@@ -69,6 +69,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "Переключиться к предыдущему агенту",
|
||||
"command.model.variant.cycle": "Цикл режимов мышления",
|
||||
"command.model.variant.cycle.description": "Переключиться к следующему уровню усилий",
|
||||
"command.prompt.mode.shell": "Переключиться в режим оболочки",
|
||||
"command.prompt.mode.normal": "Переключиться в режим промпта",
|
||||
"command.permissions.autoaccept.enable": "Авто-принятие изменений",
|
||||
"command.permissions.autoaccept.disable": "Прекратить авто-принятие изменений",
|
||||
"command.workspace.toggle": "Переключить рабочие пространства",
|
||||
@@ -227,6 +229,7 @@ export const dict = {
|
||||
"prompt.placeholder.summarizeComments": "Суммировать комментарии…",
|
||||
"prompt.placeholder.summarizeComment": "Суммировать комментарий…",
|
||||
"prompt.mode.shell": "Оболочка",
|
||||
"prompt.mode.normal": "Промпт",
|
||||
"prompt.mode.shell.exit": "esc для выхода",
|
||||
|
||||
"prompt.example.1": "Исправить TODO в коде",
|
||||
|
||||
@@ -69,6 +69,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "สลับไปยังเอเจนต์ก่อนหน้า",
|
||||
"command.model.variant.cycle": "เปลี่ยนความพยายามในการคิด",
|
||||
"command.model.variant.cycle.description": "สลับไปยังระดับความพยายามถัดไป",
|
||||
"command.prompt.mode.shell": "สลับไปยังโหมดเชลล์",
|
||||
"command.prompt.mode.normal": "สลับไปยังโหมดพรอมต์",
|
||||
"command.permissions.autoaccept.enable": "ยอมรับการแก้ไขโดยอัตโนมัติ",
|
||||
"command.permissions.autoaccept.disable": "หยุดยอมรับการแก้ไขโดยอัตโนมัติ",
|
||||
"command.workspace.toggle": "สลับพื้นที่ทำงาน",
|
||||
@@ -227,6 +229,7 @@ export const dict = {
|
||||
"prompt.placeholder.summarizeComments": "สรุปความคิดเห็น…",
|
||||
"prompt.placeholder.summarizeComment": "สรุปความคิดเห็น…",
|
||||
"prompt.mode.shell": "เชลล์",
|
||||
"prompt.mode.normal": "พรอมต์",
|
||||
"prompt.mode.shell.exit": "กด esc เพื่อออก",
|
||||
|
||||
"prompt.example.1": "แก้ไข TODO ในโค้ดเบส",
|
||||
|
||||
@@ -93,6 +93,9 @@ export const dict = {
|
||||
"command.model.variant.cycle": "切换思考强度",
|
||||
"command.model.variant.cycle.description": "切换到下一个强度等级",
|
||||
|
||||
"command.prompt.mode.shell": "切换到 Shell 模式",
|
||||
"command.prompt.mode.normal": "切换到 Prompt 模式",
|
||||
|
||||
"command.permissions.autoaccept.enable": "自动接受编辑",
|
||||
"command.permissions.autoaccept.disable": "停止自动接受编辑",
|
||||
|
||||
@@ -248,6 +251,7 @@ export const dict = {
|
||||
"prompt.placeholder.summarizeComments": "总结评论…",
|
||||
"prompt.placeholder.summarizeComment": "总结该评论…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.normal": "Prompt",
|
||||
"prompt.mode.shell.exit": "按 esc 退出",
|
||||
"prompt.example.1": "修复代码库中的一个 TODO",
|
||||
"prompt.example.2": "这个项目的技术栈是什么?",
|
||||
|
||||
@@ -73,6 +73,8 @@ export const dict = {
|
||||
"command.agent.cycle.reverse.description": "切換到上一個代理程式",
|
||||
"command.model.variant.cycle": "循環思考強度",
|
||||
"command.model.variant.cycle.description": "切換到下一個強度等級",
|
||||
"command.prompt.mode.shell": "切換到 Shell 模式",
|
||||
"command.prompt.mode.normal": "切換到 Prompt 模式",
|
||||
"command.permissions.autoaccept.enable": "自動接受編輯",
|
||||
"command.permissions.autoaccept.disable": "停止自動接受編輯",
|
||||
"command.workspace.toggle": "切換工作區",
|
||||
@@ -227,6 +229,7 @@ export const dict = {
|
||||
"prompt.placeholder.summarizeComments": "摘要評論…",
|
||||
"prompt.placeholder.summarizeComment": "摘要這則評論…",
|
||||
"prompt.mode.shell": "Shell",
|
||||
"prompt.mode.normal": "Prompt",
|
||||
"prompt.mode.shell.exit": "按 esc 退出",
|
||||
|
||||
"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,
|
||||
"w-full": 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,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -159,6 +159,38 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string):
|
||||
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({
|
||||
command: "auth",
|
||||
describe: "manage credentials",
|
||||
@@ -277,6 +309,13 @@ export const AuthLoginCommand = cmd({
|
||||
openrouter: 5,
|
||||
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({
|
||||
message: "Select provider",
|
||||
maxItems: 8,
|
||||
@@ -298,6 +337,11 @@ export const AuthLoginCommand = cmd({
|
||||
}[x.id],
|
||||
})),
|
||||
),
|
||||
...pluginProviders.map((x) => ({
|
||||
label: x.name,
|
||||
value: x.id,
|
||||
hint: "plugin",
|
||||
})),
|
||||
{
|
||||
value: "other",
|
||||
label: "Other",
|
||||
|
||||
@@ -18,7 +18,7 @@ export const ExportCommand = cmd({
|
||||
handler: async (args) => {
|
||||
await bootstrap(process.cwd(), async () => {
|
||||
let sessionID = args.sessionID
|
||||
process.stderr.write(`Exporting session: ${sessionID ?? "latest"}`)
|
||||
process.stderr.write(`Exporting session: ${sessionID ?? "latest"}\n`)
|
||||
|
||||
if (!sessionID) {
|
||||
UI.empty()
|
||||
|
||||
@@ -65,7 +65,15 @@ function EditBody(props: { request: PermissionRequest }) {
|
||||
return (
|
||||
<box flexDirection="column" gap={1}>
|
||||
<Show when={diff()}>
|
||||
<scrollbox height="100%">
|
||||
<scrollbox
|
||||
height="100%"
|
||||
verticalScrollbarOptions={{
|
||||
trackOptions: {
|
||||
backgroundColor: theme.background,
|
||||
foregroundColor: theme.borderActive,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<diff
|
||||
diff={diff()}
|
||||
view={view()}
|
||||
|
||||
@@ -80,7 +80,15 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
|
||||
paddingRight={2}
|
||||
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 paddingRight={1}>
|
||||
<text fg={theme.text}>
|
||||
|
||||
@@ -184,5 +184,6 @@ export const TuiThreadCommand = cmd({
|
||||
} finally {
|
||||
unguard?.()
|
||||
}
|
||||
process.exit(0)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -137,7 +137,12 @@ export const rpc = {
|
||||
async shutdown() {
|
||||
Log.Default.info("worker shutting down")
|
||||
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)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import z from "zod"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs/promises"
|
||||
import { readFileSync } from "fs"
|
||||
import { Log } from "../util/log"
|
||||
|
||||
export namespace Patch {
|
||||
@@ -308,16 +307,18 @@ export namespace Patch {
|
||||
content: string
|
||||
}
|
||||
|
||||
export function deriveNewContentsFromChunks(filePath: string, chunks: UpdateFileChunk[]): ApplyPatchFileUpdate {
|
||||
// Read original file content
|
||||
let originalContent: string
|
||||
try {
|
||||
originalContent = readFileSync(filePath, "utf-8")
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to read file ${filePath}: ${error}`)
|
||||
}
|
||||
export async function deriveNewContentsFromChunks(
|
||||
filePath: string,
|
||||
chunks: UpdateFileChunk[],
|
||||
originalContent?: string,
|
||||
): Promise<ApplyPatchFileUpdate> {
|
||||
const content =
|
||||
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
|
||||
if (originalLines.length > 0 && originalLines[originalLines.length - 1] === "") {
|
||||
@@ -335,7 +336,7 @@ export namespace Patch {
|
||||
const newContent = newLines.join("\n")
|
||||
|
||||
// Generate unified diff
|
||||
const unifiedDiff = generateUnifiedDiff(originalContent, newContent)
|
||||
const unifiedDiff = generateUnifiedDiff(content, newContent)
|
||||
|
||||
return {
|
||||
unified_diff: unifiedDiff,
|
||||
@@ -545,7 +546,7 @@ export namespace Patch {
|
||||
break
|
||||
|
||||
case "update":
|
||||
const fileUpdate = deriveNewContentsFromChunks(hunk.path, hunk.chunks)
|
||||
const fileUpdate = await deriveNewContentsFromChunks(hunk.path, hunk.chunks)
|
||||
|
||||
if (hunk.move_path) {
|
||||
// Handle file move
|
||||
@@ -641,7 +642,7 @@ export namespace Patch {
|
||||
case "update":
|
||||
const updatePath = path.resolve(effectiveCwd, hunk.path)
|
||||
try {
|
||||
const fileUpdate = deriveNewContentsFromChunks(updatePath, hunk.chunks)
|
||||
const fileUpdate = await deriveNewContentsFromChunks(updatePath, hunk.chunks)
|
||||
changes.set(resolvedPath, {
|
||||
type: "update",
|
||||
unified_diff: fileUpdate.unified_diff,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -579,7 +579,7 @@ export namespace Session {
|
||||
})
|
||||
|
||||
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
|
||||
Database.use((db) => {
|
||||
db.insert(MessageTable)
|
||||
|
||||
@@ -101,7 +101,7 @@ export const ApplyPatchTool = Tool.define("apply_patch", {
|
||||
|
||||
// Apply the update chunks to get new content
|
||||
try {
|
||||
const fileUpdate = Patch.deriveNewContentsFromChunks(filePath, hunk.chunks)
|
||||
const fileUpdate = await Patch.deriveNewContentsFromChunks(filePath, hunk.chunks, oldContent)
|
||||
newContent = fileUpdate.content
|
||||
} catch (error) {
|
||||
throw new Error(`apply_patch verification failed: ${error}`)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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 { createInterface } from "readline"
|
||||
import { Tool } from "./tool"
|
||||
import { LSP } from "../lsp"
|
||||
import { FileTime } from "../file/time"
|
||||
@@ -11,7 +13,9 @@ import { InstructionPrompt } from "../session/instruction"
|
||||
|
||||
const DEFAULT_READ_LIMIT = 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_LABEL = `${MAX_BYTES / 1024} KB`
|
||||
|
||||
export const ReadTool = Tool.define("read", {
|
||||
description: DESCRIPTION,
|
||||
@@ -49,14 +53,18 @@ export const ReadTool = Tool.define("read", {
|
||||
const dir = path.dirname(filepath)
|
||||
const base = path.basename(filepath)
|
||||
|
||||
const dirEntries = fs.readdirSync(dir)
|
||||
const suggestions = dirEntries
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.toLowerCase().includes(base.toLowerCase()) || base.toLowerCase().includes(entry.toLowerCase()),
|
||||
const suggestions = await fs
|
||||
.readdir(dir)
|
||||
.then((entries) =>
|
||||
entries
|
||||
.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))
|
||||
.slice(0, 3)
|
||||
.catch(() => [])
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
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()) {
|
||||
const dirents = await fs.promises.readdir(filepath, { withFileTypes: true })
|
||||
const dirents = await fs.readdir(filepath, { withFileTypes: true })
|
||||
const entries = await Promise.all(
|
||||
dirents.map(async (dirent) => {
|
||||
if (dirent.isDirectory()) return dirent.name + "/"
|
||||
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 + "/"
|
||||
}
|
||||
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}`)
|
||||
|
||||
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 offset = params.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[] = []
|
||||
let bytes = 0
|
||||
let lines = 0
|
||||
let truncatedByBytes = false
|
||||
for (let i = start; i < Math.min(lines.length, start + limit); i++) {
|
||||
const line = lines[i].length > MAX_LINE_LENGTH ? lines[i].substring(0, MAX_LINE_LENGTH) + "..." : lines[i]
|
||||
const size = Buffer.byteLength(line, "utf-8") + (raw.length > 0 ? 1 : 0)
|
||||
if (bytes + size > MAX_BYTES) {
|
||||
truncatedByBytes = true
|
||||
break
|
||||
let hasMoreLines = false
|
||||
try {
|
||||
for await (const text of rl) {
|
||||
lines += 1
|
||||
if (lines <= start) continue
|
||||
|
||||
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)
|
||||
bytes += size
|
||||
} finally {
|
||||
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) => {
|
||||
@@ -165,15 +199,15 @@ export const ReadTool = Tool.define("read", {
|
||||
let output = [`<path>${filepath}</path>`, `<type>file</type>`, "<content>"].join("\n")
|
||||
output += content.join("\n")
|
||||
|
||||
const totalLines = lines.length
|
||||
const totalLines = lines
|
||||
const lastReadLine = offset + raw.length - 1
|
||||
const hasMoreLines = totalLines > lastReadLine
|
||||
const nextOffset = lastReadLine + 1
|
||||
const truncated = hasMoreLines || 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) {
|
||||
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 {
|
||||
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()
|
||||
// binary check for common non-text extensions
|
||||
switch (ext) {
|
||||
@@ -236,22 +270,25 @@ async function isBinaryFile(filepath: string, file: Bun.BunFile): Promise<boolea
|
||||
break
|
||||
}
|
||||
|
||||
const stat = await file.stat()
|
||||
const fileSize = stat.size
|
||||
if (fileSize === 0) return false
|
||||
|
||||
const bufferSize = Math.min(4096, fileSize)
|
||||
const buffer = await file.arrayBuffer()
|
||||
if (buffer.byteLength === 0) return false
|
||||
const bytes = new Uint8Array(buffer.slice(0, bufferSize))
|
||||
const fh = await fs.open(filepath, "r")
|
||||
try {
|
||||
const sampleSize = Math.min(4096, fileSize)
|
||||
const bytes = Buffer.alloc(sampleSize)
|
||||
const result = await fh.read(bytes, 0, sampleSize, 0)
|
||||
if (result.bytesRead === 0) return false
|
||||
|
||||
let nonPrintableCount = 0
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
if (bytes[i] === 0) return true
|
||||
if (bytes[i] < 9 || (bytes[i] > 13 && bytes[i] < 32)) {
|
||||
nonPrintableCount++
|
||||
let nonPrintableCount = 0
|
||||
for (let i = 0; i < result.bytesRead; i++) {
|
||||
if (bytes[i] === 0) return true
|
||||
if (bytes[i] < 9 || (bytes[i] > 13 && bytes[i] < 32)) {
|
||||
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
|
||||
}
|
||||
|
||||
120
packages/opencode/test/cli/plugin-auth-picker.test.ts
Normal file
120
packages/opencode/test/cli/plugin-auth-picker.test.ts
Normal file
@@ -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 result = await read.execute({ filePath: path.join(tmp.path, "large.json") }, ctx)
|
||||
expect(result.metadata.truncated).toBe(true)
|
||||
expect(result.output).toContain("Output truncated at")
|
||||
expect(result.output).toContain("bytes")
|
||||
expect(result.output).toContain("Output capped at")
|
||||
expect(result.output).toContain("Use offset=")
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -230,7 +230,8 @@ describe("tool.read truncation", () => {
|
||||
const read = await ReadTool.init()
|
||||
const result = await read.execute({ filePath: path.join(tmp.path, "many-lines.txt"), limit: 10 }, ctx)
|
||||
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("line9")
|
||||
expect(result.output).not.toContain("line10")
|
||||
@@ -267,6 +268,10 @@ describe("tool.read truncation", () => {
|
||||
fn: async () => {
|
||||
const read = await ReadTool.init()
|
||||
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("line14")
|
||||
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 () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
@@ -324,7 +363,7 @@ describe("tool.read truncation", () => {
|
||||
fn: async () => {
|
||||
const read = await ReadTool.init()
|
||||
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)
|
||||
},
|
||||
})
|
||||
@@ -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",
|
||||
)
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -117,6 +117,7 @@ function createThrottledValue(getValue: () => string) {
|
||||
createEffect(() => {
|
||||
const next = getValue()
|
||||
const now = Date.now()
|
||||
|
||||
const remaining = TEXT_RENDER_THROTTLE_MS - (now - last)
|
||||
if (remaining <= 0) {
|
||||
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 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 {
|
||||
return part.type === "tool" && CONTEXT_GROUP_TOOLS.has(part.tool)
|
||||
@@ -390,6 +511,8 @@ export function AssistantMessageDisplay(props: {
|
||||
}
|
||||
|
||||
parts.forEach((part, index) => {
|
||||
if (!renderable(part)) return
|
||||
|
||||
if (isContextGroupTool(part)) {
|
||||
if (start < 0) start = index
|
||||
return
|
||||
@@ -408,31 +531,43 @@ export function AssistantMessageDisplay(props: {
|
||||
<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
|
||||
})
|
||||
return (
|
||||
<Show when={item()}>
|
||||
{(value) => {
|
||||
const entry = value()
|
||||
if (entry.type === "context") return <ContextToolGroup parts={entry.parts} />
|
||||
return (
|
||||
<>
|
||||
<Show when={ctx()}>{(entry) => <ContextToolGroup parts={entry().parts} />}</Show>
|
||||
<Show when={part()}>
|
||||
{(entry) => (
|
||||
<Part
|
||||
part={entry.part}
|
||||
part={entry().part}
|
||||
message={props.message}
|
||||
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
)}
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
)
|
||||
}
|
||||
|
||||
function ContextToolGroup(props: { parts: ToolPart[] }) {
|
||||
function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) {
|
||||
const i18n = useI18n()
|
||||
const [open, setOpen] = createSignal(false)
|
||||
const pending = createMemo(() =>
|
||||
props.parts.some((part) => part.state.status === "pending" || part.state.status === "running"),
|
||||
const pending = createMemo(
|
||||
() =>
|
||||
!!props.busy || props.parts.some((part) => part.state.status === "pending" || part.state.status === "running"),
|
||||
)
|
||||
const summary = createMemo(() => contextToolSummary(props.parts))
|
||||
const details = createMemo(() => summary().join(", "))
|
||||
@@ -445,7 +580,7 @@ function ContextToolGroup(props: { parts: ToolPart[] }) {
|
||||
when={pending()}
|
||||
fallback={
|
||||
<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}>
|
||||
<span data-slot="context-tool-group-summary">{details()}</span>
|
||||
</Show>
|
||||
|
||||
@@ -1,85 +1,121 @@
|
||||
[data-component="radio-group"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--spacing) * 2);
|
||||
--radio-group-height: 28px;
|
||||
--radio-group-gap: 4px;
|
||||
--radio-group-padding: 2px;
|
||||
|
||||
display: inline-flex;
|
||||
|
||||
[data-slot="radio-group-wrapper"] {
|
||||
all: unset;
|
||||
background-color: var(--surface-base);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-xs-border);
|
||||
background-color: var(--surface-inset-base);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-xxs-border);
|
||||
display: inline-flex;
|
||||
height: var(--radio-group-height);
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
&[data-fill] [data-slot="radio-group-wrapper"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-slot="radio-group-items"] {
|
||||
display: inline-flex;
|
||||
list-style: none;
|
||||
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"] {
|
||||
background: var(--button-secondary-base);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface-raised-stronger-non-alpha);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-xs-border);
|
||||
content: "";
|
||||
opacity: var(--indicator-opacity, 1);
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
transition:
|
||||
opacity 300ms ease-in-out,
|
||||
opacity 200ms ease-out,
|
||||
box-shadow 100ms ease-in-out,
|
||||
width 150ms ease,
|
||||
height 150ms ease,
|
||||
transform 150ms ease;
|
||||
width 200ms ease-out,
|
||||
height 200ms ease-out,
|
||||
transform 200ms ease-out;
|
||||
will-change: transform;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
[data-slot="radio-group-item"] {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Separator between items */
|
||||
[data-slot="radio-group-item"]:not(:first-of-type)::before {
|
||||
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);
|
||||
&[data-fill] [data-slot="radio-group-item"] {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Hide separator when item or previous item is checked */
|
||||
[data-slot="radio-group-item"]:has([data-slot="radio-group-item-input"][data-checked])::before,
|
||||
[data-slot="radio-group-item"]:has([data-slot="radio-group-item-input"][data-checked])
|
||||
+ [data-slot="radio-group-item"]::before {
|
||||
opacity: 0;
|
||||
[data-slot="radio-group-item-input"] {
|
||||
border-width: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
[data-slot="radio-group-item-label"] {
|
||||
color: var(--text-weak);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: calc(var(--spacing) * 1);
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
line-height: 1;
|
||||
padding: 6px 12px;
|
||||
place-content: center;
|
||||
padding: var(--radio-group-padding);
|
||||
position: relative;
|
||||
transition-duration: 150ms;
|
||||
transition-property: color, opacity;
|
||||
transition-timing-function: ease-in-out;
|
||||
transition:
|
||||
color 200ms ease-out,
|
||||
opacity 200ms ease-out;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
[data-slot="radio-group-item-input"] {
|
||||
all: unset;
|
||||
[data-slot="radio-group-item-control"] {
|
||||
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 */
|
||||
@@ -87,28 +123,26 @@
|
||||
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 */
|
||||
[data-slot="radio-group-item-input"][data-disabled] + [data-slot="radio-group-item-label"] {
|
||||
cursor: not-allowed;
|
||||
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 */
|
||||
[data-slot="radio-group-wrapper"]:has([data-slot="radio-group-item-input"]:focus-visible)
|
||||
[data-slot="radio-group-indicator"] {
|
||||
@@ -126,27 +160,23 @@
|
||||
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 */
|
||||
&[data-size="small"] {
|
||||
--radio-group-height: 24px;
|
||||
--radio-group-gap: 3px;
|
||||
--radio-group-padding: 2px;
|
||||
|
||||
[data-slot="radio-group-item-label"] {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
[data-slot="radio-group-item"]:not(:first-of-type)::before {
|
||||
inset: 4px 0;
|
||||
[data-slot="radio-group-item-control"] {
|
||||
padding: var(--radio-group-control-padding, 0 8px);
|
||||
}
|
||||
}
|
||||
|
||||
&[aria-orientation="vertical"] [data-slot="radio-group-item"]:not(:first-of-type)::before {
|
||||
inset: 0 4px;
|
||||
}
|
||||
&[data-size="small"][data-pad="normal"] {
|
||||
--radio-group-control-padding: 0 8px;
|
||||
}
|
||||
|
||||
/* Disabled root state */
|
||||
|
||||
@@ -15,6 +15,8 @@ export type RadioGroupProps<T> = Omit<
|
||||
class?: ComponentProps<"div">["class"]
|
||||
classList?: ComponentProps<"div">["classList"]
|
||||
size?: "small" | "medium"
|
||||
fill?: boolean
|
||||
pad?: "none" | "normal"
|
||||
}
|
||||
|
||||
export function RadioGroup<T>(props: RadioGroupProps<T>) {
|
||||
@@ -28,6 +30,8 @@ export function RadioGroup<T>(props: RadioGroupProps<T>) {
|
||||
"label",
|
||||
"onSelect",
|
||||
"size",
|
||||
"fill",
|
||||
"pad",
|
||||
])
|
||||
|
||||
const getValue = (item: T): string => {
|
||||
@@ -49,6 +53,8 @@ export function RadioGroup<T>(props: RadioGroupProps<T>) {
|
||||
{...others}
|
||||
data-component="radio-group"
|
||||
data-size={local.size ?? "medium"}
|
||||
data-fill={local.fill ? "" : undefined}
|
||||
data-pad={local.pad ?? "normal"}
|
||||
classList={{
|
||||
...(local.classList ?? {}),
|
||||
[local.class ?? ""]: !!local.class,
|
||||
@@ -62,9 +68,11 @@ export function RadioGroup<T>(props: RadioGroupProps<T>) {
|
||||
<div role="presentation" data-slot="radio-group-items">
|
||||
<For each={local.options}>
|
||||
{(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.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>
|
||||
)}
|
||||
</For>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Binary } from "@opencode-ai/util/binary"
|
||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||
import { createMemo, createSignal, For, ParentProps, Show } from "solid-js"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import { Message } from "./message-part"
|
||||
import { AssistantParts, Message } from "./message-part"
|
||||
import { Card } from "./card"
|
||||
import { Collapsible } from "./collapsible"
|
||||
import { DiffChanges } from "./diff-changes"
|
||||
@@ -91,13 +91,6 @@ function visible(part: PartType) {
|
||||
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(
|
||||
props: ParentProps<{
|
||||
sessionID: string
|
||||
@@ -237,8 +230,7 @@ export function SessionTurn(
|
||||
const working = createMemo(() => status().type !== "idle" && isLastUserMessage())
|
||||
|
||||
const assistantCopyPartID = createMemo(() => {
|
||||
if (!isLastUserMessage()) return null
|
||||
if (status().type !== "idle") return null
|
||||
if (working()) return null
|
||||
return showAssistantCopyPartID() ?? null
|
||||
})
|
||||
const assistantVisible = createMemo(() =>
|
||||
@@ -281,14 +273,11 @@ export function SessionTurn(
|
||||
</Show>
|
||||
<Show when={assistantMessages().length > 0}>
|
||||
<div data-slot="session-turn-assistant-content" aria-hidden={working()}>
|
||||
<For each={assistantMessages()}>
|
||||
{(assistantMessage) => (
|
||||
<AssistantMessageItem
|
||||
message={assistantMessage}
|
||||
showAssistantCopyPartID={assistantCopyPartID()}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
<AssistantParts
|
||||
messages={assistantMessages()}
|
||||
showAssistantCopyPartID={assistantCopyPartID()}
|
||||
working={working()}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={edited() > 0}>
|
||||
|
||||
@@ -33,7 +33,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "تفويض العمل",
|
||||
"ui.sessionTurn.status.planning": "تخطيط الخطوات التالية",
|
||||
"ui.sessionTurn.status.gatheringContext": "جمع السياق",
|
||||
"ui.sessionTurn.status.gatheringContext": "استكشاف...",
|
||||
"ui.sessionTurn.status.gatheredContext": "تم الاستكشاف",
|
||||
"ui.sessionTurn.status.searchingCodebase": "البحث في قاعدة التعليمات البرمجية",
|
||||
"ui.sessionTurn.status.searchingWeb": "البحث في الويب",
|
||||
"ui.sessionTurn.status.makingEdits": "إجراء تعديلات",
|
||||
|
||||
@@ -33,7 +33,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "Delegando trabalho",
|
||||
"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.searchingWeb": "Pesquisando na web",
|
||||
"ui.sessionTurn.status.makingEdits": "Fazendo edições",
|
||||
|
||||
@@ -37,7 +37,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "Delegiranje posla",
|
||||
"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.searchingWeb": "Pretraživanje weba",
|
||||
"ui.sessionTurn.status.makingEdits": "Pravljenje izmjena",
|
||||
|
||||
@@ -32,7 +32,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "Delegerer arbejde",
|
||||
"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.searchingWeb": "Søger på nettet",
|
||||
"ui.sessionTurn.status.makingEdits": "Laver ændringer",
|
||||
|
||||
@@ -36,7 +36,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "Arbeit delegieren",
|
||||
"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.searchingWeb": "Web durchsuchen",
|
||||
"ui.sessionTurn.status.makingEdits": "Änderungen vornehmen",
|
||||
|
||||
@@ -33,7 +33,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "Delegating work",
|
||||
"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.searchingWeb": "Searching the web",
|
||||
"ui.sessionTurn.status.makingEdits": "Making edits",
|
||||
|
||||
@@ -33,7 +33,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "Delegando trabajo",
|
||||
"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.searchingWeb": "Buscando en la web",
|
||||
"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.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.searchingWeb": "Recherche sur le web",
|
||||
"ui.sessionTurn.status.makingEdits": "Application des modifications",
|
||||
|
||||
@@ -32,7 +32,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "作業を委任中",
|
||||
"ui.sessionTurn.status.planning": "次のステップを計画中",
|
||||
"ui.sessionTurn.status.gatheringContext": "コンテキストを収集中",
|
||||
"ui.sessionTurn.status.gatheringContext": "探索中...",
|
||||
"ui.sessionTurn.status.gatheredContext": "探索済み",
|
||||
"ui.sessionTurn.status.searchingCodebase": "コードベースを検索中",
|
||||
"ui.sessionTurn.status.searchingWeb": "ウェブを検索中",
|
||||
"ui.sessionTurn.status.makingEdits": "編集を実行中",
|
||||
|
||||
@@ -33,7 +33,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "작업 위임 중",
|
||||
"ui.sessionTurn.status.planning": "다음 단계 계획 중",
|
||||
"ui.sessionTurn.status.gatheringContext": "컨텍스트 수집 중",
|
||||
"ui.sessionTurn.status.gatheringContext": "탐색 중...",
|
||||
"ui.sessionTurn.status.gatheredContext": "탐색됨",
|
||||
"ui.sessionTurn.status.searchingCodebase": "코드베이스 검색 중",
|
||||
"ui.sessionTurn.status.searchingWeb": "웹 검색 중",
|
||||
"ui.sessionTurn.status.makingEdits": "편집 수행 중",
|
||||
|
||||
@@ -36,7 +36,8 @@ export const dict: Record<Keys, string> = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "Delegerer arbeid",
|
||||
"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.searchingWeb": "Søker på nettet",
|
||||
"ui.sessionTurn.status.makingEdits": "Gjør endringer",
|
||||
|
||||
@@ -32,7 +32,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "Delegowanie pracy",
|
||||
"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.searchingWeb": "Przeszukiwanie sieci",
|
||||
"ui.sessionTurn.status.makingEdits": "Wprowadzanie zmian",
|
||||
|
||||
@@ -32,7 +32,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "Делегирование работы",
|
||||
"ui.sessionTurn.status.planning": "Планирование следующих шагов",
|
||||
"ui.sessionTurn.status.gatheringContext": "Сбор контекста",
|
||||
"ui.sessionTurn.status.gatheringContext": "Исследование...",
|
||||
"ui.sessionTurn.status.gatheredContext": "Исследовано",
|
||||
"ui.sessionTurn.status.searchingCodebase": "Поиск в кодовой базе",
|
||||
"ui.sessionTurn.status.searchingWeb": "Поиск в интернете",
|
||||
"ui.sessionTurn.status.makingEdits": "Внесение изменений",
|
||||
|
||||
@@ -33,7 +33,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "มอบหมายงาน",
|
||||
"ui.sessionTurn.status.planning": "วางแผนขั้นตอนถัดไป",
|
||||
"ui.sessionTurn.status.gatheringContext": "รวบรวมบริบท",
|
||||
"ui.sessionTurn.status.gatheringContext": "กำลังสำรวจ...",
|
||||
"ui.sessionTurn.status.gatheredContext": "สำรวจแล้ว",
|
||||
"ui.sessionTurn.status.searchingCodebase": "กำลังค้นหาโค้ดเบส",
|
||||
"ui.sessionTurn.status.searchingWeb": "กำลังค้นหาบนเว็บ",
|
||||
"ui.sessionTurn.status.makingEdits": "กำลังแก้ไข",
|
||||
|
||||
@@ -37,7 +37,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "正在委派工作",
|
||||
"ui.sessionTurn.status.planning": "正在规划下一步",
|
||||
"ui.sessionTurn.status.gatheringContext": "正在收集上下文",
|
||||
"ui.sessionTurn.status.gatheringContext": "正在探索...",
|
||||
"ui.sessionTurn.status.gatheredContext": "已探索",
|
||||
"ui.sessionTurn.status.searchingCodebase": "正在搜索代码库",
|
||||
"ui.sessionTurn.status.searchingWeb": "正在搜索网页",
|
||||
"ui.sessionTurn.status.makingEdits": "正在修改",
|
||||
|
||||
@@ -37,7 +37,8 @@ export const dict = {
|
||||
|
||||
"ui.sessionTurn.status.delegating": "正在委派工作",
|
||||
"ui.sessionTurn.status.planning": "正在規劃下一步",
|
||||
"ui.sessionTurn.status.gatheringContext": "正在收集上下文",
|
||||
"ui.sessionTurn.status.gatheringContext": "正在探索...",
|
||||
"ui.sessionTurn.status.gatheredContext": "已探索",
|
||||
"ui.sessionTurn.status.searchingCodebase": "正在搜尋程式碼庫",
|
||||
"ui.sessionTurn.status.searchingWeb": "正在搜尋網頁",
|
||||
"ui.sessionTurn.status.makingEdits": "正在修改",
|
||||
|
||||
@@ -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