feat: models

This commit is contained in:
Gab
2026-03-30 18:59:09 +11:00
parent 43048f89c5
commit ffc99307a8
6 changed files with 88 additions and 33 deletions

View File

@@ -381,7 +381,7 @@
},
"packages/tfcode": {
"name": "@toothfairyai/tfcode",
"version": "1.0.22",
"version": "1.0.23",
"bin": {
"tfcode": "./bin/tfcode",
},

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.0.22",
"version": "1.0.23",
"name": "@toothfairyai/tfcode",
"type": "module",
"license": "MIT",

View File

@@ -1,5 +1,5 @@
import { Prompt, type PromptRef } from "@tui/component/prompt"
import { createEffect, createMemo, Match, on, onMount, Show, Switch } from "solid-js"
import { createEffect, createMemo, Match, on, onMount, Show, Switch, createSignal } from "solid-js"
import { useTheme } from "@tui/context/theme"
import { useKeybind } from "@tui/context/keybind"
import { Logo } from "../component/logo"
@@ -15,6 +15,10 @@ import { Installation } from "@/installation"
import { useKV } from "../context/kv"
import { useCommandDialog } from "../component/dialog-command"
import { useLocal } from "../context/local"
import { useSDK } from "../context/sdk"
import semver from "semver"
import { TextAttributes } from "@opentui/core"
import { Flag } from "@/flag/flag"
// TODO: what is the best way to do this?
let once = false
@@ -26,6 +30,7 @@ export function Home() {
const route = useRouteData("home")
const promptRef = usePromptRef()
const command = useCommandDialog()
const sdk = useSDK()
const mcp = createMemo(() => Object.keys(sync.data.mcp).length > 0)
const mcpError = createMemo(() => {
return Object.values(sync.data.mcp).some((x) => x.status === "failed")
@@ -43,6 +48,19 @@ export function Home() {
return !tipsHidden()
})
const [updateAvailable, setUpdateAvailable] = createSignal<string | null>(null)
const skippedVersion = createMemo(() => kv.get("skipped_version"))
onMount(() => {
const unsub = sdk.event.on("installation.update-available", (evt) => {
const version = evt.properties.version
const skipped = skippedVersion()
if (skipped && !semver.gt(version, skipped)) return
setUpdateAvailable(version)
})
return unsub
})
command.register(() => [
{
title: tipsHidden() ? "Show tips" : "Hide tips",
@@ -132,6 +150,27 @@ export function Home() {
<box flexGrow={1} minHeight={0} />
<Toast />
</box>
<Show when={updateAvailable()}>
<box
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
paddingRight={2}
flexDirection="row"
flexShrink={0}
gap={2}
alignItems="center"
>
<box backgroundColor={theme.warning} paddingLeft={1} paddingRight={1} paddingTop={0} paddingBottom={0}>
<text fg={theme.background} attributes={TextAttributes.BOLD}>
!
</text>
</box>
<text fg={theme.text}>Update available: v{updateAvailable()}</text>
<box flexGrow={1} />
<text fg={theme.textMuted}>/changelog</text>
</box>
</Show>
<box paddingTop={1} paddingBottom={1} paddingLeft={2} paddingRight={2} flexDirection="row" flexShrink={0} gap={2}>
<text fg={theme.textMuted}>{directory()}</text>
<box gap={1} flexDirection="row" flexShrink={0}>

View File

@@ -15,6 +15,7 @@ export async function upgrade() {
}
if (Installation.VERSION === latest) return
if (config.autoupdate === false || Flag.OPENCODE_DISABLE_AUTOUPDATE) return
const kind = Installation.getReleaseType(Installation.VERSION, latest)

View File

@@ -62,7 +62,7 @@ export namespace Installation {
export const VERSION = typeof OPENCODE_VERSION === "string" ? OPENCODE_VERSION : "local"
export const CHANNEL = typeof OPENCODE_CHANNEL === "string" ? OPENCODE_CHANNEL : "local"
export const USER_AGENT = `opencode/${CHANNEL}/${VERSION}/${Flag.OPENCODE_CLIENT}`
export const USER_AGENT = `tfcode/${CHANNEL}/${VERSION}/${Flag.OPENCODE_CLIENT}`
export function isPreview() {
return CHANNEL !== "latest"
@@ -141,16 +141,16 @@ export namespace Installation {
)
const getBrewFormula = Effect.fnUntraced(function* () {
const tapFormula = yield* text(["brew", "list", "--formula", "anomalyco/tap/opencode"])
if (tapFormula.includes("opencode")) return "anomalyco/tap/opencode"
const coreFormula = yield* text(["brew", "list", "--formula", "opencode"])
if (coreFormula.includes("opencode")) return "opencode"
return "opencode"
const tapFormula = yield* text(["brew", "list", "--formula", "toothfairyai/tap/tfcode"])
if (tapFormula.includes("tfcode")) return "toothfairyai/tap/tfcode"
const coreFormula = yield* text(["brew", "list", "--formula", "tfcode"])
if (coreFormula.includes("tfcode")) return "tfcode"
return "tfcode"
})
const upgradeCurl = Effect.fnUntraced(
function* (target: string) {
const response = yield* httpOk.execute(HttpClientRequest.get("https://opencode.ai/install"))
const response = yield* httpOk.execute(HttpClientRequest.get("https://toothfairyai.com/install"))
const body = yield* response.text
const bodyBytes = new TextEncoder().encode(body)
const proc = ChildProcess.make("bash", [], {
@@ -180,9 +180,9 @@ export namespace Installation {
{ name: "yarn", command: () => text(["yarn", "global", "list"]) },
{ name: "pnpm", command: () => text(["pnpm", "list", "-g", "--depth=0"]) },
{ name: "bun", command: () => text(["bun", "pm", "ls", "-g"]) },
{ name: "brew", command: () => text(["brew", "list", "--formula", "opencode"]) },
{ name: "scoop", command: () => text(["scoop", "list", "opencode"]) },
{ name: "choco", command: () => text(["choco", "list", "--limit-output", "opencode"]) },
{ name: "brew", command: () => text(["brew", "list", "--formula", "tfcode"]) },
{ name: "scoop", command: () => text(["scoop", "list", "tfcode"]) },
{ name: "choco", command: () => text(["choco", "list", "--limit-output", "tfcode"]) },
]
checks.sort((a, b) => {
@@ -196,7 +196,9 @@ export namespace Installation {
for (const check of checks) {
const output = yield* check.command()
const installedName =
check.name === "brew" || check.name === "choco" || check.name === "scoop" ? "opencode" : "opencode-ai"
check.name === "brew" || check.name === "choco" || check.name === "scoop"
? "tfcode"
: "@toothfairyai/tfcode"
if (output.includes(installedName)) {
return check.name
}
@@ -216,7 +218,7 @@ export namespace Installation {
return info.formulae[0].versions.stable
}
const response = yield* httpOk.execute(
HttpClientRequest.get("https://formulae.brew.sh/api/formula/opencode.json").pipe(
HttpClientRequest.get("https://formulae.brew.sh/api/formula/tfcode.json").pipe(
HttpClientRequest.acceptJson,
),
)
@@ -230,7 +232,7 @@ export namespace Installation {
const registry = reg.endsWith("/") ? reg.slice(0, -1) : reg
const channel = CHANNEL
const response = yield* httpOk.execute(
HttpClientRequest.get(`${registry}/opencode-ai/${channel}`).pipe(HttpClientRequest.acceptJson),
HttpClientRequest.get(`${registry}/@toothfairyai/tfcode/${channel}`).pipe(HttpClientRequest.acceptJson),
)
const data = yield* HttpClientResponse.schemaBodyJson(NpmPackage)(response)
return data.version
@@ -239,7 +241,7 @@ export namespace Installation {
if (detectedMethod === "choco") {
const response = yield* httpOk.execute(
HttpClientRequest.get(
"https://community.chocolatey.org/api/v2/Packages?$filter=Id%20eq%20%27opencode%27%20and%20IsLatestVersion&$select=Version",
"https://community.chocolatey.org/api/v2/Packages?$filter=Id%20eq%20%27tfcode%27%20and%20IsLatestVersion&$select=Version",
).pipe(HttpClientRequest.setHeaders({ Accept: "application/json;odata=verbose" })),
)
const data = yield* HttpClientResponse.schemaBodyJson(ChocoPackage)(response)
@@ -249,7 +251,7 @@ export namespace Installation {
if (detectedMethod === "scoop") {
const response = yield* httpOk.execute(
HttpClientRequest.get(
"https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/opencode.json",
"https://raw.githubusercontent.com/ScoopInstaller/Main/master/bucket/tfcode.json",
).pipe(HttpClientRequest.setHeaders({ Accept: "application/json" })),
)
const data = yield* HttpClientResponse.schemaBodyJson(ScoopManifest)(response)
@@ -257,7 +259,7 @@ export namespace Installation {
}
const response = yield* httpOk.execute(
HttpClientRequest.get("https://api.github.com/repos/anomalyco/opencode/releases/latest").pipe(
HttpClientRequest.get("https://api.github.com/repos/ToothFairyAI/tfcode/releases/latest").pipe(
HttpClientRequest.acceptJson,
),
)
@@ -272,24 +274,24 @@ export namespace Installation {
result = yield* upgradeCurl(target)
break
case "npm":
result = yield* run(["npm", "install", "-g", `opencode-ai@${target}`])
result = yield* run(["npm", "install", "-g", `@toothfairyai/tfcode@${target}`])
break
case "pnpm":
result = yield* run(["pnpm", "install", "-g", `opencode-ai@${target}`])
result = yield* run(["pnpm", "install", "-g", `@toothfairyai/tfcode@${target}`])
break
case "bun":
result = yield* run(["bun", "install", "-g", `opencode-ai@${target}`])
result = yield* run(["bun", "install", "-g", `@toothfairyai/tfcode@${target}`])
break
case "brew": {
const formula = yield* getBrewFormula()
const env = { HOMEBREW_NO_AUTO_UPDATE: "1" }
if (formula.includes("/")) {
const tap = yield* run(["brew", "tap", "anomalyco/tap"], { env })
const tap = yield* run(["brew", "tap", "toothfairyai/tap"], { env })
if (tap.code !== 0) {
result = tap
break
}
const repo = yield* text(["brew", "--repo", "anomalyco/tap"])
const repo = yield* text(["brew", "--repo", "toothfairyai/tap"])
const dir = repo.trim()
if (dir) {
const pull = yield* run(["git", "pull", "--ff-only"], { cwd: dir, env })
@@ -303,10 +305,10 @@ export namespace Installation {
break
}
case "choco":
result = yield* run(["choco", "upgrade", "opencode", `--version=${target}`, "-y"])
result = yield* run(["choco", "upgrade", "tfcode", `--version=${target}`, "-y"])
break
case "scoop":
result = yield* run(["scoop", "install", `opencode@${target}`])
result = yield* run(["scoop", "install", `tfcode@${target}`])
break
default:
return yield* new UpgradeFailedError({ stderr: `Unknown method: ${m}` })

View File

@@ -7,7 +7,11 @@ import type { OpenAICompatibleChatPrompt } from "./openai-compatible-api-types"
import { convertToBase64 } from "@ai-sdk/provider-utils"
function getOpenAIMetadata(message: { providerOptions?: SharedV2ProviderMetadata }) {
return message?.providerOptions?.copilot ?? {}
const opts = message?.providerOptions ?? {}
return {
...(opts.openaiCompatible as Record<string, unknown> | undefined),
...(opts.copilot as Record<string, unknown> | undefined),
}
}
export function convertToOpenAICompatibleChatMessages(prompt: LanguageModelV2Prompt): OpenAICompatibleChatPrompt {
@@ -82,7 +86,6 @@ export function convertToOpenAICompatibleChatMessages(prompt: LanguageModelV2Pro
for (const part of content) {
const partMetadata = getOpenAIMetadata(part)
// Check for reasoningOpaque on any part (may be attached to text/tool-call)
const partOpaque = (part.providerOptions as { copilot?: { reasoningOpaque?: string } })?.copilot
?.reasoningOpaque
if (partOpaque && !reasoningOpaque) {
@@ -113,15 +116,25 @@ export function convertToOpenAICompatibleChatMessages(prompt: LanguageModelV2Pro
}
}
messages.push({
const msg: any = {
role: "assistant",
content: text || undefined,
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
reasoning_text: reasoningOpaque ? reasoningText : undefined,
reasoning_opaque: reasoningOpaque,
reasoning_content: reasoningText,
...metadata,
})
}
if (metadata?.reasoning_content) {
msg.reasoning_content = metadata.reasoning_content
} else if (reasoningText) {
msg.reasoning_content = reasoningText
}
if (reasoningOpaque) {
msg.reasoning_opaque = reasoningOpaque
if (reasoningText) msg.reasoning_text = reasoningText
}
messages.push(msg)
break
}