diff --git a/bun.lock b/bun.lock index 70e142747..1d789519a 100644 --- a/bun.lock +++ b/bun.lock @@ -381,7 +381,7 @@ }, "packages/tfcode": { "name": "@toothfairyai/tfcode", - "version": "1.0.22", + "version": "1.0.23", "bin": { "tfcode": "./bin/tfcode", }, diff --git a/packages/tfcode/package.json b/packages/tfcode/package.json index d28f7ebc5..e26ca849a 100644 --- a/packages/tfcode/package.json +++ b/packages/tfcode/package.json @@ -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", diff --git a/packages/tfcode/src/cli/cmd/tui/routes/home.tsx b/packages/tfcode/src/cli/cmd/tui/routes/home.tsx index e76e165b2..91273250f 100644 --- a/packages/tfcode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/tfcode/src/cli/cmd/tui/routes/home.tsx @@ -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(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() { + + + + + ! + + + Update available: v{updateAvailable()} + + /changelog + + {directory()} diff --git a/packages/tfcode/src/cli/upgrade.ts b/packages/tfcode/src/cli/upgrade.ts index e40750a2e..7acc4fa27 100644 --- a/packages/tfcode/src/cli/upgrade.ts +++ b/packages/tfcode/src/cli/upgrade.ts @@ -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) diff --git a/packages/tfcode/src/installation/index.ts b/packages/tfcode/src/installation/index.ts index 9b345c4df..8d3b70f5a 100644 --- a/packages/tfcode/src/installation/index.ts +++ b/packages/tfcode/src/installation/index.ts @@ -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}` }) diff --git a/packages/tfcode/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts b/packages/tfcode/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts index 72e8e25df..a2228943d 100644 --- a/packages/tfcode/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts +++ b/packages/tfcode/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts @@ -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 | undefined), + ...(opts.copilot as Record | 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 }