Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/opencode/src/cli/cancelled-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Schema } from "effect"

// Lives in its own module so that `src/cli/ui.ts` (loaded eagerly on every
// CLI invocation) doesn't have to pull `effect/Schema` just to define the
// error class. Callers that throw or match on this class are themselves
// lazy-loaded (github/agent prompts, error formatter).
export class CancelledError extends Schema.TaggedErrorClass<CancelledError>()("UICancelledError", {}) {}
13 changes: 7 additions & 6 deletions packages/opencode/src/cli/cmd/agent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { cmd } from "./cmd"
import * as prompts from "@clack/prompts"
import { UI } from "../ui"
import { CancelledError } from "../cancelled-error"
import { Global } from "@opencode-ai/core/global"
import { Agent } from "../../agent/agent"
import { Provider } from "@/provider/provider"
Expand Down Expand Up @@ -103,7 +104,7 @@ const AgentCreateCommand = effectCmd({
},
],
})
if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
if (prompts.isCancel(scopeResult)) throw new CancelledError()
scope = scopeResult
}
targetPath = path.join(scope === "global" ? Global.Path.config : path.join(ctx.worktree, ".opencode"), "agents")
Expand All @@ -119,7 +120,7 @@ const AgentCreateCommand = effectCmd({
placeholder: "What should this agent do?",
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(query)) throw new UI.CancelledError()
if (prompts.isCancel(query)) throw new CancelledError()
description = query
}

Expand All @@ -130,7 +131,7 @@ const AgentCreateCommand = effectCmd({
const generated = await Effect.runPromise(agentSvc.generate({ description, model })).catch((error) => {
spinner.stop(`LLM failed to generate agent: ${error.message}`, 1)
if (isFullyNonInteractive) process.exit(1)
throw new UI.CancelledError()
throw new CancelledError()
})
spinner.stop(`Agent ${generated.identifier} generated`)

Expand All @@ -147,7 +148,7 @@ const AgentCreateCommand = effectCmd({
})),
initialValues: AVAILABLE_PERMISSIONS,
})
if (prompts.isCancel(result)) throw new UI.CancelledError()
if (prompts.isCancel(result)) throw new CancelledError()
selected = result
}

Expand Down Expand Up @@ -177,7 +178,7 @@ const AgentCreateCommand = effectCmd({
],
initialValue: "all" as const,
})
if (prompts.isCancel(modeResult)) throw new UI.CancelledError()
if (prompts.isCancel(modeResult)) throw new CancelledError()
mode = modeResult
}

Expand Down Expand Up @@ -214,7 +215,7 @@ const AgentCreateCommand = effectCmd({
process.exit(1)
}
prompts.log.error(`Agent file already exists: ${filePath}`)
throw new UI.CancelledError()
throw new CancelledError()
}

await Filesystem.write(filePath, content)
Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/src/cli/cmd/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { MessageV2 } from "../../session/message-v2"
import { SessionID } from "../../session/schema"
import { effectCmd, fail } from "../effect-cmd"
import { UI } from "../ui"
import { CancelledError } from "../cancelled-error"
import * as prompts from "@clack/prompts"
import { EOL } from "os"
import { Effect } from "effect"
Expand Down Expand Up @@ -269,7 +270,7 @@ const run = Effect.fn("Cli.export.body")(function* (args: { sessionID?: string;
)

if (prompts.isCancel(selectedSession)) {
return yield* Effect.die(new UI.CancelledError())
return yield* Effect.die(new CancelledError())
}

sessionID = selectedSession
Expand Down
11 changes: 6 additions & 5 deletions packages/opencode/src/cli/cmd/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
PullRequestEvent,
} from "@octokit/webhooks-types"
import { UI } from "../ui"
import { CancelledError } from "../cancelled-error"
import { cmd } from "./cmd"
import { effectCmd } from "../effect-cmd"
import { ModelsDev } from "@opencode-ai/core/models"
Expand Down Expand Up @@ -248,7 +249,7 @@ export const GithubInstallCommand = effectCmd({
const project = ctx.project
if (project.vcs !== "git") {
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
throw new UI.CancelledError()
throw new CancelledError()
}

// Get repo info
Expand All @@ -258,7 +259,7 @@ export const GithubInstallCommand = effectCmd({
const parsed = parseGitHubRemote(info)
if (!parsed) {
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
throw new UI.CancelledError()
throw new CancelledError()
}
return { owner: parsed.owner, repo: parsed.repo, root: ctx.worktree }
}
Expand Down Expand Up @@ -288,7 +289,7 @@ export const GithubInstallCommand = effectCmd({
),
})

if (prompts.isCancel(provider)) throw new UI.CancelledError()
if (prompts.isCancel(provider)) throw new CancelledError()

return provider
}
Expand All @@ -310,7 +311,7 @@ export const GithubInstallCommand = effectCmd({
),
})

if (prompts.isCancel(model)) throw new UI.CancelledError()
if (prompts.isCancel(model)) throw new CancelledError()
return model
}

Expand Down Expand Up @@ -349,7 +350,7 @@ export const GithubInstallCommand = effectCmd({
s.stop(
`Failed to detect GitHub app installation. Make sure to install the app for the \`${app.owner}/${app.repo}\` repository.`,
)
throw new UI.CancelledError()
throw new CancelledError()
}

retries++
Expand Down
25 changes: 13 additions & 12 deletions packages/opencode/src/cli/cmd/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"
import * as prompts from "@clack/prompts"
import { UI } from "../ui"
import { CancelledError } from "../cancelled-error"
import { MCP } from "../../mcp"
import { McpAuth } from "../../mcp/auth"
import { McpOAuthProvider } from "../../mcp/oauth-provider"
Expand Down Expand Up @@ -220,7 +221,7 @@ export const McpAuthCommand = effectCmd({
options,
}),
)
if (prompts.isCancel(selected)) throw new UI.CancelledError()
if (prompts.isCancel(selected)) throw new CancelledError()
serverName = selected
}

