mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-20 15:54:21 +00:00
Compare commits
8 Commits
update-sta
...
github-v1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2410593023 | ||
|
|
1de12604cf | ||
|
|
ac0b37a7b7 | ||
|
|
7e1051af07 | ||
|
|
93615bef28 | ||
|
|
a04e4e81fb | ||
|
|
296250f1b7 | ||
|
|
443214871e |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -27,3 +27,4 @@ target
|
||||
opencode-dev
|
||||
logs/
|
||||
*.bun-build
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
30
bun.lock
30
bun.lock
@@ -25,7 +25,7 @@
|
||||
},
|
||||
"packages/app": {
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -75,7 +75,7 @@
|
||||
},
|
||||
"packages/console/app": {
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"dependencies": {
|
||||
"@cloudflare/vite-plugin": "1.15.2",
|
||||
"@ibm/plex": "6.4.1",
|
||||
@@ -109,7 +109,7 @@
|
||||
},
|
||||
"packages/console/core": {
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sts": "3.782.0",
|
||||
"@jsx-email/render": "1.1.1",
|
||||
@@ -136,7 +136,7 @@
|
||||
},
|
||||
"packages/console/function": {
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "2.0.0",
|
||||
"@ai-sdk/openai": "2.0.2",
|
||||
@@ -160,7 +160,7 @@
|
||||
},
|
||||
"packages/console/mail": {
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
@@ -184,7 +184,7 @@
|
||||
},
|
||||
"packages/desktop": {
|
||||
"name": "@opencode-ai/desktop",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"dependencies": {
|
||||
"@opencode-ai/app": "workspace:*",
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
@@ -217,7 +217,7 @@
|
||||
},
|
||||
"packages/enterprise": {
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"dependencies": {
|
||||
"@opencode-ai/ui": "workspace:*",
|
||||
"@opencode-ai/util": "workspace:*",
|
||||
@@ -246,7 +246,7 @@
|
||||
},
|
||||
"packages/function": {
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"dependencies": {
|
||||
"@octokit/auth-app": "8.0.1",
|
||||
"@octokit/rest": "catalog:",
|
||||
@@ -262,7 +262,7 @@
|
||||
},
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
@@ -376,7 +376,7 @@
|
||||
},
|
||||
"packages/plugin": {
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"zod": "catalog:",
|
||||
@@ -396,7 +396,7 @@
|
||||
},
|
||||
"packages/sdk/js": {
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"devDependencies": {
|
||||
"@hey-api/openapi-ts": "0.90.10",
|
||||
"@tsconfig/node22": "catalog:",
|
||||
@@ -407,7 +407,7 @@
|
||||
},
|
||||
"packages/slack": {
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"dependencies": {
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
"@slack/bolt": "^3.17.1",
|
||||
@@ -420,7 +420,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"dependencies": {
|
||||
"@kobalte/core": "catalog:",
|
||||
"@opencode-ai/sdk": "workspace:*",
|
||||
@@ -462,7 +462,7 @@
|
||||
},
|
||||
"packages/util": {
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
},
|
||||
@@ -473,7 +473,7 @@
|
||||
},
|
||||
"packages/web": {
|
||||
"name": "@opencode-ai/web",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"dependencies": {
|
||||
"@astrojs/cloudflare": "12.6.3",
|
||||
"@astrojs/markdown-remark": "6.3.1",
|
||||
|
||||
@@ -30,6 +30,10 @@ inputs:
|
||||
description: "Comma-separated list of trigger phrases (case-insensitive). Defaults to '/opencode,/oc'"
|
||||
required: false
|
||||
|
||||
variant:
|
||||
description: "Model variant for provider-specific reasoning effort (e.g., high, max, minimal)"
|
||||
required: false
|
||||
|
||||
oidc_base_url:
|
||||
description: "Base URL for OIDC token exchange API. Only required when running a custom GitHub App install. Defaults to https://api.opencode.ai"
|
||||
required: false
|
||||
@@ -71,4 +75,5 @@ runs:
|
||||
PROMPT: ${{ inputs.prompt }}
|
||||
USE_GITHUB_TOKEN: ${{ inputs.use_github_token }}
|
||||
MENTIONS: ${{ inputs.mentions }}
|
||||
VARIANT: ${{ inputs.variant }}
|
||||
OIDC_BASE_URL: ${{ inputs.oidc_base_url }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -491,34 +491,19 @@ render(() => {
|
||||
// Gate component that waits for the server to be ready
|
||||
function ServerGate(props: { children: (data: ServerReadyData) => JSX.Element }) {
|
||||
const [serverData] = createResource(() => commands.awaitInitialization(new Channel<InitStep>() as any))
|
||||
if (serverData.state === "errored") throw serverData.error
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={serverData.state !== "errored"}
|
||||
when={serverData.state !== "pending" && serverData()}
|
||||
fallback={
|
||||
<div class="h-screen w-screen flex flex-col items-center justify-center bg-background-base gap-4">
|
||||
<Splash class="w-16 h-20 opacity-50" />
|
||||
<div class="max-w-md px-4 text-center">
|
||||
<p class="text-sm font-medium text-red-400">Failed to start server</p>
|
||||
<p class="mt-2 text-xs text-zinc-400 break-words whitespace-pre-wrap">
|
||||
{String(serverData.error ?? "Unknown error")}
|
||||
</p>
|
||||
</div>
|
||||
<div class="h-screen w-screen flex flex-col items-center justify-center bg-background-base">
|
||||
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||
<div data-tauri-decorum-tb class="flex flex-row absolute top-0 right-0 z-10 h-10" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={serverData.state !== "pending" && serverData()}
|
||||
fallback={
|
||||
<div class="h-screen w-screen flex flex-col items-center justify-center bg-background-base">
|
||||
<Splash class="w-16 h-20 opacity-50 animate-pulse" />
|
||||
<div data-tauri-decorum-tb class="flex flex-row absolute top-0 right-0 z-10 h-10" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{(data) => props.children(data())}
|
||||
</Show>
|
||||
{(data) => props.children(data())}
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.2.9"
|
||||
version = "1.2.10"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/anomalyco/opencode"
|
||||
@@ -11,26 +11,26 @@ name = "OpenCode"
|
||||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.9/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.10/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.9/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.10/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.9/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.10/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.9/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.10/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.9/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.2.10/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -450,6 +450,7 @@ export const GithubRunCommand = cmd({
|
||||
const isWorkflowDispatchEvent = context.eventName === "workflow_dispatch"
|
||||
|
||||
const { providerID, modelID } = normalizeModel()
|
||||
const variant = process.env["VARIANT"] || undefined
|
||||
const runId = normalizeRunId()
|
||||
const share = normalizeShare()
|
||||
const oidcBaseUrl = normalizeOidcBaseUrl()
|
||||
@@ -912,6 +913,7 @@ export const GithubRunCommand = cmd({
|
||||
const result = await SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
messageID: Identifier.ascending("message"),
|
||||
variant,
|
||||
model: {
|
||||
providerID,
|
||||
modelID,
|
||||
@@ -965,6 +967,7 @@ export const GithubRunCommand = cmd({
|
||||
const summary = await SessionPrompt.prompt({
|
||||
sessionID: session.id,
|
||||
messageID: Identifier.ascending("message"),
|
||||
variant,
|
||||
model: {
|
||||
providerID,
|
||||
modelID,
|
||||
|
||||
@@ -6,10 +6,6 @@ import { Database } from "../../storage/db"
|
||||
import { SessionTable } from "../../session/session.sql"
|
||||
import { Project } from "../../project/project"
|
||||
import { Instance } from "../../project/instance"
|
||||
import { inArray } from "drizzle-orm"
|
||||
import { MessageTable, PartTable } from "../../session/session.sql"
|
||||
import type { MessageV2 } from "../../session/message-v2"
|
||||
import { and, eq, gte } from "drizzle-orm"
|
||||
|
||||
interface SessionStats {
|
||||
totalSessions: number
|
||||
@@ -24,7 +20,7 @@ interface SessionStats {
|
||||
write: number
|
||||
}
|
||||
}
|
||||
toolUsage: Record<string, { calls: number; errors: number }>
|
||||
toolUsage: Record<string, number>
|
||||
modelUsage: Record<
|
||||
string,
|
||||
{
|
||||
@@ -38,7 +34,6 @@ interface SessionStats {
|
||||
}
|
||||
}
|
||||
cost: number
|
||||
toolUsage: Record<string, { calls: number; errors: number }>
|
||||
}
|
||||
>
|
||||
dateRange: {
|
||||
@@ -67,11 +62,6 @@ export const StatsCommand = cmd({
|
||||
.option("models", {
|
||||
describe: "show model statistics (default: hidden). Pass a number to show top N, otherwise shows all",
|
||||
})
|
||||
.option("model", {
|
||||
describe: "filter models to show (can be used multiple times)",
|
||||
type: "array",
|
||||
string: true,
|
||||
})
|
||||
.option("project", {
|
||||
describe: "filter by project (default: all projects, empty string: current project)",
|
||||
type: "string",
|
||||
@@ -82,20 +72,13 @@ export const StatsCommand = cmd({
|
||||
const stats = await aggregateSessionStats(args.days, args.project)
|
||||
|
||||
let modelLimit: number | undefined
|
||||
let modelFilter: string[] | undefined
|
||||
|
||||
if (args.models === true) {
|
||||
modelLimit = Infinity
|
||||
} else if (typeof args.models === "number") {
|
||||
modelLimit = args.models
|
||||
}
|
||||
|
||||
if (args.model && args.model.length > 0) {
|
||||
modelFilter = args.model as string[]
|
||||
modelLimit = modelLimit ?? Infinity
|
||||
}
|
||||
|
||||
displayStats(stats, args.tools, modelLimit, modelFilter)
|
||||
displayStats(stats, args.tools, modelLimit)
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -103,7 +86,14 @@ export const StatsCommand = cmd({
|
||||
async function getCurrentProject(): Promise<Project.Info> {
|
||||
return Instance.project
|
||||
}
|
||||
|
||||
async function getAllSessions(): Promise<Session.Info[]> {
|
||||
const rows = Database.use((db) => db.select().from(SessionTable).all())
|
||||
return rows.map((row) => Session.fromRow(row))
|
||||
}
|
||||
|
||||
export async function aggregateSessionStats(days?: number, projectFilter?: string): Promise<SessionStats> {
|
||||
const sessions = await getAllSessions()
|
||||
const MS_IN_DAY = 24 * 60 * 60 * 1000
|
||||
|
||||
const cutoffTime = (() => {
|
||||
@@ -122,34 +112,17 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
||||
return days
|
||||
})()
|
||||
|
||||
let projectID: string | undefined
|
||||
let filteredSessions = cutoffTime > 0 ? sessions.filter((session) => session.time.updated >= cutoffTime) : sessions
|
||||
|
||||
if (projectFilter !== undefined) {
|
||||
if (projectFilter === "") {
|
||||
const currentProject = await getCurrentProject()
|
||||
projectID = currentProject.id
|
||||
filteredSessions = filteredSessions.filter((session) => session.projectID === currentProject.id)
|
||||
} else {
|
||||
projectID = projectFilter
|
||||
filteredSessions = filteredSessions.filter((session) => session.projectID === projectFilter)
|
||||
}
|
||||
}
|
||||
|
||||
const rows = Database.use((db) => {
|
||||
const conditions = []
|
||||
if (cutoffTime > 0) {
|
||||
conditions.push(gte(SessionTable.time_updated, cutoffTime))
|
||||
}
|
||||
if (projectID !== undefined) {
|
||||
conditions.push(eq(SessionTable.project_id, projectID))
|
||||
}
|
||||
|
||||
const baseQuery = db.select().from(SessionTable)
|
||||
if (conditions.length > 0) {
|
||||
return baseQuery.where(and(...conditions)).all()
|
||||
}
|
||||
return baseQuery.all()
|
||||
})
|
||||
|
||||
const filteredSessions = rows.map((row) => Session.fromRow(row))
|
||||
|
||||
const stats: SessionStats = {
|
||||
totalSessions: filteredSessions.length,
|
||||
totalMessages: 0,
|
||||
@@ -189,58 +162,16 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
||||
|
||||
const sessionTotalTokens: number[] = []
|
||||
|
||||
const BATCH_SIZE = 100
|
||||
const BATCH_SIZE = 20
|
||||
for (let i = 0; i < filteredSessions.length; i += BATCH_SIZE) {
|
||||
const batch = filteredSessions.slice(i, i + BATCH_SIZE)
|
||||
const sessionIds = batch.map((s) => s.id)
|
||||
|
||||
// Bulk fetch messages for this batch of sessions
|
||||
const messageRows = Database.use((db) =>
|
||||
db.select().from(MessageTable).where(inArray(MessageTable.session_id, sessionIds)).all(),
|
||||
)
|
||||
|
||||
// Group messages by session_id
|
||||
const messagesBySession = new Map<string, typeof messageRows>()
|
||||
const messageIds = messageRows.map((r) => r.id)
|
||||
|
||||
for (const row of messageRows) {
|
||||
const msgs = messagesBySession.get(row.session_id) || []
|
||||
msgs.push(row)
|
||||
messagesBySession.set(row.session_id, msgs)
|
||||
}
|
||||
|
||||
// Bulk fetch parts for all these messages
|
||||
let partRows: (typeof PartTable.$inferSelect)[] = []
|
||||
if (messageIds.length > 0) {
|
||||
// Chunk message IDs if there are too many for a single IN clause (SQLite has limits)
|
||||
const PART_BATCH_SIZE = 500
|
||||
for (let j = 0; j < messageIds.length; j += PART_BATCH_SIZE) {
|
||||
const idBatch = messageIds.slice(j, j + PART_BATCH_SIZE)
|
||||
const parts = Database.use((db) =>
|
||||
db.select().from(PartTable).where(inArray(PartTable.message_id, idBatch)).all(),
|
||||
)
|
||||
partRows.push(...parts)
|
||||
}
|
||||
}
|
||||
|
||||
// Group parts by message_id
|
||||
const partsByMessage = new Map<string, MessageV2.Part[]>()
|
||||
for (const row of partRows) {
|
||||
const parts = partsByMessage.get(row.message_id) || []
|
||||
parts.push({ ...row.data, id: row.id, sessionID: row.session_id, messageID: row.message_id } as MessageV2.Part)
|
||||
partsByMessage.set(row.message_id, parts)
|
||||
}
|
||||
|
||||
const batchResults = batch.map((session) => {
|
||||
const rawMessages = messagesBySession.get(session.id) || []
|
||||
const messages = rawMessages.map((row) => ({
|
||||
info: { ...row.data, id: row.id, sessionID: row.session_id } as MessageV2.Info,
|
||||
parts: partsByMessage.get(row.id) || [],
|
||||
}))
|
||||
const batchPromises = batch.map(async (session) => {
|
||||
const messages = await Session.messages({ sessionID: session.id })
|
||||
|
||||
let sessionCost = 0
|
||||
let sessionTokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }
|
||||
let sessionToolUsage: Record<string, { calls: number; errors: number }> = {}
|
||||
let sessionToolUsage: Record<string, number> = {}
|
||||
let sessionModelUsage: Record<
|
||||
string,
|
||||
{
|
||||
@@ -254,7 +185,6 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
||||
}
|
||||
}
|
||||
cost: number
|
||||
toolUsage: Record<string, { calls: number; errors: number }>
|
||||
}
|
||||
> = {}
|
||||
|
||||
@@ -268,7 +198,6 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
||||
messages: 0,
|
||||
tokens: { input: 0, output: 0, cache: { read: 0, write: 0 } },
|
||||
cost: 0,
|
||||
toolUsage: {},
|
||||
}
|
||||
}
|
||||
sessionModelUsage[modelKey].messages++
|
||||
@@ -287,22 +216,11 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
||||
sessionModelUsage[modelKey].tokens.cache.read += message.info.tokens.cache?.read || 0
|
||||
sessionModelUsage[modelKey].tokens.cache.write += message.info.tokens.cache?.write || 0
|
||||
}
|
||||
}
|
||||
|
||||
for (const part of message.parts) {
|
||||
if (part.type === "tool" && part.tool) {
|
||||
const isError =
|
||||
part.state && part.state.status === "error" && part.state.error !== "Tool execution aborted"
|
||||
|
||||
if (!sessionToolUsage[part.tool]) sessionToolUsage[part.tool] = { calls: 0, errors: 0 }
|
||||
sessionToolUsage[part.tool].calls++
|
||||
if (isError) sessionToolUsage[part.tool].errors++
|
||||
|
||||
if (!sessionModelUsage[modelKey].toolUsage[part.tool]) {
|
||||
sessionModelUsage[modelKey].toolUsage[part.tool] = { calls: 0, errors: 0 }
|
||||
}
|
||||
sessionModelUsage[modelKey].toolUsage[part.tool].calls++
|
||||
if (isError) sessionModelUsage[modelKey].toolUsage[part.tool].errors++
|
||||
}
|
||||
for (const part of message.parts) {
|
||||
if (part.type === "tool" && part.tool) {
|
||||
sessionToolUsage[part.tool] = (sessionToolUsage[part.tool] || 0) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -324,6 +242,8 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
||||
}
|
||||
})
|
||||
|
||||
const batchResults = await Promise.all(batchPromises)
|
||||
|
||||
for (const result of batchResults) {
|
||||
earliestTime = Math.min(earliestTime, result.earliestTime)
|
||||
latestTime = Math.max(latestTime, result.latestTime)
|
||||
@@ -338,9 +258,7 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
||||
stats.totalTokens.cache.write += result.sessionTokens.cache.write
|
||||
|
||||
for (const [tool, count] of Object.entries(result.sessionToolUsage)) {
|
||||
if (!stats.toolUsage[tool]) stats.toolUsage[tool] = { calls: 0, errors: 0 }
|
||||
stats.toolUsage[tool].calls += count.calls
|
||||
stats.toolUsage[tool].errors += count.errors
|
||||
stats.toolUsage[tool] = (stats.toolUsage[tool] || 0) + count
|
||||
}
|
||||
|
||||
for (const [model, usage] of Object.entries(result.sessionModelUsage)) {
|
||||
@@ -349,7 +267,6 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
||||
messages: 0,
|
||||
tokens: { input: 0, output: 0, cache: { read: 0, write: 0 } },
|
||||
cost: 0,
|
||||
toolUsage: {},
|
||||
}
|
||||
}
|
||||
stats.modelUsage[model].messages += usage.messages
|
||||
@@ -358,14 +275,6 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
||||
stats.modelUsage[model].tokens.cache.read += usage.tokens.cache.read
|
||||
stats.modelUsage[model].tokens.cache.write += usage.tokens.cache.write
|
||||
stats.modelUsage[model].cost += usage.cost
|
||||
|
||||
for (const [tool, toolUsage] of Object.entries(usage.toolUsage)) {
|
||||
if (!stats.modelUsage[model].toolUsage[tool]) {
|
||||
stats.modelUsage[model].toolUsage[tool] = { calls: 0, errors: 0 }
|
||||
}
|
||||
stats.modelUsage[model].toolUsage[tool].calls += toolUsage.calls
|
||||
stats.modelUsage[model].toolUsage[tool].errors += toolUsage.errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -397,7 +306,7 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
|
||||
return stats
|
||||
}
|
||||
|
||||
export function displayStats(stats: SessionStats, toolLimit?: number, modelLimit?: number, modelFilter?: string[]) {
|
||||
export function displayStats(stats: SessionStats, toolLimit?: number, modelLimit?: number) {
|
||||
const width = 56
|
||||
|
||||
function renderRow(label: string, value: string): string {
|
||||
@@ -437,73 +346,43 @@ export function displayStats(stats: SessionStats, toolLimit?: number, modelLimit
|
||||
console.log()
|
||||
|
||||
// Model Usage section
|
||||
if ((modelLimit !== undefined || modelFilter !== undefined) && Object.keys(stats.modelUsage).length > 0) {
|
||||
let sortedModels = Object.entries(stats.modelUsage).sort(([, a], [, b]) => b.messages - a.messages)
|
||||
if (modelLimit !== undefined && Object.keys(stats.modelUsage).length > 0) {
|
||||
const sortedModels = Object.entries(stats.modelUsage).sort(([, a], [, b]) => b.messages - a.messages)
|
||||
const modelsToDisplay = modelLimit === Infinity ? sortedModels : sortedModels.slice(0, modelLimit)
|
||||
|
||||
if (modelFilter && modelFilter.length > 0) {
|
||||
sortedModels = sortedModels.filter(([model]) => modelFilter.some((filter) => model.includes(filter)))
|
||||
}
|
||||
console.log("┌────────────────────────────────────────────────────────┐")
|
||||
console.log("│ MODEL USAGE │")
|
||||
console.log("├────────────────────────────────────────────────────────┤")
|
||||
|
||||
const modelsToDisplay =
|
||||
modelLimit === Infinity || modelLimit === undefined ? sortedModels : sortedModels.slice(0, modelLimit)
|
||||
|
||||
if (modelsToDisplay.length > 0) {
|
||||
console.log("┌────────────────────────────────────────────────────────┐")
|
||||
console.log("│ MODEL USAGE │")
|
||||
for (const [model, usage] of modelsToDisplay) {
|
||||
console.log(`│ ${model.padEnd(54)} │`)
|
||||
console.log(renderRow(" Messages", usage.messages.toLocaleString()))
|
||||
console.log(renderRow(" Input Tokens", formatNumber(usage.tokens.input)))
|
||||
console.log(renderRow(" Output Tokens", formatNumber(usage.tokens.output)))
|
||||
console.log(renderRow(" Cache Read", formatNumber(usage.tokens.cache.read)))
|
||||
console.log(renderRow(" Cache Write", formatNumber(usage.tokens.cache.write)))
|
||||
console.log(renderRow(" Cost", `$${usage.cost.toFixed(4)}`))
|
||||
console.log("├────────────────────────────────────────────────────────┤")
|
||||
|
||||
for (const [model, usage] of modelsToDisplay) {
|
||||
console.log(`│ ${model.padEnd(54)} │`)
|
||||
console.log(renderRow(" Messages", usage.messages.toLocaleString()))
|
||||
console.log(renderRow(" Input Tokens", formatNumber(usage.tokens.input)))
|
||||
console.log(renderRow(" Output Tokens", formatNumber(usage.tokens.output)))
|
||||
console.log(renderRow(" Cache Read", formatNumber(usage.tokens.cache.read)))
|
||||
console.log(renderRow(" Cache Write", formatNumber(usage.tokens.cache.write)))
|
||||
console.log(renderRow(" Cost", `$${usage.cost.toFixed(4)}`))
|
||||
|
||||
if (Object.keys(usage.toolUsage).length > 0) {
|
||||
console.log(`│ │`)
|
||||
console.log(`│ Tool Call Rate Error Rate │`)
|
||||
|
||||
const totalModelTools = Object.values(usage.toolUsage).reduce((sum, t) => sum + t.calls, 0)
|
||||
const sortedTools = Object.entries(usage.toolUsage).sort((a, b) => b[1].calls - a[1].calls)
|
||||
|
||||
for (const [tool, toolStats] of sortedTools) {
|
||||
const callRate = ((toolStats.calls / totalModelTools) * 100).toFixed(1) + "%"
|
||||
const errorRate = toolStats.calls > 0 ? ((toolStats.errors / toolStats.calls) * 100).toFixed(1) + "%" : "0%"
|
||||
|
||||
const toolName = tool.length > 22 ? tool.substring(0, 20) + ".." : tool
|
||||
const paddedTool = toolName.padEnd(24)
|
||||
const callStr = callRate.padStart(13)
|
||||
const errStr = errorRate.padStart(15)
|
||||
|
||||
console.log(`│ ${paddedTool}${callStr}${errStr} │`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log("├────────────────────────────────────────────────────────┤")
|
||||
}
|
||||
// Remove last separator and add bottom border
|
||||
process.stdout.write("\x1B[1A") // Move up one line
|
||||
console.log("└────────────────────────────────────────────────────────┘")
|
||||
}
|
||||
// Remove last separator and add bottom border
|
||||
process.stdout.write("\x1B[1A") // Move up one line
|
||||
console.log("└────────────────────────────────────────────────────────┘")
|
||||
}
|
||||
console.log()
|
||||
|
||||
// Tool Usage section
|
||||
if (Object.keys(stats.toolUsage).length > 0) {
|
||||
const sortedTools = Object.entries(stats.toolUsage).sort(([, a], [, b]) => b.calls - a.calls)
|
||||
const sortedTools = Object.entries(stats.toolUsage).sort(([, a], [, b]) => b - a)
|
||||
const toolsToDisplay = toolLimit ? sortedTools.slice(0, toolLimit) : sortedTools
|
||||
|
||||
console.log("┌────────────────────────────────────────────────────────┐")
|
||||
console.log("│ TOOL USAGE │")
|
||||
console.log("├────────────────────────────────────────────────────────┤")
|
||||
|
||||
const maxCount = Math.max(...toolsToDisplay.map(([, toolStats]) => toolStats.calls))
|
||||
const totalToolUsage = Object.values(stats.toolUsage).reduce((a, b) => a + b.calls, 0)
|
||||
const maxCount = Math.max(...toolsToDisplay.map(([, count]) => count))
|
||||
const totalToolUsage = Object.values(stats.toolUsage).reduce((a, b) => a + b, 0)
|
||||
|
||||
for (const [tool, toolStats] of toolsToDisplay) {
|
||||
const count = toolStats.calls
|
||||
for (const [tool, count] of toolsToDisplay) {
|
||||
const barLength = Math.max(1, Math.floor((count / maxCount) * 20))
|
||||
const bar = "█".repeat(barLength)
|
||||
const percentage = ((count / totalToolUsage) * 100).toFixed(1)
|
||||
|
||||
@@ -292,7 +292,9 @@ export namespace Config {
|
||||
...(proxied() ? ["--no-cache"] : []),
|
||||
],
|
||||
{ cwd: dir },
|
||||
).catch(() => {})
|
||||
).catch((err) => {
|
||||
log.warn("failed to install dependencies", { dir, error: err })
|
||||
})
|
||||
}
|
||||
|
||||
async function isWritable(dir: string) {
|
||||
|
||||
@@ -41,8 +41,10 @@ export namespace Plugin {
|
||||
|
||||
for (const plugin of INTERNAL_PLUGINS) {
|
||||
log.info("loading internal plugin", { name: plugin.name })
|
||||
const init = await plugin(input)
|
||||
hooks.push(init)
|
||||
const init = await plugin(input).catch((err) => {
|
||||
log.error("failed to load internal plugin", { name: plugin.name, error: err })
|
||||
})
|
||||
if (init) hooks.push(init)
|
||||
}
|
||||
|
||||
let plugins = config.plugin ?? []
|
||||
@@ -59,37 +61,40 @@ export namespace Plugin {
|
||||
const lastAtIndex = plugin.lastIndexOf("@")
|
||||
const pkg = lastAtIndex > 0 ? plugin.substring(0, lastAtIndex) : plugin
|
||||
const version = lastAtIndex > 0 ? plugin.substring(lastAtIndex + 1) : "latest"
|
||||
const builtin = BUILTIN.some((x) => x.startsWith(pkg + "@"))
|
||||
plugin = await BunProc.install(pkg, version).catch((err) => {
|
||||
if (!builtin) throw err
|
||||
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
log.error("failed to install builtin plugin", {
|
||||
pkg,
|
||||
version,
|
||||
error: message,
|
||||
})
|
||||
const cause = err instanceof Error ? err.cause : err
|
||||
const detail = cause instanceof Error ? cause.message : String(cause ?? err)
|
||||
log.error("failed to install plugin", { pkg, version, error: detail })
|
||||
Bus.publish(Session.Event.Error, {
|
||||
error: new NamedError.Unknown({
|
||||
message: `Failed to install built-in plugin ${pkg}@${version}: ${message}`,
|
||||
message: `Failed to install plugin ${pkg}@${version}: ${detail}`,
|
||||
}).toObject(),
|
||||
})
|
||||
|
||||
return ""
|
||||
})
|
||||
if (!plugin) continue
|
||||
}
|
||||
const mod = await import(plugin)
|
||||
// Prevent duplicate initialization when plugins export the same function
|
||||
// as both a named export and default export (e.g., `export const X` and `export default X`).
|
||||
// Object.entries(mod) would return both entries pointing to the same function reference.
|
||||
const seen = new Set<PluginInstance>()
|
||||
for (const [_name, fn] of Object.entries<PluginInstance>(mod)) {
|
||||
if (seen.has(fn)) continue
|
||||
seen.add(fn)
|
||||
const init = await fn(input)
|
||||
hooks.push(init)
|
||||
}
|
||||
await import(plugin)
|
||||
.then(async (mod) => {
|
||||
const seen = new Set<PluginInstance>()
|
||||
for (const [_name, fn] of Object.entries<PluginInstance>(mod)) {
|
||||
if (seen.has(fn)) continue
|
||||
seen.add(fn)
|
||||
hooks.push(await fn(input))
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
log.error("failed to load plugin", { path: plugin, error: message })
|
||||
Bus.publish(Session.Event.Error, {
|
||||
error: new NamedError.Unknown({
|
||||
message: `Failed to load plugin ${plugin}: ${message}`,
|
||||
}).toObject(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -66,7 +66,7 @@ export namespace Snapshot {
|
||||
await $`git --git-dir ${git} config core.autocrlf false`.quiet().nothrow()
|
||||
log.info("initialized")
|
||||
}
|
||||
await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow()
|
||||
await add(git)
|
||||
const hash = await $`git --git-dir ${git} --work-tree ${Instance.worktree} write-tree`
|
||||
.quiet()
|
||||
.cwd(Instance.directory)
|
||||
@@ -84,7 +84,7 @@ export namespace Snapshot {
|
||||
|
||||
export async function patch(hash: string): Promise<Patch> {
|
||||
const git = gitdir()
|
||||
await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow()
|
||||
await add(git)
|
||||
const result =
|
||||
await $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff --name-only ${hash} -- .`
|
||||
.quiet()
|
||||
@@ -162,7 +162,7 @@ export namespace Snapshot {
|
||||
|
||||
export async function diff(hash: string) {
|
||||
const git = gitdir()
|
||||
await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow()
|
||||
await add(git)
|
||||
const result =
|
||||
await $`git -c core.autocrlf=false -c core.quotepath=false --git-dir ${git} --work-tree ${Instance.worktree} diff --no-ext-diff ${hash} -- .`
|
||||
.quiet()
|
||||
@@ -253,4 +253,38 @@ export namespace Snapshot {
|
||||
const project = Instance.project
|
||||
return path.join(Global.Path.data, "snapshot", project.id)
|
||||
}
|
||||
|
||||
async function add(git: string) {
|
||||
await syncExclude(git)
|
||||
await $`git --git-dir ${git} --work-tree ${Instance.worktree} add .`.quiet().cwd(Instance.directory).nothrow()
|
||||
}
|
||||
|
||||
async function syncExclude(git: string) {
|
||||
const file = await excludes()
|
||||
const target = path.join(git, "info", "exclude")
|
||||
await fs.mkdir(path.join(git, "info"), { recursive: true })
|
||||
if (!file) {
|
||||
await Bun.write(target, "")
|
||||
return
|
||||
}
|
||||
const text = await Bun.file(file)
|
||||
.text()
|
||||
.catch(() => "")
|
||||
await Bun.write(target, text)
|
||||
}
|
||||
|
||||
async function excludes() {
|
||||
const file = await $`git rev-parse --path-format=absolute --git-path info/exclude`
|
||||
.quiet()
|
||||
.cwd(Instance.worktree)
|
||||
.nothrow()
|
||||
.text()
|
||||
if (!file.trim()) return
|
||||
const exists = await fs
|
||||
.stat(file.trim())
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
if (!exists) return
|
||||
return file.trim()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,6 +508,68 @@ test("gitignore changes", async () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("git info exclude changes", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
const file = `${tmp.path}/.git/info/exclude`
|
||||
const text = await Bun.file(file).text()
|
||||
await Bun.write(file, `${text.trimEnd()}\nignored.txt\n`)
|
||||
await Bun.write(`${tmp.path}/ignored.txt`, "ignored content")
|
||||
await Bun.write(`${tmp.path}/normal.txt`, "normal content")
|
||||
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(`${tmp.path}/normal.txt`)
|
||||
expect(patch.files).not.toContain(`${tmp.path}/ignored.txt`)
|
||||
|
||||
const after = await Snapshot.track()
|
||||
const diffs = await Snapshot.diffFull(before!, after!)
|
||||
expect(diffs.some((x) => x.file === "normal.txt")).toBe(true)
|
||||
expect(diffs.some((x) => x.file === "ignored.txt")).toBe(false)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("git info exclude keeps global excludes", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const global = `${tmp.path}/global.ignore`
|
||||
const config = `${tmp.path}/global.gitconfig`
|
||||
await Bun.write(global, "global.tmp\n")
|
||||
await Bun.write(config, `[core]\n\texcludesFile = ${global}\n`)
|
||||
|
||||
const prev = process.env.GIT_CONFIG_GLOBAL
|
||||
process.env.GIT_CONFIG_GLOBAL = config
|
||||
try {
|
||||
const before = await Snapshot.track()
|
||||
expect(before).toBeTruthy()
|
||||
|
||||
const file = `${tmp.path}/.git/info/exclude`
|
||||
const text = await Bun.file(file).text()
|
||||
await Bun.write(file, `${text.trimEnd()}\ninfo.tmp\n`)
|
||||
|
||||
await Bun.write(`${tmp.path}/global.tmp`, "global content")
|
||||
await Bun.write(`${tmp.path}/info.tmp`, "info content")
|
||||
await Bun.write(`${tmp.path}/normal.txt`, "normal content")
|
||||
|
||||
const patch = await Snapshot.patch(before!)
|
||||
expect(patch.files).toContain(`${tmp.path}/normal.txt`)
|
||||
expect(patch.files).not.toContain(`${tmp.path}/global.tmp`)
|
||||
expect(patch.files).not.toContain(`${tmp.path}/info.tmp`)
|
||||
} finally {
|
||||
if (prev) process.env.GIT_CONFIG_GLOBAL = prev
|
||||
else delete process.env.GIT_CONFIG_GLOBAL
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("concurrent file operations during patch", async () => {
|
||||
await using tmp = await bootstrap()
|
||||
await Instance.provide({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
@@ -13,15 +13,15 @@
|
||||
"./client": "./src/client.ts",
|
||||
"./server": "./src/server.ts",
|
||||
"./v2": {
|
||||
"types": "./dist/src/v2/index.d.ts",
|
||||
"types": "./dist/v2/index.d.ts",
|
||||
"default": "./src/v2/index.ts"
|
||||
},
|
||||
"./v2/client": {
|
||||
"types": "./dist/src/v2/client.d.ts",
|
||||
"types": "./dist/v2/client.d.ts",
|
||||
"default": "./src/v2/client.ts"
|
||||
},
|
||||
"./v2/gen/client": {
|
||||
"types": "./dist/src/v2/gen/client/index.d.ts",
|
||||
"types": "./dist/v2/gen/client/index.d.ts",
|
||||
"default": "./src/v2/gen/client/index.ts"
|
||||
},
|
||||
"./v2/server": "./src/v2/server.ts"
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"declaration": true,
|
||||
"moduleResolution": "nodenext",
|
||||
"lib": ["es2022", "dom", "dom.iterable"],
|
||||
"composite": true
|
||||
"composite": true,
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
||||
@@ -104,6 +104,7 @@ export interface MessagePartProps {
|
||||
hideDetails?: boolean
|
||||
defaultOpen?: boolean
|
||||
showAssistantCopyPartID?: string | null
|
||||
turnDurationMs?: number
|
||||
}
|
||||
|
||||
export type PartComponent = Component<MessagePartProps>
|
||||
@@ -149,6 +150,8 @@ function createThrottledValue(getValue: () => string) {
|
||||
function relativizeProjectPaths(text: string, directory?: string) {
|
||||
if (!text) return ""
|
||||
if (!directory) return text
|
||||
if (directory === "/") return text
|
||||
if (directory === "\\") return text
|
||||
return text.split(directory).join("")
|
||||
}
|
||||
|
||||
@@ -275,6 +278,7 @@ function renderable(part: PartType) {
|
||||
export function AssistantParts(props: {
|
||||
messages: AssistantMessage[]
|
||||
showAssistantCopyPartID?: string | null
|
||||
turnDurationMs?: number
|
||||
working?: boolean
|
||||
}) {
|
||||
const data = useData()
|
||||
@@ -365,6 +369,7 @@ export function AssistantParts(props: {
|
||||
part={entry().part}
|
||||
message={entry().message}
|
||||
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
||||
turnDurationMs={props.turnDurationMs}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
@@ -849,6 +854,7 @@ export function Part(props: MessagePartProps) {
|
||||
hideDetails={props.hideDetails}
|
||||
defaultOpen={props.defaultOpen}
|
||||
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
||||
turnDurationMs={props.turnDurationMs}
|
||||
/>
|
||||
</Show>
|
||||
)
|
||||
@@ -1060,8 +1066,12 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
|
||||
if (props.message.role !== "assistant") return ""
|
||||
const message = props.message as AssistantMessage
|
||||
const completed = message.time.completed
|
||||
if (typeof completed !== "number") return ""
|
||||
const ms = completed - message.time.created
|
||||
const ms =
|
||||
typeof props.turnDurationMs === "number"
|
||||
? props.turnDurationMs
|
||||
: typeof completed === "number"
|
||||
? completed - message.time.created
|
||||
: -1
|
||||
if (!(ms >= 0)) return ""
|
||||
const total = Math.round(ms / 1000)
|
||||
if (total < 60) return `${total}s`
|
||||
|
||||
@@ -247,6 +247,21 @@ export function SessionTurn(
|
||||
if (working()) return null
|
||||
return showAssistantCopyPartID() ?? null
|
||||
})
|
||||
const turnDurationMs = createMemo(() => {
|
||||
const start = message()?.time.created
|
||||
if (typeof start !== "number") return undefined
|
||||
|
||||
const end = assistantMessages().reduce<number | undefined>((max, item) => {
|
||||
const completed = item.time.completed
|
||||
if (typeof completed !== "number") return max
|
||||
if (max === undefined) return completed
|
||||
return Math.max(max, completed)
|
||||
}, undefined)
|
||||
|
||||
if (typeof end !== "number") return undefined
|
||||
if (end < start) return undefined
|
||||
return end - start
|
||||
})
|
||||
const assistantVisible = createMemo(() =>
|
||||
assistantMessages().reduce((count, message) => {
|
||||
const parts = list(data.store.part?.[message.id], emptyParts)
|
||||
@@ -290,6 +305,7 @@ export function SessionTurn(
|
||||
<AssistantParts
|
||||
messages={assistantMessages()}
|
||||
showAssistantCopyPartID={assistantCopyPartID()}
|
||||
turnDurationMs={turnDurationMs()}
|
||||
working={working()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "opencode",
|
||||
"displayName": "opencode",
|
||||
"description": "opencode for VS Code",
|
||||
"version": "1.2.9",
|
||||
"version": "1.2.10",
|
||||
"publisher": "sst-dev",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user