mirror of
https://github.com/sst/opencode.git
synced 2025-07-07 16:14:59 +00:00
Merge branch 'dev' into fix-cancle-compact
This commit is contained in:
commit
0055a5104a
11 changed files with 151 additions and 153 deletions
2
STATS.md
2
STATS.md
|
@ -8,3 +8,5 @@
|
|||
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
|
||||
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
|
||||
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
|
||||
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
|
||||
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
|
||||
|
|
|
@ -35,6 +35,15 @@ export const UpgradeCommand = {
|
|||
}
|
||||
prompts.log.info("Using method: " + method)
|
||||
const target = args.target ?? (await Installation.latest())
|
||||
|
||||
if (Installation.VERSION === target) {
|
||||
prompts.log.warn(
|
||||
`opencode upgrade skipped: ${target} is already installed`,
|
||||
)
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
|
||||
prompts.log.info(`From ${Installation.VERSION} → ${target}`)
|
||||
const spinner = prompts.spinner()
|
||||
spinner.start("Upgrading...")
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { App } from "../app/app"
|
||||
import { Log } from "../util/log"
|
||||
|
||||
export namespace FileTime {
|
||||
const log = Log.create({ service: "file.time" })
|
||||
export const state = App.state("tool.filetimes", () => {
|
||||
const read: {
|
||||
[sessionID: string]: {
|
||||
|
@ -13,6 +15,7 @@ export namespace FileTime {
|
|||
})
|
||||
|
||||
export function read(sessionID: string, file: string) {
|
||||
log.info("read", { sessionID, file })
|
||||
const { read } = state()
|
||||
read[sessionID] = read[sessionID] || {}
|
||||
read[sessionID][file] = new Date()
|
||||
|
|
|
@ -66,6 +66,7 @@ export namespace LSPClient {
|
|||
log.info("sending initialize", { id: serverID })
|
||||
await withTimeout(
|
||||
connection.sendRequest("initialize", {
|
||||
rootUri: "file://" + app.path.cwd,
|
||||
processId: server.process.pid,
|
||||
workspaceFolders: [
|
||||
{
|
||||
|
|
|
@ -17,6 +17,9 @@ export namespace ProviderTransform {
|
|||
anthropic: {
|
||||
cacheControl: { type: "ephemeral" },
|
||||
},
|
||||
openaiCompatible: {
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,41 +1,42 @@
|
|||
import path from "node:path"
|
||||
import { App } from "../app/app"
|
||||
import { Identifier } from "../id/id"
|
||||
import { Storage } from "../storage/storage"
|
||||
import { Log } from "../util/log"
|
||||
import { Decimal } from "decimal.js"
|
||||
import { z, ZodSchema } from "zod"
|
||||
import {
|
||||
generateText,
|
||||
LoadAPIKeyError,
|
||||
convertToCoreMessages,
|
||||
streamText,
|
||||
tool,
|
||||
wrapLanguageModel,
|
||||
type Tool as AITool,
|
||||
type LanguageModelUsage,
|
||||
type CoreMessage,
|
||||
type UIMessage,
|
||||
type ProviderMetadata,
|
||||
wrapLanguageModel,
|
||||
type Attachment,
|
||||
} from "ai"
|
||||
import { z, ZodSchema } from "zod"
|
||||
import { Decimal } from "decimal.js"
|
||||
|
||||
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
|
||||
|
||||
import { Share } from "../share/share"
|
||||
import { Message } from "./message"
|
||||
import { App } from "../app/app"
|
||||
import { Bus } from "../bus"
|
||||
import { Provider } from "../provider/provider"
|
||||
import { MCP } from "../mcp"
|
||||
import { NamedError } from "../util/error"
|
||||
import type { Tool } from "../tool/tool"
|
||||
import { SystemPrompt } from "./system"
|
||||
import { Flag } from "../flag/flag"
|
||||
import type { ModelsDev } from "../provider/models"
|
||||
import { Installation } from "../installation"
|
||||
import { Config } from "../config/config"
|
||||
import { Flag } from "../flag/flag"
|
||||
import { Identifier } from "../id/id"
|
||||
import { Installation } from "../installation"
|
||||
import { MCP } from "../mcp"
|
||||
import { Provider } from "../provider/provider"
|
||||
import { ProviderTransform } from "../provider/transform"
|
||||
import type { ModelsDev } from "../provider/models"
|
||||
import { Share } from "../share/share"
|
||||
import { Snapshot } from "../snapshot"
|
||||
import { Storage } from "../storage/storage"
|
||||
import type { Tool } from "../tool/tool"
|
||||
import { Log } from "../util/log"
|
||||
import { NamedError } from "../util/error"
|
||||
import { Message } from "./message"
|
||||
import { SystemPrompt } from "./system"
|
||||
import { FileTime } from "../file/time"
|
||||
|
||||
export namespace Session {
|
||||
const log = Log.create({ service: "session" })
|
||||
|
@ -362,35 +363,59 @@ export namespace Session {
|
|||
|
||||
const app = App.info()
|
||||
input.parts = await Promise.all(
|
||||
input.parts.map(async (part) => {
|
||||
input.parts.map(async (part): Promise<Message.MessagePart[]> => {
|
||||
if (part.type === "file") {
|
||||
const url = new URL(part.url)
|
||||
switch (url.protocol) {
|
||||
case "file:":
|
||||
let content = await Bun.file(
|
||||
path.join(app.path.cwd, url.pathname),
|
||||
).text()
|
||||
const range = {
|
||||
start: url.searchParams.get("start"),
|
||||
end: url.searchParams.get("end"),
|
||||
}
|
||||
if (range.start != null && part.mediaType === "text/plain") {
|
||||
const lines = content.split("\n")
|
||||
const start = parseInt(range.start)
|
||||
const end = range.end ? parseInt(range.end) : lines.length
|
||||
content = lines.slice(start, end).join("\n")
|
||||
}
|
||||
return {
|
||||
type: "file",
|
||||
url: `data:${part.mediaType};base64,` + btoa(content),
|
||||
mediaType: part.mediaType,
|
||||
filename: part.filename,
|
||||
const filepath = path.join(app.path.cwd, url.pathname)
|
||||
let file = Bun.file(filepath)
|
||||
|
||||
if (part.mediaType === "text/plain") {
|
||||
let text = await file.text()
|
||||
const range = {
|
||||
start: url.searchParams.get("start"),
|
||||
end: url.searchParams.get("end"),
|
||||
}
|
||||
if (range.start != null && part.mediaType === "text/plain") {
|
||||
const lines = text.split("\n")
|
||||
const start = parseInt(range.start)
|
||||
const end = range.end ? parseInt(range.end) : lines.length
|
||||
text = lines.slice(start, end).join("\n")
|
||||
}
|
||||
FileTime.read(input.sessionID, filepath)
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
text: [
|
||||
"Called the Read tool on " + url.pathname,
|
||||
"<results>",
|
||||
text,
|
||||
"</results>",
|
||||
].join("\n"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
text: ["Called the Read tool on " + url.pathname].join("\n"),
|
||||
},
|
||||
{
|
||||
type: "file",
|
||||
url:
|
||||
`data:${part.mediaType};base64,` +
|
||||
Buffer.from(await file.bytes()).toString("base64url"),
|
||||
mediaType: part.mediaType,
|
||||
filename: part.filename!,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
return part
|
||||
return [part]
|
||||
}),
|
||||
)
|
||||
).then((x) => x.flat())
|
||||
if (msgs.length === 0 && !session.parentID) {
|
||||
generateText({
|
||||
maxTokens: input.providerID === "google" ? 1024 : 20,
|
||||
|
|
|
@ -92,7 +92,6 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
// Now, insert the attachment at the position where the '@' was.
|
||||
// The cursor is now at `atIndex` after the replacement.
|
||||
filePath := msg.CompletionValue
|
||||
fileName := filepath.Base(filePath)
|
||||
extension := filepath.Ext(filePath)
|
||||
mediaType := ""
|
||||
switch extension {
|
||||
|
@ -107,7 +106,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
}
|
||||
attachment := &textarea.Attachment{
|
||||
ID: uuid.NewString(),
|
||||
Display: "@" + fileName,
|
||||
Display: "@" + filePath,
|
||||
URL: fmt.Sprintf("file://./%s", filePath),
|
||||
Filename: filePath,
|
||||
MediaType: mediaType,
|
||||
|
|
|
@ -68,11 +68,9 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
m.selectedPart = -1
|
||||
return m, nil
|
||||
case app.OptimisticMessageAddedMsg:
|
||||
m.renderView(m.width)
|
||||
if m.tail {
|
||||
m.viewport.GotoBottom()
|
||||
}
|
||||
return m, nil
|
||||
m.tail = true
|
||||
m.rendering = true
|
||||
return m, m.Reload()
|
||||
case dialog.ThemeSelectedMsg:
|
||||
m.cache.Clear()
|
||||
m.rendering = true
|
||||
|
@ -134,6 +132,7 @@ func (m *messagesComponent) renderView(width int) {
|
|||
|
||||
switch message.Role {
|
||||
case opencode.MessageRoleUser:
|
||||
userLoop:
|
||||
for partIndex, part := range message.Parts {
|
||||
switch part := part.AsUnion().(type) {
|
||||
case opencode.TextPart:
|
||||
|
@ -195,6 +194,8 @@ func (m *messagesComponent) renderView(width int) {
|
|||
m = m.updateSelected(content, part.Text)
|
||||
blocks = append(blocks, content)
|
||||
}
|
||||
// Only render the first text part
|
||||
break userLoop
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1512,7 +1512,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
|||
m.transposeLeft()
|
||||
|
||||
default:
|
||||
m.insertRunesFromUserInput([]rune{msg.Code})
|
||||
m.insertRunesFromUserInput([]rune(msg.Text))
|
||||
}
|
||||
|
||||
case pasteMsg:
|
||||
|
|
|
@ -243,6 +243,44 @@ function getStatusText(status: [Status, string?]): string {
|
|||
}
|
||||
}
|
||||
|
||||
function checkOverflow(getEl: () => HTMLElement | undefined, watch?: () => any) {
|
||||
const [needsToggle, setNeedsToggle] = createSignal(false)
|
||||
|
||||
function measure() {
|
||||
const el = getEl()
|
||||
if (!el) return
|
||||
setNeedsToggle(el.scrollHeight > el.clientHeight + 1)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
let raf = 0
|
||||
|
||||
function probe() {
|
||||
const el = getEl()
|
||||
if (el && el.offsetParent !== null && el.getBoundingClientRect().height) {
|
||||
measure()
|
||||
}
|
||||
else {
|
||||
raf = requestAnimationFrame(probe)
|
||||
}
|
||||
}
|
||||
raf = requestAnimationFrame(probe)
|
||||
|
||||
const ro = new ResizeObserver(measure)
|
||||
const el = getEl()
|
||||
if (el) ro.observe(el)
|
||||
|
||||
onCleanup(() => {
|
||||
cancelAnimationFrame(raf)
|
||||
ro.disconnect()
|
||||
})
|
||||
})
|
||||
|
||||
if (watch) createEffect(measure)
|
||||
|
||||
return needsToggle
|
||||
}
|
||||
|
||||
function ProviderIcon(props: { provider: string; size?: number }) {
|
||||
const size = props.size || 16
|
||||
return (
|
||||
|
@ -296,34 +334,11 @@ interface TextPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|||
expand?: boolean
|
||||
}
|
||||
function TextPart(props: TextPartProps) {
|
||||
const [local, rest] = splitProps(props, [
|
||||
"text",
|
||||
"expand",
|
||||
])
|
||||
const [expanded, setExpanded] = createSignal(false)
|
||||
const [overflowed, setOverflowed] = createSignal(false)
|
||||
let preEl: HTMLPreElement | undefined
|
||||
|
||||
function checkOverflow() {
|
||||
if (preEl && !local.expand) {
|
||||
setOverflowed(preEl.scrollHeight > preEl.clientHeight + 1)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
checkOverflow()
|
||||
window.addEventListener("resize", checkOverflow)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
local.text
|
||||
local.expand
|
||||
setTimeout(checkOverflow, 0)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("resize", checkOverflow)
|
||||
})
|
||||
const [local, rest] = splitProps(props, ["text", "expand"])
|
||||
const [expanded, setExpanded] = createSignal(false)
|
||||
const overflowed = checkOverflow(() => preEl, () => local.expand)
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -331,7 +346,7 @@ function TextPart(props: TextPartProps) {
|
|||
data-expanded={expanded() || local.expand === true}
|
||||
{...rest}
|
||||
>
|
||||
<pre ref={(el) => (preEl = el)}>{local.text}</pre>
|
||||
<pre ref={preEl}>{local.text}</pre>
|
||||
{((!local.expand && overflowed()) || expanded()) && (
|
||||
<button
|
||||
type="button"
|
||||
|
@ -349,31 +364,11 @@ interface ErrorPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|||
expand?: boolean
|
||||
}
|
||||
function ErrorPart(props: ErrorPartProps) {
|
||||
let preEl: HTMLDivElement | undefined
|
||||
|
||||
const [local, rest] = splitProps(props, ["expand", "children"])
|
||||
const [expanded, setExpanded] = createSignal(false)
|
||||
const [overflowed, setOverflowed] = createSignal(false)
|
||||
let preEl: HTMLElement | undefined
|
||||
|
||||
function checkOverflow() {
|
||||
if (preEl && !local.expand) {
|
||||
setOverflowed(preEl.scrollHeight > preEl.clientHeight + 1)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
checkOverflow()
|
||||
window.addEventListener("resize", checkOverflow)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
local.children
|
||||
local.expand
|
||||
setTimeout(checkOverflow, 0)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("resize", checkOverflow)
|
||||
})
|
||||
const overflowed = checkOverflow(() => preEl, () => local.expand)
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -381,7 +376,7 @@ function ErrorPart(props: ErrorPartProps) {
|
|||
data-expanded={expanded() || local.expand === true}
|
||||
{...rest}
|
||||
>
|
||||
<div data-section="content" ref={(el) => (preEl = el)}>
|
||||
<div data-section="content" ref={preEl}>
|
||||
{local.children}
|
||||
</div>
|
||||
{((!local.expand && overflowed()) || expanded()) && (
|
||||
|
@ -403,31 +398,11 @@ interface MarkdownPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|||
highlight?: boolean
|
||||
}
|
||||
function MarkdownPart(props: MarkdownPartProps) {
|
||||
const [local, rest] = splitProps(props, ["text", "expand", "highlight"])
|
||||
const [expanded, setExpanded] = createSignal(false)
|
||||
const [overflowed, setOverflowed] = createSignal(false)
|
||||
let divEl: HTMLDivElement | undefined
|
||||
|
||||
function checkOverflow() {
|
||||
if (divEl && !local.expand) {
|
||||
setOverflowed(divEl.scrollHeight > divEl.clientHeight + 1)
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
checkOverflow()
|
||||
window.addEventListener("resize", checkOverflow)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
local.text
|
||||
local.expand
|
||||
setTimeout(checkOverflow, 0)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("resize", checkOverflow)
|
||||
})
|
||||
const [local, rest] = splitProps(props, ["text", "expand", "highlight"])
|
||||
const [expanded, setExpanded] = createSignal(false)
|
||||
const overflowed = checkOverflow(() => divEl, () => local.expand)
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -469,36 +444,16 @@ function TerminalPart(props: TerminalPartProps) {
|
|||
"desc",
|
||||
"expand",
|
||||
])
|
||||
let preEl: HTMLDivElement | undefined
|
||||
|
||||
const [expanded, setExpanded] = createSignal(false)
|
||||
const [overflowed, setOverflowed] = createSignal(false)
|
||||
let preEl: HTMLElement | undefined
|
||||
|
||||
function checkOverflow() {
|
||||
if (!preEl) return
|
||||
|
||||
const code = preEl.getElementsByTagName("code")[0]
|
||||
|
||||
if (code && !local.expand) {
|
||||
setOverflowed(preEl.clientHeight < code.offsetHeight)
|
||||
}
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
local.command
|
||||
local.result
|
||||
local.error
|
||||
local.expand
|
||||
setTimeout(checkOverflow, 0)
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
checkOverflow()
|
||||
window.addEventListener("resize", checkOverflow)
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
window.removeEventListener("resize", checkOverflow)
|
||||
})
|
||||
const overflowed = checkOverflow(
|
||||
() => {
|
||||
if (!preEl) return
|
||||
return preEl.getElementsByTagName("pre")[0]
|
||||
},
|
||||
() => local.expand
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -515,16 +470,16 @@ function TerminalPart(props: TerminalPartProps) {
|
|||
<Switch>
|
||||
<Match when={local.error}>
|
||||
<CodeBlock
|
||||
data-section="error"
|
||||
ref={preEl}
|
||||
lang="text"
|
||||
ref={(el) => (preEl = el)}
|
||||
data-section="error"
|
||||
code={local.error || ""}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={local.result}>
|
||||
<CodeBlock
|
||||
ref={preEl}
|
||||
lang="console"
|
||||
ref={(el) => (preEl = el)}
|
||||
code={local.result || ""}
|
||||
/>
|
||||
</Match>
|
||||
|
|
|
@ -253,7 +253,7 @@
|
|||
line-height: 18px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--sl-color-text-secondary);
|
||||
max-width: var(--sm-tool-width);
|
||||
max-width: var(--md-tool-width);
|
||||
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue