From e698507b1488d69bf9f310d3552fdb54d229dc24 Mon Sep 17 00:00:00 2001 From: RikishK Date: Sat, 14 Feb 2026 13:39:47 +1100 Subject: [PATCH 1/2] Context usage tool --- packages/opencode/src/tool/context.ts | 40 ++++++++++++++++++++++++++ packages/opencode/src/tool/context.txt | 1 + packages/opencode/src/tool/registry.ts | 3 ++ 3 files changed, 44 insertions(+) create mode 100644 packages/opencode/src/tool/context.ts create mode 100644 packages/opencode/src/tool/context.txt diff --git a/packages/opencode/src/tool/context.ts b/packages/opencode/src/tool/context.ts new file mode 100644 index 000000000000..281c228173d7 --- /dev/null +++ b/packages/opencode/src/tool/context.ts @@ -0,0 +1,40 @@ +import z from "zod" +import { Tool } from "./tool" +import type { Provider } from "../provider/provider" +import DESCRIPTION from "./context.txt" + +export const ContextUsageTool = Tool.define("check_context_usage", { + description: DESCRIPTION, + parameters: z.object({}), + async execute(_params, ctx) { + const model = ctx.extra?.model as Provider.Model | undefined + const last = ctx.messages.filter((msg) => msg.info.role === "assistant" && msg.info.tokens.output > 0).at(-1) + + if (!last || last.info.role !== "assistant") { + return { + title: "No usage data", + metadata: {}, + output: "No context usage data available yet.", + } + } + + const tokens = last.info.tokens + const total = tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write + const limit = model?.limit.context + const percentage = limit ? Math.round((total / limit) * 100) : null + + return { + title: percentage !== null ? `${percentage}% used` : `${total.toLocaleString()} tokens`, + metadata: { + total, + limit, + percentage, + tokens, + }, + output: + percentage !== null + ? `Context usage: ${total.toLocaleString()} tokens (${percentage}% of ${limit?.toLocaleString()} context limit)` + : `Context usage: ${total.toLocaleString()} tokens (context limit unknown)`, + } + }, +}) diff --git a/packages/opencode/src/tool/context.txt b/packages/opencode/src/tool/context.txt new file mode 100644 index 000000000000..6fa92cae630d --- /dev/null +++ b/packages/opencode/src/tool/context.txt @@ -0,0 +1 @@ +Check your current context window usage. Returns the total token count, the percentage of the context window used, and the context limit. Use this to monitor how much of your context window remains before deciding whether to compact or adjust your approach. diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index f6324b3d7690..3638560e404f 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -47,6 +47,7 @@ import { AppFileSystem } from "../filesystem" import { Agent } from "../agent/agent" import { Skill } from "../skill" import { Permission } from "@/permission" +import { ContextUsageTool } from "./context" export namespace ToolRegistry { const log = Log.create({ service: "tool.registry" }) @@ -191,6 +192,7 @@ export namespace ToolRegistry { question: Tool.init(question), lsp: Tool.init(lsptool), plan: Tool.init(plan), + context: Tool.init(ContextUsageTool), }) return { @@ -207,6 +209,7 @@ export namespace ToolRegistry { tool.task, tool.fetch, tool.todo, + tool.context, tool.search, tool.code, tool.skill, From 52b94cb5d097b48b5514575bc272ab8925c47206 Mon Sep 17 00:00:00 2001 From: RikishK Date: Thu, 2 Apr 2026 21:32:09 +1100 Subject: [PATCH 2/2] new session tool --- .npmrc | 1 + packages/opencode/.npmrc | 1 + packages/opencode/src/cli/cmd/tui/app.tsx | 34 ++++++++++++++++++++++ packages/opencode/src/cli/cmd/tui/event.ts | 7 +++++ packages/opencode/src/tool/new-session.ts | 24 +++++++++++++++ packages/opencode/src/tool/new-session.txt | 5 ++++ packages/opencode/src/tool/registry.ts | 4 +++ packages/sdk/js/src/v2/gen/sdk.gen.ts | 8 ++++- packages/sdk/js/src/v2/gen/types.gen.ts | 17 ++++++++++- 9 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 .npmrc create mode 100644 packages/opencode/.npmrc create mode 100644 packages/opencode/src/tool/new-session.ts create mode 100644 packages/opencode/src/tool/new-session.txt diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000000..214c29d13959 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +registry=https://registry.npmjs.org/ diff --git a/packages/opencode/.npmrc b/packages/opencode/.npmrc new file mode 100644 index 000000000000..214c29d13959 --- /dev/null +++ b/packages/opencode/.npmrc @@ -0,0 +1 @@ +registry=https://registry.npmjs.org/ diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 8c4f596fd337..10ac5d542ee8 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -50,6 +50,7 @@ import { ToastProvider, useToast } from "./ui/toast" import { ExitProvider, useExit } from "./context/exit" import { Session as SessionApi } from "@/session" import { TuiEvent } from "./event" +import { MessageID, PartID } from "@/session/schema" import { KVProvider, useKV } from "./context/kv" import { Provider } from "@/provider/provider" import { ArgsProvider, useArgs, type Args } from "./context/args" @@ -810,6 +811,39 @@ function App(props: { onSnapshot?: () => Promise }) { }) }) + event.on(TuiEvent.SessionNew.type, async (evt) => { + const model = local.model.current() + if (!model) return + + // Abort the old session + await sdk.client.session.abort({ sessionID: evt.properties.sessionID }).catch(() => {}) + + // Create and navigate to the new session + const res = await sdk.client.session.create({}) + if (res.error) return + const id = res.data.id + + route.navigate({ type: "session", sessionID: id }) + + // Auto-send the initial message + sdk.client.session + .prompt({ + sessionID: id, + ...model, + messageID: MessageID.ascending(), + agent: local.agent.current().name, + model, + parts: [ + { + id: PartID.ascending(), + type: "text" as const, + text: evt.properties.message, + }, + ], + }) + .catch(() => {}) + }) + event.on("session.deleted", (evt) => { if (route.data.type === "session" && route.data.sessionID === evt.properties.info.id) { route.navigate({ type: "home" }) diff --git a/packages/opencode/src/cli/cmd/tui/event.ts b/packages/opencode/src/cli/cmd/tui/event.ts index b2e4b92c5513..76a3e421c474 100644 --- a/packages/opencode/src/cli/cmd/tui/event.ts +++ b/packages/opencode/src/cli/cmd/tui/event.ts @@ -46,4 +46,11 @@ export const TuiEvent = { sessionID: SessionID.zod.describe("Session ID to navigate to"), }), ), + SessionNew: BusEvent.define( + "tui.session.new", + z.object({ + message: z.string().describe("Initial message to send in the new session"), + sessionID: SessionID.zod.describe("Session ID of the current session to abort"), + }), + ), } diff --git a/packages/opencode/src/tool/new-session.ts b/packages/opencode/src/tool/new-session.ts new file mode 100644 index 000000000000..6530468db663 --- /dev/null +++ b/packages/opencode/src/tool/new-session.ts @@ -0,0 +1,24 @@ +import z from "zod" +import { Tool } from "./tool" +import { Bus } from "../bus" +import { TuiEvent } from "../cli/cmd/tui/event" +import DESCRIPTION from "./new-session.txt" + +export const NewSessionTool = Tool.define("new_session", { + description: DESCRIPTION, + parameters: z.object({ + initial_message: z.string().describe("The message to send as the first user message in the new session"), + }), + async execute(args, ctx) { + await Bus.publish(TuiEvent.SessionNew, { + message: args.initial_message, + sessionID: ctx.sessionID, + }) + + return { + title: "Starting new session", + metadata: {}, + output: `New session requested with message: "${args.initial_message}". Current session will be terminated.`, + } + }, +}) diff --git a/packages/opencode/src/tool/new-session.txt b/packages/opencode/src/tool/new-session.txt new file mode 100644 index 000000000000..7a5bc0195e03 --- /dev/null +++ b/packages/opencode/src/tool/new-session.txt @@ -0,0 +1,5 @@ +Stop the current session and start a new one with the given initial message. Use this when you need to start fresh in a new session, for example after completing a large task and the user wants to move on to something unrelated, or when context is getting too large and you want to continue work in a clean session. + +The current session will be aborted immediately and a new session will be created with the initial_message sent as the first user message. The new session will begin processing automatically. + +IMPORTANT: After calling this tool, do not call any other tools or generate further output - the current session is being terminated. diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 3638560e404f..e86fad2b6c44 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -48,6 +48,7 @@ import { Agent } from "../agent/agent" import { Skill } from "../skill" import { Permission } from "@/permission" import { ContextUsageTool } from "./context" +import { NewSessionTool } from "./new-session" export namespace ToolRegistry { const log = Log.create({ service: "tool.registry" }) @@ -173,6 +174,7 @@ export namespace ToolRegistry { const cfg = yield* config.get() const questionEnabled = ["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) || Flag.OPENCODE_ENABLE_QUESTION_TOOL + const tui = ["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) const tool = yield* Effect.all({ invalid: Tool.init(InvalidTool), @@ -193,6 +195,7 @@ export namespace ToolRegistry { lsp: Tool.init(lsptool), plan: Tool.init(plan), context: Tool.init(ContextUsageTool), + session: Tool.init(NewSessionTool), }) return { @@ -210,6 +213,7 @@ export namespace ToolRegistry { tool.fetch, tool.todo, tool.context, + ...(tui ? [tool.session] : []), tool.search, tool.code, tool.skill, diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index d06a504d6c3c..2e6835c4f007 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -22,6 +22,7 @@ import type { EventSubscribeResponses, EventTuiCommandExecute, EventTuiPromptAppend, + EventTuiSessionNew, EventTuiSessionSelect, EventTuiToastShow, ExperimentalConsoleGetResponses, @@ -3824,7 +3825,12 @@ export class Tui extends HeyApiClient { parameters?: { directory?: string workspace?: string - body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect + body?: + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow + | EventTuiSessionSelect + | EventTuiSessionNew }, options?: Options, ) { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index c1a77bfe8842..7a2878061494 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -904,6 +904,20 @@ export type EventMessagePartRemoved = { } } +export type EventTuiSessionNew = { + type: "tui.session.new" + properties: { + /** + * Initial message to send in the new session + */ + message: string + /** + * Session ID of the current session to abort + */ + sessionID: string + } +} + export type PermissionAction = "allow" | "deny" | "ask" export type PermissionRule = { @@ -992,6 +1006,7 @@ export type Event = | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect + | EventTuiSessionNew | EventMcpToolsChanged | EventMcpBrowserOpenFailed | EventCommandExecuted @@ -4971,7 +4986,7 @@ export type TuiShowToastResponses = { export type TuiShowToastResponse = TuiShowToastResponses[keyof TuiShowToastResponses] export type TuiPublishData = { - body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect + body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect | EventTuiSessionNew path?: never query?: { directory?: string