Expand Down Expand Up @@ -380,7 +381,7 @@ export const McpLogoutCommand = effectCmd({
}),
}),
)
if (prompts.isCancel(selected)) throw new UI.CancelledError()
if (prompts.isCancel(selected)) throw new CancelledError()
serverName = selected
}

Expand Down Expand Up @@ -468,15 +469,15 @@ export const McpAddCommand = effectCmd({
},
],
})
if (prompts.isCancel(scopeResult)) throw new UI.CancelledError()
if (prompts.isCancel(scopeResult)) throw new CancelledError()
configPath = scopeResult
}

const name = await prompts.text({
message: "Enter MCP server name",
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(name)) throw new UI.CancelledError()
if (prompts.isCancel(name)) throw new CancelledError()

const type = await prompts.select({
message: "Select MCP server type",
Expand All @@ -493,15 +494,15 @@ export const McpAddCommand = effectCmd({
},
],
})
if (prompts.isCancel(type)) throw new UI.CancelledError()
if (prompts.isCancel(type)) throw new CancelledError()

if (type === "local") {
const command = await prompts.text({
message: "Enter command to run",
placeholder: "e.g., opencode x @modelcontextprotocol/server-filesystem",
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(command)) throw new UI.CancelledError()
if (prompts.isCancel(command)) throw new CancelledError()

const mcpConfig: ConfigMCP.Info = {
type: "local",
Expand All @@ -525,13 +526,13 @@ export const McpAddCommand = effectCmd({
return isValid ? undefined : "Invalid URL"
},
})
if (prompts.isCancel(url)) throw new UI.CancelledError()
if (prompts.isCancel(url)) throw new CancelledError()

const useOAuth = await prompts.confirm({
message: "Does this server require OAuth authentication?",
initialValue: false,
})
if (prompts.isCancel(useOAuth)) throw new UI.CancelledError()
if (prompts.isCancel(useOAuth)) throw new CancelledError()

let mcpConfig: ConfigMCP.Info

Expand All @@ -540,27 +541,27 @@ export const McpAddCommand = effectCmd({
message: "Do you have a pre-registered client ID?",
initialValue: false,
})
if (prompts.isCancel(hasClientId)) throw new UI.CancelledError()
if (prompts.isCancel(hasClientId)) throw new CancelledError()

if (hasClientId) {
const clientId = await prompts.text({
message: "Enter client ID",
validate: (x) => (x && x.length > 0 ? undefined : "Required"),
})
if (prompts.isCancel(clientId)) throw new UI.CancelledError()
if (prompts.isCancel(clientId)) throw new CancelledError()

const hasSecret = await prompts.confirm({
message: "Do you have a client secret?",
initialValue: false,
})
if (prompts.isCancel(hasSecret)) throw new UI.CancelledError()
if (prompts.isCancel(hasSecret)) throw new CancelledError()

let clientSecret: string | undefined
if (hasSecret) {
const secret = await prompts.password({
message: "Enter client secret",
})
if (prompts.isCancel(secret)) throw new UI.CancelledError()
if (prompts.isCancel(secret)) throw new CancelledError()
clientSecret = secret
}

Expand Down
3 changes: 2 additions & 1 deletion packages/opencode/src/cli/cmd/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Auth } from "../../auth"
import { cmd } from "./cmd"
import { CliError, effectCmd, fail } from "../effect-cmd"
import { UI } from "../ui"
import { CancelledError } from "../cancelled-error"
import * as Prompt from "../effect/prompt"
import { ModelsDev } from "@opencode-ai/core/models"

Expand All @@ -20,7 +21,7 @@ import { Effect, Option } from "effect"
type PluginAuth = NonNullable<Hooks["auth"]>

const promptValue = <Value>(value: Option.Option<Value>) => {
if (Option.isNone(value)) return Effect.die(new UI.CancelledError())
if (Option.isNone(value)) return Effect.die(new CancelledError())
return Effect.succeed(value.value)
}

Expand Down
47 changes: 47 additions & 0 deletions packages/opencode/src/cli/cmd/tui/thread-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { Argv } from "yargs"
import { withNetworkOptions } from "@/cli/network-options"

// Single source of truth for the default ($0) command's yargs spec.
//
// This module is imported eagerly from `src/index.ts` so the top-level
// `--help` listing can render the default command's options synchronously
// without dynamic-importing `./thread.ts` (and its Effect/SDK/TUI graph).
// The real `TuiThreadCommand` in `./thread.ts` spreads `TuiThreadSpec` so
// the option contract has exactly one definition.
export const TuiThreadSpec = {
command: "$0 [project]",
describe: "start opencode tui",
builder: (yargs: Argv) =>
withNetworkOptions(yargs)
.positional("project", {
type: "string",
describe: "path to start opencode in",
})
.option("model", {
type: "string",
alias: ["m"],
describe: "model to use in the format of provider/model",
})
.option("continue", {
alias: ["c"],
describe: "continue the last session",
type: "boolean",
})
.option("session", {
alias: ["s"],
type: "string",
describe: "session id to continue",
})
.option("fork", {
type: "boolean",
describe: "fork the session when continuing (use with --continue or --session)",
})
.option("prompt", {
type: "string",
describe: "prompt to use",
})
.option("agent", {
type: "string",
describe: "agent to use",
}),
} as const
Loading
Loading