Compare commits

..

1 Commits

Author SHA1 Message Date
Aiden Cline dd3a279458 fix(core): merge authenticated provider allowlists 2026-08-08 02:36:26 +00:00
366 changed files with 26784 additions and 18067 deletions
-1
View File
@@ -4,7 +4,6 @@ on:
push:
branches:
- dev
- v2
jobs:
generate:
+1
View File
@@ -251,3 +251,4 @@ opencode-drive stop --name demo
```bash
opencode-drive dir --name demo
```
+1 -2
View File
@@ -133,8 +133,7 @@ const countHints = (request: LLMRequest) =>
export const applyCachePolicy = (request: LLMRequest): LLMRequest => {
if (!RESPECTS_INLINE_HINTS.has(request.model.route.id)) return request
if (request.model.route.id === "openrouter" && (request.cache === undefined || request.cache === "auto"))
return request
if (request.model.route.id === "openrouter" && (request.cache === undefined || request.cache === "auto")) return request
const policy = resolve(request.cache)
if (!policy.tools && !policy.system && !policy.messages) return request
@@ -422,12 +422,14 @@ const lowerMedia = Effect.fn("AnthropicMessages.lowerMedia")(function* (part: Me
// Tool results may carry structured text, images, and documents. Keep media as provider-native
// content instead of JSON-stringifying base64 into a prompt string.
const lowerToolResultContentItem = Effect.fnUntraced(function* (item: Tool.Content) {
const lowerToolResultContentItem = Effect.fn("AnthropicMessages.lowerToolResultContentItem")(function* (
item: Tool.Content,
) {
if (item.type === "text") return { type: "text" as const, text: item.text } satisfies AnthropicTextBlock
return yield* lowerMedia({ type: "media", mediaType: item.mime, data: item.uri, filename: item.name })
})
const lowerToolResultContent = Effect.fnUntraced(function* (part: ToolResultPart) {
const lowerToolResultContent = Effect.fn("AnthropicMessages.lowerToolResultContent")(function* (part: ToolResultPart) {
// Text / json / error results stay as a string for backward compatibility
// with existing cassettes and provider expectations.
if (part.result.type !== "content") return ProviderShared.toolResultText(part)
+3 -3
View File
@@ -358,7 +358,7 @@ const lowerMedia = Effect.fn("OpenResponses.lowerMedia")(function* (
return { type: "input_image" as const, image_url: media.dataUrl }
})
const lowerUserContent = Effect.fnUntraced(function* (
const lowerUserContent = Effect.fn("OpenResponses.lowerUserContent")(function* (
part: LLMRequest["messages"][number]["content"][number],
request: LLMRequest,
extension: Extension,
@@ -370,7 +370,7 @@ const lowerUserContent = Effect.fnUntraced(function* (
// Tool results may carry structured text, images, and files. Keep media as provider-native
// content instead of JSON-stringifying base64 into a prompt string.
const lowerToolResultContentItem = Effect.fnUntraced(function* (
const lowerToolResultContentItem = Effect.fn("OpenResponses.lowerToolResultContentItem")(function* (
item: Content,
request: LLMRequest,
extension: Extension,
@@ -383,7 +383,7 @@ const lowerToolResultContentItem = Effect.fnUntraced(function* (
)
})
const lowerToolResultOutput = Effect.fnUntraced(function* (
const lowerToolResultOutput = Effect.fn("OpenResponses.lowerToolResultOutput")(function* (
part: ToolResultPart,
request: LLMRequest,
extension: Extension,
+17 -18
View File
@@ -371,13 +371,12 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
return text
})()
const cached = message.content.findLast((part) => "cache" in part && part.cache !== undefined)
const cacheControl = options.cacheControl?.(cached && "cache" in cached ? cached.cache : undefined)
const result = {
role: "assistant" as const,
content: content.length > 0 ? content.map((part) => part.text).join("") : toolCalls.length > 0 ? null : "",
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
...(details !== undefined ? { reasoning_details: details } : {}),
...(cacheControl !== undefined ? { cache_control: cacheControl } : {}),
content: content.length === 0 ? null : ProviderShared.joinText(content),
tool_calls: toolCalls.length === 0 ? undefined : toolCalls,
reasoning_details: details,
cache_control: options.cacheControl?.(cached && "cache" in cached ? cached.cache : undefined),
}
if (field === undefined || reasoningText === undefined) return result
return { ...result, [field]: reasoningText }
@@ -710,20 +709,23 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
Boolean(delta?.content) ||
reasoning !== undefined ||
(Array.isArray(delta?.reasoning_details) && delta.reasoning_details.length > 0) ||
toolDeltas.some((tool) => Boolean(tool.id) || Boolean(tool.function?.name) || Boolean(tool.function?.arguments))
toolDeltas.some(
(tool) => Boolean(tool.id) || Boolean(tool.function?.name) || Boolean(tool.function?.arguments),
)
if (state.finishReason !== undefined) {
if (hasLateContent)
return yield* ProviderShared.eventError(ADAPTER, "OpenAI Chat received content after the finish reason")
return [{ ...state, usage }, events] as const
}
const reasoningField = state.reasoningField ?? reasoning?.field
const reasoningField = state.reasoningField ?? (!state.lifecycle.text.has("text-0") ? reasoning?.field : undefined)
const detailDelta = Array.isArray(delta?.reasoning_details) ? delta.reasoning_details : undefined
if (detailDelta !== undefined) appendReasoningDetails(state.reasoningDetails, detailDelta)
const reasoningDetailsObserved = state.reasoningDetailsObserved || detailDelta !== undefined
const deltaMetadata = reasoningMetadata(reasoningField)
const text = detailDelta?.length ? (detailText(detailDelta) ?? reasoning?.text) : reasoning?.text
if (text !== undefined) lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", text, deltaMetadata)
if (!state.lifecycle.text.has("text-0") && text !== undefined)
lifecycle = Lifecycle.reasoningDelta(lifecycle, events, "reasoning-0", text, deltaMetadata)
else if (
reasoningDetailsObserved &&
!lifecycle.reasoning.has("reasoning-0") &&
@@ -749,7 +751,8 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
const fallback = toolDeltas.length > 1 ? position : (latestToolIndex ?? position)
const fallbackTool = tools[fallback] ?? pendingTools[fallback]
const index =
tool.index ?? matched ?? (tool.id && fallbackTool?.id && fallbackTool.id !== tool.id ? nextToolIndex : fallback)
tool.index ?? matched ??
(tool.id && fallbackTool?.id && fallbackTool.id !== tool.id ? nextToolIndex : fallback)
const current = tools[index]
const pending = pendingTools[index]
const id = current?.id ?? pending?.id ?? (tool.id || undefined)
@@ -812,18 +815,14 @@ const step = (state: ParserState, event: OpenAIChatEvent) =>
const finishEvents = (state: ParserState): ReadonlyArray<LLMEvent> => {
const events: LLMEvent[] = []
const toolCallEvents =
state.finishReason === undefined && Object.keys(state.tools).length > 0
? Effect.runSync(ToolStream.finishAll(ADAPTER, state.tools)).events
: state.toolCallEvents
const hasToolCalls = toolCallEvents.length > 0
const hasToolCalls = state.toolCallEvents.length > 0
const reason = state.finishReason
? {
...state.finishReason,
normalized:
state.finishReason.normalized === "stop" && hasToolCalls ? "tool-calls" : state.finishReason.normalized,
}
: { normalized: hasToolCalls ? ("tool-calls" as const) : ("unknown" as const) }
: undefined
const metadata = reasoningMetadata(
state.reasoningField,
state.reasoningDetailsObserved ? state.reasoningDetails : undefined,
@@ -833,9 +832,9 @@ const finishEvents = (state: ParserState): ReadonlyArray<LLMEvent> => {
? Lifecycle.reasoningStart(state.lifecycle, events, "reasoning-0", reasoningMetadata(state.reasoningField))
: state.lifecycle
const ended = Lifecycle.reasoningEnd(started, events, "reasoning-0", metadata)
const lifecycle = toolCallEvents.length ? Lifecycle.stepStart(ended, events) : ended
events.push(...toolCallEvents)
Lifecycle.finish(lifecycle, events, { reason, usage: state.usage })
const lifecycle = state.toolCallEvents.length ? Lifecycle.stepStart(ended, events) : ended
events.push(...state.toolCallEvents)
if (reason) Lifecycle.finish(lifecycle, events, { reason, usage: state.usage })
return events
}
@@ -0,0 +1,71 @@
import { AuthOptions, type ProviderAuthOption } from "../route/auth-options"
import type { RouteDefaultsInput } from "../route/client"
import { ProviderID, type ModelID } from "../schema"
import * as OpenAIChat from "../protocols/openai-chat"
import * as OpenAIResponses from "../protocols/openai-responses"
import { withOpenAIOptions, type OpenAIProviderOptionsInput } from "./openai-options"
export const id = ProviderID.make("github-copilot")
// GitHub Copilot has no canonical public URL — callers (opencode, etc.) must
// supply `baseURL` explicitly.
export type LanguageModelOptions = Omit<RouteDefaultsInput, "providerOptions"> &
ProviderAuthOption<"optional"> & {
readonly baseURL: string
readonly endpoint?: "chat" | "responses"
readonly providerOptions?: OpenAIProviderOptionsInput
}
export const shouldUseResponsesApi = (modelID: string | ModelID, endpoint?: LanguageModelOptions["endpoint"]) => {
if (endpoint) return endpoint === "responses"
const model = String(modelID)
const match = /^gpt-(\d+)/.exec(model)
if (!match) return false
return Number(match[1]) >= 5 && !model.startsWith("gpt-5-mini")
}
export const routes = [OpenAIResponses.route, OpenAIChat.route]
const chatRoute = OpenAIChat.route.with({ provider: id })
const responsesRoute = OpenAIResponses.route.with({ provider: id })
const defaults = (options: LanguageModelOptions) => {
const { apiKey: _, auth: _auth, baseURL: _baseURL, endpoint: _endpoint, ...rest } = options
return rest
}
const configuredResponsesRoute = (options: LanguageModelOptions) =>
responsesRoute.with({
endpoint: { baseURL: options.baseURL },
auth: AuthOptions.bearer(options, []),
})
const configuredChatRoute = (options: LanguageModelOptions) =>
chatRoute.with({
endpoint: { baseURL: options.baseURL },
auth: AuthOptions.bearer(options, []),
})
export const configure = (options: LanguageModelOptions) => {
const responsesRoute = configuredResponsesRoute(options)
const chatRoute = configuredChatRoute(options)
const responses = (modelID: string | ModelID) =>
responsesRoute
.with(withOpenAIOptions(modelID, defaults(options)))
.model<OpenAIProviderOptionsInput>({ id: modelID })
const chat = (modelID: string | ModelID) =>
chatRoute.with(withOpenAIOptions(modelID, defaults(options))).model<OpenAIProviderOptionsInput>({ id: modelID })
return {
id,
model: (modelID: string | ModelID) =>
shouldUseResponsesApi(modelID, options.endpoint) ? responses(modelID) : chat(modelID),
responses,
chat,
configure,
}
}
export const provider = {
id,
configure,
}
+1
View File
@@ -5,6 +5,7 @@ export * as AmazonBedrockMantle from "./amazon-bedrock-mantle"
export * as Azure from "./azure"
export * as Cloudflare from "./cloudflare"
export { CloudflareAIGateway, CloudflareWorkersAI } from "./cloudflare"
export * as GitHubCopilot from "./github-copilot"
export * as Google from "./google"
export * as GoogleVertex from "./google-vertex"
export * as GoogleVertexChat from "./google-vertex-chat"
+7 -1
View File
@@ -255,7 +255,13 @@ const requireTerminalEvent = (route: string) => (events: Stream.Stream<LLMEvent,
if (LLMEvent.is.finish(event) || LLMEvent.is.providerError(event)) terminal = true
return Effect.succeed(event)
}),
Stream.onEnd(Effect.suspend(() => (terminal ? Effect.void : Effect.fail(incompleteStreamError(route))))),
Stream.onEnd(
Effect.suspend(() =>
terminal
? Effect.void
: Effect.fail(incompleteStreamError(route)),
),
),
)
})
+2 -14
View File
@@ -1,4 +1,4 @@
import { Cause, Context, Effect, Layer, Option, Schema } from "effect"
import { Cause, Context, Effect, Layer } from "effect"
import {
FetchHttpClient,
Headers,
@@ -198,20 +198,8 @@ const responseBody = (body: string | void, request: HttpClientRequest.HttpClient
return { body: redacted.slice(0, BODY_LIMIT), bodyTruncated: true }
}
const decodeProviderBody = Schema.decodeUnknownOption(
Schema.fromJsonString(
Schema.Struct({
message: Schema.optionalKey(Schema.String),
error: Schema.optionalKey(Schema.Struct({ message: Schema.optionalKey(Schema.String) })),
}),
),
)
const providerMessage = (status: number, body: { readonly body?: string }) => {
if (body.body && body.body.length <= 500) {
const decoded = Option.getOrUndefined(decodeProviderBody(body.body))
return `Provider request failed with HTTP ${status}: ${decoded?.error?.message ?? decoded?.message ?? body.body}`
}
if (body.body && body.body.length <= 500) return `Provider request failed with HTTP ${status}: ${body.body}`
return `Provider request failed with HTTP ${status}`
}
+28 -27
View File
@@ -45,34 +45,35 @@ const isToolResultValue = (value: unknown): value is ToolResultValue =>
(value.type === "text" || value.type === "json" || value.type === "error" || value.type === "content") &&
"value" in value
const toolResultValueSchema = Schema.Union([
Schema.Struct({
type: Schema.Literal("json"),
value: Schema.Unknown,
}),
Schema.Struct({
type: Schema.Literal("text"),
value: Schema.Unknown,
}),
Schema.Struct({
type: Schema.Literal("error"),
value: Schema.Unknown,
}),
Schema.Struct({
type: Schema.Literal("content"),
value: Schema.Array(Tool.Content),
}),
]).annotate({ identifier: "LLM.ToolResult" })
export type ToolResultValue = Schema.Schema.Type<typeof toolResultValueSchema>
export const ToolResultValue = Object.assign(toolResultValueSchema, {
is: isToolResultValue,
make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue => {
if (isToolResultValue(value)) return value
if (type === "content") return { type, value: Array.isArray(value) ? value : [] }
return { type, value }
export const ToolResultValue = Object.assign(
Schema.Union([
Schema.Struct({
type: Schema.Literal("json"),
value: Schema.Unknown,
}),
Schema.Struct({
type: Schema.Literal("text"),
value: Schema.Unknown,
}),
Schema.Struct({
type: Schema.Literal("error"),
value: Schema.Unknown,
}),
Schema.Struct({
type: Schema.Literal("content"),
value: Schema.Array(Tool.Content),
}),
]).annotate({ identifier: "LLM.ToolResult" }),
{
is: isToolResultValue,
make: (value: unknown, type: ToolResultValue["type"] = "json"): ToolResultValue => {
if (isToolResultValue(value)) return value
if (type === "content") return { type, value: Array.isArray(value) ? value : [] }
return { type, value }
},
},
})
)
export type ToolResultValue = Schema.Schema.Type<typeof ToolResultValue>
export interface ToolOutput {
readonly structured: unknown
+7 -2
View File
@@ -1,6 +1,10 @@
import { Effect, JsonSchema, Schema } from "effect"
import { Tool } from "@opencode-ai/schema/tool"
import type { ToolCallPart, ToolDefinition as ToolDefinitionClass, ToolOutput as ToolOutputType } from "./schema"
import type {
ToolCallPart,
ToolDefinition as ToolDefinitionClass,
ToolOutput as ToolOutputType,
} from "./schema"
import { ToolDefinition, ToolFailure, ToolOutput } from "./schema"
/**
@@ -240,7 +244,8 @@ const project = (
): ToolOutputType =>
ToolOutput.make(
toStructuredOutput?.(output) ?? output,
toModelOutput?.({ id, parameters, output }) ?? (typeof output === "string" ? [{ type: "text", text: output }] : []),
toModelOutput?.({ id, parameters, output }) ??
(typeof output === "string" ? [{ type: "text", text: output }] : []),
)
export { ToolFailure }
+5
View File
@@ -7,6 +7,7 @@ import * as Anthropic from "../src/providers/anthropic"
import * as AnthropicCompatible from "../src/providers/anthropic-compatible"
import * as Azure from "../src/providers/azure"
import * as Cloudflare from "../src/providers/cloudflare"
import * as GitHubCopilot from "../src/providers/github-copilot"
import * as Google from "../src/providers/google"
import * as GoogleVertex from "../src/providers/google-vertex"
import * as GoogleVertexChat from "../src/providers/google-vertex-chat"
@@ -269,3 +270,7 @@ OpenAICompatible.deepseek.configure({ apiKey: "deepseek-key" }).model("deepseek-
Cloudflare.CloudflareWorkersAI.configure({ accountId: "account", apiKey: "cf-key" }).model("@cf/meta/llama")
// @ts-expect-error Cloudflare Workers AI model selectors only accept model ids.
Cloudflare.CloudflareWorkersAI.configure({ accountId: "account", apiKey: "cf-key" }).model("@cf/meta/llama", {})
GitHubCopilot.configure({ baseURL: "https://copilot.test", apiKey: "copilot-key" }).model("gpt-4.1")
// @ts-expect-error GitHub Copilot model selectors only accept model ids.
GitHubCopilot.configure({ baseURL: "https://copilot.test", apiKey: "copilot-key" }).model("gpt-4.1", {})
+18
View File
@@ -10,6 +10,7 @@ import {
OpenRouter,
XAI,
} from "@opencode-ai/ai/providers"
import * as GitHubCopilot from "@opencode-ai/ai/providers/github-copilot"
import {
OpenAIChat,
OpenAICompatibleChat,
@@ -59,6 +60,23 @@ describe("public exports", () => {
expect(XAI.provider.chat).toBe(XAI.chat)
expect(XAI.configure({ apiKey: "fixture" }).responses("grok-4.3").route.id).toBe("openai-responses")
expect(XAI.configure({ apiKey: "fixture" }).chat("grok-4.3").route.id).toBe("openai-compatible-chat")
expect(
GitHubCopilot.configure({ baseURL: "https://api.githubcopilot.test", apiKey: "fixture" }).model,
).toBeFunction()
expect(
GitHubCopilot.configure({
baseURL: "https://api.githubcopilot.test",
apiKey: "fixture",
endpoint: "responses",
}).model("mai-code-1-flash-picker").route.id,
).toBe("openai-responses")
expect(
GitHubCopilot.configure({
baseURL: "https://api.githubcopilot.test",
apiKey: "fixture",
endpoint: "chat",
}).model("gpt-5").route.id,
).toBe("openai-chat")
})
test("protocol barrels expose supported low-level routes", () => {
@@ -1,7 +1,13 @@
{
"version": 1,
"metadata": {
"tags": ["prefix:anthropic-messages-cache", "provider:anthropic", "protocol:anthropic-messages", "cache", "tool"],
"tags": [
"prefix:anthropic-messages-cache",
"provider:anthropic",
"protocol:anthropic-messages",
"cache",
"tool"
],
"name": "anthropic-messages-cache/keeps-a-long-tool-turn-inside-the-cache-lookback",
"recordedAt": "2026-07-24T16:22:29.494Z"
},
@@ -1,7 +1,12 @@
{
"version": 1,
"metadata": {
"tags": ["prefix:bedrock-converse-cache", "provider:amazon-bedrock", "protocol:bedrock-converse", "cache"],
"tags": [
"prefix:bedrock-converse-cache",
"provider:amazon-bedrock",
"protocol:bedrock-converse",
"cache"
],
"name": "bedrock-converse-cache/writes-then-reads-cachepoint-on-identical-second-call",
"recordedAt": "2026-07-23T02:29:10.955Z"
},
@@ -1,7 +1,11 @@
{
"version": 1,
"metadata": {
"tags": ["prefix:google-images", "provider:google", "protocol:google-images"],
"tags": [
"prefix:google-images",
"provider:google",
"protocol:google-images"
],
"name": "google-images/generates-an-image",
"recordedAt": "2026-07-19T16:05:51.868Z"
},
@@ -1,7 +1,11 @@
{
"version": 1,
"metadata": {
"tags": ["prefix:openai-images", "provider:openai", "protocol:openai-images"],
"tags": [
"prefix:openai-images",
"provider:openai",
"protocol:openai-images"
],
"name": "openai-images/generates-an-image",
"recordedAt": "2026-07-19T14:41:43.188Z"
},
@@ -1,7 +1,11 @@
{
"version": 1,
"metadata": {
"tags": ["prefix:openai-responses-images", "provider:openai", "protocol:openai-responses"],
"tags": [
"prefix:openai-responses-images",
"provider:openai",
"protocol:openai-responses"
],
"name": "openai-responses-images/generates-and-edits-an-image-with-the-hosted-tool",
"recordedAt": "2026-07-19T14:57:16.284Z"
},
@@ -2,7 +2,12 @@
"version": 1,
"metadata": {
"model": "anthropic/claude-sonnet-4.6",
"tags": ["prefix:openai-compatible-chat", "provider:openrouter", "protocol:openai-chat", "reasoning"],
"tags": [
"prefix:openai-compatible-chat",
"provider:openrouter",
"protocol:openai-chat",
"reasoning"
],
"name": "openrouter-reasoning",
"recordedAt": "2026-07-18T11:28:39.267Z"
},
@@ -1,7 +1,14 @@
{
"version": 1,
"metadata": {
"tags": ["prefix:pdf", "pdf", "provider:anthropic", "protocol:anthropic-messages", "tool", "tool-result"],
"tags": [
"prefix:pdf",
"pdf",
"provider:anthropic",
"protocol:anthropic-messages",
"tool",
"tool-result"
],
"name": "pdf/anthropic-tool-result",
"recordedAt": "2026-07-22T18:15:39.002Z"
},
@@ -1,7 +1,13 @@
{
"version": 1,
"metadata": {
"tags": ["prefix:pdf", "pdf", "provider:anthropic", "protocol:anthropic-messages", "user-input"],
"tags": [
"prefix:pdf",
"pdf",
"provider:anthropic",
"protocol:anthropic-messages",
"user-input"
],
"name": "pdf/anthropic-user-input",
"recordedAt": "2026-07-22T18:15:37.979Z"
},
@@ -1,7 +1,14 @@
{
"version": 1,
"metadata": {
"tags": ["prefix:pdf", "pdf", "provider:amazon-bedrock", "protocol:bedrock-converse", "tool", "tool-result"],
"tags": [
"prefix:pdf",
"pdf",
"provider:amazon-bedrock",
"protocol:bedrock-converse",
"tool",
"tool-result"
],
"name": "pdf/bedrock-tool-result",
"recordedAt": "2026-07-22T18:15:52.400Z"
},
@@ -1,7 +1,13 @@
{
"version": 1,
"metadata": {
"tags": ["prefix:pdf", "pdf", "provider:amazon-bedrock", "protocol:bedrock-converse", "user-input"],
"tags": [
"prefix:pdf",
"pdf",
"provider:amazon-bedrock",
"protocol:bedrock-converse",
"user-input"
],
"name": "pdf/bedrock-user-input",
"recordedAt": "2026-07-22T18:15:48.408Z"
},
@@ -1,7 +1,14 @@
{
"version": 1,
"metadata": {
"tags": ["prefix:pdf", "pdf", "provider:google", "protocol:gemini", "tool", "tool-result"],
"tags": [
"prefix:pdf",
"pdf",
"provider:google",
"protocol:gemini",
"tool",
"tool-result"
],
"name": "pdf/gemini-tool-result",
"recordedAt": "2026-07-22T18:21:59.606Z"
},
@@ -1,7 +1,13 @@
{
"version": 1,
"metadata": {
"tags": ["prefix:pdf", "pdf", "provider:google", "protocol:gemini", "user-input"],
"tags": [
"prefix:pdf",
"pdf",
"provider:google",
"protocol:gemini",
"user-input"
],
"name": "pdf/gemini-user-input",
"recordedAt": "2026-07-22T18:20:55.140Z"
},
@@ -1,7 +1,14 @@
{
"version": 1,
"metadata": {
"tags": ["prefix:pdf", "pdf", "provider:openai", "protocol:openai-responses", "tool", "tool-result"],
"tags": [
"prefix:pdf",
"pdf",
"provider:openai",
"protocol:openai-responses",
"tool",
"tool-result"
],
"name": "pdf/openai-tool-result",
"recordedAt": "2026-07-22T18:15:36.438Z"
},
@@ -1,7 +1,13 @@
{
"version": 1,
"metadata": {
"tags": ["prefix:pdf", "pdf", "provider:openai", "protocol:openai-responses", "user-input"],
"tags": [
"prefix:pdf",
"pdf",
"provider:openai",
"protocol:openai-responses",
"user-input"
],
"name": "pdf/openai-user-input",
"recordedAt": "2026-07-22T18:15:34.867Z"
},
@@ -1,7 +1,14 @@
{
"version": 1,
"metadata": {
"tags": ["prefix:pdf", "pdf", "provider:xai", "protocol:openai-responses", "tool", "tool-result"],
"tags": [
"prefix:pdf",
"pdf",
"provider:xai",
"protocol:openai-responses",
"tool",
"tool-result"
],
"name": "pdf/xai-tool-result",
"recordedAt": "2026-07-22T18:15:43.608Z"
},
@@ -1,7 +1,13 @@
{
"version": 1,
"metadata": {
"tags": ["prefix:pdf", "pdf", "provider:xai", "protocol:openai-responses", "user-input"],
"tags": [
"prefix:pdf",
"pdf",
"provider:xai",
"protocol:openai-responses",
"user-input"
],
"name": "pdf/xai-user-input",
"recordedAt": "2026-07-22T18:15:42.429Z"
},
@@ -2,7 +2,12 @@
"version": 1,
"metadata": {
"model": "anthropic/claude-sonnet-4.6",
"tags": ["prefix:openai-compatible-chat", "provider:vercel-ai-gateway", "protocol:openai-chat", "reasoning"],
"tags": [
"prefix:openai-compatible-chat",
"provider:vercel-ai-gateway",
"protocol:openai-chat",
"reasoning"
],
"name": "vercel-ai-gateway-reasoning",
"recordedAt": "2026-07-18T11:28:42.077Z"
},
@@ -1,7 +1,11 @@
{
"version": 1,
"metadata": {
"tags": ["prefix:xai-images", "provider:xai", "protocol:xai-images"],
"tags": [
"prefix:xai-images",
"provider:xai",
"protocol:xai-images"
],
"name": "xai-images/generates-an-image",
"recordedAt": "2026-07-19T15:56:20.098Z"
},
@@ -0,0 +1,13 @@
import { LLM } from "../../src"
import { GitHubCopilot } from "../../src/providers"
const model = GitHubCopilot.configure({ baseURL: "https://example.com" }).model("gpt-5")
LLM.request({ model, prompt: "Hello", providerOptions: { openai: { reasoningSummary: "auto" } } })
LLM.request({
model,
prompt: "Hello",
// @ts-expect-error Copilot reasoning summaries use the OpenAI union.
providerOptions: { openai: { reasoningSummary: "full" } },
})
@@ -519,8 +519,14 @@ describe("Bedrock Converse route", () => {
fixedBytes(
eventStreamBody(
["messageStart", { role: "assistant" }],
["contentBlockDelta", { contentBlockIndex: 0, delta: { reasoningContent: { text: "Let me think." } } }],
["contentBlockDelta", { contentBlockIndex: 0, delta: { reasoningContent: { signature: "sig_1" } } }],
[
"contentBlockDelta",
{ contentBlockIndex: 0, delta: { reasoningContent: { text: "Let me think." } } },
],
[
"contentBlockDelta",
{ contentBlockIndex: 0, delta: { reasoningContent: { signature: "sig_1" } } },
],
["messageStop", { stopReason: "end_turn" }],
),
),
@@ -555,7 +561,10 @@ describe("Bedrock Converse route", () => {
Effect.gen(function* () {
const body = eventStreamBody(
["messageStart", { role: "assistant" }],
["contentBlockDelta", { contentBlockIndex: 0, delta: { reasoningContent: { signature: "sig_1" } } }],
[
"contentBlockDelta",
{ contentBlockIndex: 0, delta: { reasoningContent: { signature: "sig_1" } } },
],
["contentBlockStop", { contentBlockIndex: 0 }],
["messageStop", { stopReason: "end_turn" }],
)
+26 -48
View File
@@ -102,24 +102,6 @@ describe("OpenAI Chat route", () => {
}),
)
it.effect("concatenates assistant text parts without adding separators", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
LLM.request({
model,
messages: [
Message.assistant([
{ type: "text", text: "Hello" },
{ type: "text", text: " world" },
]),
],
}),
)
expect(prepared.body.messages).toEqual([{ role: "assistant", content: "Hello world" }])
}),
)
it.effect("writes reasoning to a configured custom field on every assistant message", () =>
Effect.gen(function* () {
const prepared = yield* compileRequest(
@@ -596,7 +578,7 @@ describe("OpenAI Chat route", () => {
}),
)
expect(prepared.body.messages).toEqual([{ role: "assistant", content: "", reasoning_content: "hidden" }])
expect(prepared.body.messages).toEqual([{ role: "assistant", content: null, reasoning_content: "hidden" }])
}),
)
@@ -845,7 +827,7 @@ describe("OpenAI Chat route", () => {
}),
)
it.effect("preserves scalar reasoning after content starts", () =>
it.effect("ignores scalar reasoning after content starts", () =>
Effect.gen(function* () {
const details = [{ type: "reasoning.text", text: "detail", format: "unknown", index: 0 }]
const response = yield* LLMClient.generate(request).pipe(
@@ -861,11 +843,11 @@ describe("OpenAI Chat route", () => {
),
)
expect(response.reasoning).toBe("detailscalar")
expect(response.events.filter(LLMEvent.is.reasoningStart)).toHaveLength(2)
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(2)
expect(response.reasoning).toBe("detail")
expect(response.events.filter(LLMEvent.is.reasoningStart)).toHaveLength(1)
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1)
expect(response.message.content.find((part) => part.type === "reasoning")?.providerMetadata).toEqual({
openai: { reasoningField: "reasoning", reasoningDetails: details },
openai: { reasoningDetails: details },
})
}),
)
@@ -965,7 +947,7 @@ describe("OpenAI Chat route", () => {
expect(response.events.filter(LLMEvent.is.reasoningEnd)).toHaveLength(1)
const replay = yield* compileRequest(LLM.request({ model, messages: [response.message] }))
expect(replay.body.messages).toEqual([{ role: "assistant", content: "", reasoning_details: details }])
expect(replay.body.messages).toEqual([{ role: "assistant", content: null, reasoning_details: details }])
}),
)
@@ -1015,7 +997,7 @@ describe("OpenAI Chat route", () => {
)
expect(replay.body.messages).toEqual([
{ role: "assistant", content: "", reasoning: "firstsecond", reasoning_details: [first, second] },
{ role: "assistant", content: null, reasoning: "firstsecond", reasoning_details: [first, second] },
])
}),
)
@@ -1040,7 +1022,7 @@ describe("OpenAI Chat route", () => {
)
expect(replay.body.messages).toEqual([
{ role: "assistant", content: "", reasoning_content: "AB", reasoning_details: [detail] },
{ role: "assistant", content: null, reasoning_content: "AB", reasoning_details: [detail] },
])
}),
)
@@ -1062,7 +1044,7 @@ describe("OpenAI Chat route", () => {
)
expect(replay.body.messages).toEqual([
{ role: "assistant", content: "", reasoning_content: "thinking", reasoning_details: details },
{ role: "assistant", content: null, reasoning_content: "thinking", reasoning_details: details },
])
}),
)
@@ -1170,7 +1152,7 @@ describe("OpenAI Chat route", () => {
}),
)
it.effect("finalizes a streamed tool call when the provider ends without a finish reason", () =>
it.effect("fails a streamed tool call when the provider ends without a finish reason", () =>
Effect.gen(function* () {
const body = sseEvents(
deltaChunk({
@@ -1182,31 +1164,27 @@ describe("OpenAI Chat route", () => {
const input = LLMRequest.update(request, {
tools: [ToolDefinition.make({ name: "lookup", description: "Lookup data", inputSchema: { type: "object" } })],
})
const response = yield* LLMClient.generate(input).pipe(Effect.provide(fixedResponse(body)))
const events: LLMEvent[] = []
const streamError = yield* LLMClient.stream(input).pipe(
Stream.runForEach((event) => Effect.sync(() => events.push(event))),
Effect.flip,
Effect.provide(fixedResponse(body)),
)
const error = yield* LLMClient.generate(input).pipe(Effect.provide(fixedResponse(body)), Effect.flip)
expect(response.events).toEqual([
expect(events).toEqual([
{ type: "step-start", index: 0 },
{ type: "tool-input-start", id: "call_1", name: "lookup", providerMetadata: undefined },
{ type: "tool-input-delta", id: "call_1", name: "lookup", text: '{"query"' },
{ type: "tool-input-delta", id: "call_1", name: "lookup", text: ':"weather"}' },
{ type: "tool-input-end", id: "call_1", name: "lookup", providerMetadata: undefined },
{
type: "tool-call",
id: "call_1",
name: "lookup",
input: { query: "weather" },
providerExecuted: undefined,
providerMetadata: undefined,
},
{
type: "step-finish",
index: 0,
reason: { normalized: "tool-calls" },
usage: undefined,
providerMetadata: undefined,
},
{ type: "finish", reason: { normalized: "tool-calls" }, usage: undefined },
])
expect(events.filter(LLMEvent.is.toolCall)).toEqual([])
expect(streamError.reason).toMatchObject({
_tag: "InvalidProviderOutput",
classification: "incomplete-stream",
})
expect(streamError.message).toContain("The provider response ended unexpectedly.")
expect(error.message).toContain("The provider response ended unexpectedly.")
}),
)
@@ -300,9 +300,7 @@ describe("OpenAI-compatible Chat route", () => {
Effect.gen(function* () {
const response = yield* LLMClient.generate(
LLMRequest.update(request, {
tools: [
ToolDefinition.make({ name: "weather", description: "Get weather", inputSchema: { type: "object" } }),
],
tools: [ToolDefinition.make({ name: "weather", description: "Get weather", inputSchema: { type: "object" } })],
}),
).pipe(
Effect.provide(
@@ -177,8 +177,8 @@ test("shows all and expands historical diff summary without overlap", async ({ p
const firstUser = userMessage(undefined, {
summary: {
diffs: Array.from({ length: 12 }, (_, index) => ({
file: `src/diff-${index}.ts`,
status: "modified",
file: `src/diff-${index}.ts`,
status: "modified",
additions: 1,
deletions: 1,
patch: `@@ -1 +1 @@\n-export const value = ${index}\n+export const value = ${index + 1}`,
@@ -85,7 +85,8 @@ async function mockServers(page: Page, requests: string[]) {
const current = url.origin === serverA ? sessionA : sessionB
const directory = url.searchParams.get("directory")
if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500)
if (url.pathname === "/api/event") return sse(route)
if (url.pathname === "/api/event")
return sse(route)
if (url.pathname === "/api/health") return json(route, { pid: 1 })
if (url.pathname === "/api/session") return json(route, { data: [currentSession(current)], cursor: {} })
if (url.pathname === "/api/session/active") return json(route, { data: {} })
@@ -181,17 +181,12 @@ async function mockServers(page: Page, permissionRequests: string[], permissionR
return json(route, true)
}
if (requestDirectory && requestDirectory !== directory) return json(route, { name: "InvalidDirectory" }, 500)
if (url.pathname === "/api/event") return sse(route)
if (url.pathname === "/api/event")
return sse(route)
if (url.pathname === "/api/provider")
return json(route, {
location: { directory },
data: [
{
id: remote ? "server-b" : "server-a",
name: remote ? "Server B Provider" : "Server A Provider",
package: "test",
},
],
data: [{ id: remote ? "server-b" : "server-a", name: remote ? "Server B Provider" : "Server A Provider", package: "test" }],
})
if (url.pathname === "/api/model") return json(route, { location: { directory }, data: [model(remote)] })
if (url.pathname === "/api/model/default") return json(route, { location: { directory }, data: model(remote) })
@@ -58,7 +58,8 @@ async function mockServers(page: Page) {
const current = url.origin === serverA ? sessionA : sessionB
const directory = url.searchParams.get("directory")
if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500)
if (url.pathname === "/api/event") return sse(route, url.pathname === "/api/event")
if (url.pathname === "/api/event")
return sse(route, url.pathname === "/api/event")
if (url.pathname === "/api/health") return json(route, { pid: 1 })
if (url.pathname === "/api/session/active")
return json(route, { data: url.origin === serverB ? { [sessionB.id]: { type: "running" } } : {} })
@@ -171,8 +171,8 @@ test("keeps the review tree and terminal sized when both panels are open", async
expect(bottomGap).toBeLessThanOrEqual(16)
const lazyDiff = page.waitForRequest((request) => {
const url = new URL(request.url())
return (
url.pathname === "/api/vcs/diff" &&
return (
url.pathname === "/api/vcs/diff" &&
url.searchParams.get("location[directory]")?.replaceAll("\\", "/").endsWith("/src/branch/d00027") === true
)
})
@@ -86,10 +86,15 @@ test.describe("session timeline projection", () => {
],
{ summary: { diffs: Array.from({ length: 11 }, (_, index) => summaryDiff(index)) } },
)
const aborted = assistantMessage([{ id: "prt_before_abort", type: "text", text: "Before interruption" }], {
id: "msg_1001_assistant_aborted",
error: { name: "MessageAbortedError", data: { message: "Stopped" } },
})
const aborted = assistantMessage(
[
{ id: "prt_before_abort", type: "text", text: "Before interruption" },
],
{
id: "msg_1001_assistant_aborted",
error: { name: "MessageAbortedError", data: { message: "Stopped" } },
},
)
const failed = assistantMessage([{ id: "prt_after_abort", type: "text", text: "After interruption" }], {
id: "msg_1002_assistant_failed",
error: {
@@ -89,7 +89,8 @@ async function mockServer(page: Page) {
if (url.origin !== server) return route.fallback()
if ([`/api/session/${unresolvedSessionID}`, `/session/${unresolvedSessionID}`].includes(url.pathname))
return new Promise(() => {})
if (url.pathname === "/api/event") return sse(route)
if (url.pathname === "/api/event")
return sse(route)
if (url.pathname === "/api/session") return json(route, { data: sessions.map(currentSession), cursor: {} })
if (url.pathname === "/api/session/active") return json(route, { data: {} })
const currentSessionInfo = sessions.find((item) => url.pathname === `/api/session/${item.id}`)
@@ -300,8 +300,9 @@ export const fixture = {
.filter((message) => message.info.role === "user")
.map((message) => message.info.id),
targetPartIDs: targetMessages.flatMap(currentPartIDs),
expandedShellPartID: targetMessages.flatMap((message) => message.parts).find((part) => part.tool === "bash")!
.callID,
expandedShellPartID: targetMessages
.flatMap((message) => message.parts)
.find((part) => part.tool === "bash")!.callID,
},
}
+23 -25
View File
@@ -144,8 +144,7 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
location: location(config),
data: currentProviders(providerConfig(config)),
})
if (path === "/api/model")
return json(route, { location: location(config), data: currentModels(providerConfig(config)) })
if (path === "/api/model") return json(route, { location: location(config), data: currentModels(providerConfig(config)) })
if (path === "/api/model/default")
return json(route, { location: location(config), data: currentDefaultModel(providerConfig(config)) })
if (path === "/api/integration") return json(route, { location: location(config), data: [] })
@@ -207,8 +206,7 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
const fileRead = path.match(/^\/api\/fs\/read\/(.+)$/)?.[1]
if (fileRead && config.fileContent) {
const value = await config.fileContent(decodeURIComponent(fileRead))
const content =
value && typeof value === "object" && "content" in value ? String(value.content) : String(value ?? "")
const content = value && typeof value === "object" && "content" in value ? String(value.content) : String(value ?? "")
return route.fulfill({ status: 200, body: content, headers: { "content-type": "application/octet-stream" } })
}
if (path === "/api/fs/find" && config.findFiles) {
@@ -283,8 +281,7 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
if (/^\/api\/session\/[^/]+\/permission\/[^/]+\/reply$/.test(path) && route.request().method() === "POST") {
return route.fulfill({ status: 204, headers: { "access-control-allow-origin": "*" } })
}
if (/^\/question\/[^/]+\/(reply|reject)$/.test(path) && route.request().method() === "POST")
return json(route, true)
if (/^\/question\/[^/]+\/(reply|reject)$/.test(path) && route.request().method() === "POST") return json(route, true)
if (/^\/session\/[^/]+\/permissions\/[^/]+$/.test(path) && route.request().method() === "POST")
return json(route, true)
if (
@@ -389,13 +386,11 @@ function providerConfig(config: MockServerConfig) {
function currentProviders(value: unknown) {
if (!record(value) || !Array.isArray(value.all)) return Array.isArray(value) ? value : []
return value.all
.filter(record)
.flatMap((provider) =>
typeof provider.id === "string" && typeof provider.name === "string"
? [{ id: provider.id, name: provider.name, package: provider.id }]
: [],
)
return value.all.filter(record).flatMap((provider) =>
typeof provider.id === "string" && typeof provider.name === "string"
? [{ id: provider.id, name: provider.name, package: provider.id }]
: [],
)
}
function currentModels(value: unknown) {
@@ -445,7 +440,9 @@ function currentDefaultModel(value: unknown) {
if (!record(value) || !record(value.default)) return null
const selected = value.default
const models = currentModels(value)
return models.find((model) => model.providerID === selected.providerID && model.id === selected.modelID) ?? null
return models.find(
(model) => model.providerID === selected.providerID && model.id === selected.modelID,
) ?? null
}
function currentPermission(value: unknown) {
@@ -565,16 +562,22 @@ function legacyAgent(part: Record<string, unknown>): PromptAgentAttachment[] {
}
function mentionFrom(value: Record<string, unknown> | undefined) {
if (!value || typeof value.value !== "string" || typeof value.start !== "number" || typeof value.end !== "number")
if (
!value ||
typeof value.value !== "string" ||
typeof value.start !== "number" ||
typeof value.end !== "number"
)
return
return { text: value.value, start: value.start, end: value.end }
}
function legacyAssistantContent(part: Record<string, unknown>, created: number): SessionMessageAssistant["content"] {
function legacyAssistantContent(
part: Record<string, unknown>,
created: number,
): SessionMessageAssistant["content"] {
if (part.type === "text" && typeof part.text === "string")
return [
{ type: "text", text: part.text, ...(jsonRecord(part.metadata) ? { state: jsonRecord(part.metadata) } : {}) },
]
return [{ type: "text", text: part.text, ...(jsonRecord(part.metadata) ? { state: jsonRecord(part.metadata) } : {}) }]
if (part.type === "reasoning" && typeof part.text === "string") {
const time = record(part.time) ? part.time : undefined
return [
@@ -615,12 +618,7 @@ function legacyAssistantContent(part: Record<string, unknown>, created: number):
...(jsonRecord(part.providerResultState) ? { providerResultState: jsonRecord(part.providerResultState) } : {}),
}
if (state.status === "pending")
return [
{
...base,
state: { status: "streaming", input: typeof state.raw === "string" ? state.raw : JSON.stringify(input) },
},
]
return [{ ...base, state: { status: "streaming", input: typeof state.raw === "string" ? state.raw : JSON.stringify(input) } }]
if (state.status === "completed")
return [
{
+2 -1
View File
@@ -174,7 +174,8 @@ export async function installSseTransport<T>(
const fetch = (input: RequestInfo | URL, init?: RequestInit) => {
const request = new Request(input, init)
const url = new URL(request.url)
if (url.origin !== server || url.pathname !== "/api/event") return originalFetch(request)
if (url.origin !== server || url.pathname !== "/api/event")
return originalFetch(request)
const id = ++nextConnectionID
const record = {
+1 -4
View File
@@ -20,10 +20,7 @@
<meta property="twitter:image" content="/social-share.png" />
<script id="oc-theme-preload-script" src="/oc-theme-preload.js"></script>
</head>
<body
data-new-layout
class="antialiased overscroll-none font-(family-name:--font-family-text) text-[13px] font-[440] overflow-hidden bg-v2-background-bg-deep"
>
<body data-new-layout class="antialiased overscroll-none font-(family-name:--font-family-text) text-[13px] font-[440] overflow-hidden bg-v2-background-bg-deep">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" class="flex flex-col h-dvh bg-v2-background-bg-deep p-px"></div>
<script src="/src/entry.tsx" type="module"></script>
+8 -1
View File
@@ -9,7 +9,14 @@ import { Font } from "@opencode-ai/ui/font"
import { Splash } from "@opencode-ai/ui/logo"
import { ThemeProvider } from "@opencode-ai/ui/theme/context"
import { MetaProvider } from "@solidjs/meta"
import { type BaseRouterProps, Navigate, Route, Router, useParams, useSearchParams } from "@solidjs/router"
import {
type BaseRouterProps,
Navigate,
Route,
Router,
useParams,
useSearchParams,
} from "@solidjs/router"
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"
import { Effect } from "effect"
import {
@@ -1,4 +1,4 @@
import type { FormAnswer, IntegrationMethod, IntegrationOauthConnectOutput } from "@opencode-ai/client/promise"
import type { IntegrationMethod, IntegrationOauthConnectOutput } from "@opencode-ai/client/promise"
import { Button } from "@opencode-ai/ui/button"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { Dialog } from "@opencode-ai/ui/dialog"
@@ -40,8 +40,6 @@ import { decode64 } from "@/utils/base64"
const CUSTOM_ID = "_custom"
type ConnectMethod = Extract<IntegrationMethod, { type: "key" | "oauth" }>
type IntegrationForm = NonNullable<ConnectMethod["form"]>[number]
type StringForm = Extract<IntegrationForm, { type: "string" }>
export function useProviderConnectController(options: { onBack?: () => void } = {}) {
const [store, setStore] = createStore({ selected: undefined as string | undefined })
@@ -436,16 +434,16 @@ function ProviderConnection(props: {
const [store, setStore] = createStore({
methodIndex: undefined as undefined | number,
authorization: undefined as undefined | IntegrationOauthConnectOutput["data"],
formAnswer: undefined as FormAnswer | undefined,
state: "pending" as undefined | "pending" | "complete" | "error" | "form",
promptInputs: undefined as undefined | Record<string, string>,
state: "pending" as undefined | "pending" | "complete" | "error" | "prompt",
error: undefined as string | undefined,
})
type Action =
| { type: "method.select"; index: number }
| { type: "method.reset" }
| { type: "auth.form" }
| { type: "auth.answer"; answer: FormAnswer | undefined }
| { type: "auth.prompt" }
| { type: "auth.inputs"; inputs: Record<string, string> }
| { type: "auth.pending" }
| { type: "auth.complete"; authorization: IntegrationOauthConnectOutput["data"] }
| { type: "auth.error"; error: string }
@@ -456,7 +454,7 @@ function ProviderConnection(props: {
if (action.type === "method.select") {
draft.methodIndex = action.index
draft.authorization = undefined
draft.formAnswer = undefined
draft.promptInputs = undefined
draft.state = undefined
draft.error = undefined
return
@@ -464,18 +462,18 @@ function ProviderConnection(props: {
if (action.type === "method.reset") {
draft.methodIndex = undefined
draft.authorization = undefined
draft.formAnswer = undefined
draft.promptInputs = undefined
draft.state = undefined
draft.error = undefined
return
}
if (action.type === "auth.form") {
draft.state = "form"
if (action.type === "auth.prompt") {
draft.state = "prompt"
draft.error = undefined
return
}
if (action.type === "auth.answer") {
draft.formAnswer = action.answer
if (action.type === "auth.inputs") {
draft.promptInputs = action.inputs
draft.state = undefined
draft.error = undefined
return
@@ -533,7 +531,7 @@ function ProviderConnection(props: {
return fallback
}
async function selectMethod(index: number, answer?: FormAnswer) {
async function selectMethod(index: number, inputs?: Record<string, string>) {
if (timer.current !== undefined) {
clearTimeout(timer.current)
timer.current = undefined
@@ -542,17 +540,9 @@ function ProviderConnection(props: {
const method = methods()[index]
dispatch({ type: "method.select", index })
if (method.form?.length && !answer) {
dispatch({ type: "auth.form" })
return
}
if (method.type === "key") {
dispatch({ type: "auth.answer", answer })
return
}
if (method.type === "oauth") {
if (method.form?.some((field) => field.type !== "string")) {
dispatch({ type: "auth.error", error: "This authentication form contains unsupported fields" })
if (method.prompts?.length && !inputs) {
dispatch({ type: "auth.prompt" })
return
}
dispatch({ type: "auth.pending" })
@@ -560,7 +550,7 @@ function ProviderConnection(props: {
.api.integration.oauth.connect({
integrationID: props.provider,
methodID: method.id,
...(answer ? { answer } : {}),
inputs: inputs ?? {},
location: location(),
})
.then((x) => {
@@ -574,42 +564,41 @@ function ProviderConnection(props: {
}
}
function AuthFormView() {
function AuthPromptsView() {
const [formStore, setFormStore] = createStore({
value: {} as Record<string, string>,
index: 0,
})
const fields = createMemo<StringForm[]>(() => {
const prompts = createMemo(() => {
const value = method()
return (value?.form ?? []).flatMap((field) => (field.type === "string" ? [field] : []))
return value?.type === "oauth" ? (value.prompts ?? []) : []
})
const matches = (field: StringForm, value: Record<string, string>) => {
return (field.when ?? []).every((condition) => {
const actual = value[condition.key]
if (actual === undefined) return false
return condition.op === "eq" ? actual === condition.value : actual !== condition.value
})
const matches = (prompt: NonNullable<ReturnType<typeof prompts>[number]>, value: Record<string, string>) => {
if (!prompt.when) return true
const actual = value[prompt.when.key]
if (actual === undefined) return false
return prompt.when.op === "eq" ? actual === prompt.when.value : actual !== prompt.when.value
}
const current = createMemo(() => {
const all = fields()
const index = all.findIndex((field, index) => index >= formStore.index && matches(field, formStore.value))
const all = prompts()
const index = all.findIndex((prompt, index) => index >= formStore.index && matches(prompt, formStore.value))
if (index === -1) return
return {
index,
field: all[index],
prompt: all[index],
}
})
const valid = createMemo(() => {
const item = current()
if (!item || item.field.options) return false
if (!item.field.required) return true
return (formStore.value[item.field.key] ?? "").trim().length > 0
if (!item || item.prompt.type !== "text") return false
const value = formStore.value[item.prompt.key] ?? ""
return value.trim().length > 0
})
async function next(index: number, value: Record<string, string>) {
if (store.methodIndex === undefined) return
const next = fields().findIndex((field, i) => i > index && matches(field, value))
const next = prompts().findIndex((prompt, i) => i > index && matches(prompt, value))
if (next !== -1) {
setFormStore("index", next)
return
@@ -620,60 +609,60 @@ function ProviderConnection(props: {
async function handleSubmit(e: SubmitEvent) {
e.preventDefault()
const item = current()
if (!item || item.field.options) return
if (!item || item.prompt.type !== "text") return
if (!valid()) return
await next(item.index, formStore.value)
}
const item = () => current()
const text = createMemo(() => {
const field = item()?.field
if (!field || field.options) return
return field
const prompt = item()?.prompt
if (!prompt || prompt.type !== "text") return
return prompt
})
const select = createMemo(() => {
const field = item()?.field
if (!field?.options) return
return field
const prompt = item()?.prompt
if (!prompt || prompt.type !== "select") return
return prompt
})
return (
<form onSubmit={handleSubmit} class="flex flex-col items-start gap-4">
<Switch>
<Match when={item()?.field.options === undefined}>
<Match when={item()?.prompt.type === "text"}>
<TextField
type="text"
label={text()?.title ?? ""}
label={text()?.message ?? ""}
placeholder={text()?.placeholder}
value={text() ? (formStore.value[text()!.key] ?? "") : ""}
onChange={(value) => {
const field = text()
if (!field) return
setFormStore("value", field.key, value)
const prompt = text()
if (!prompt) return
setFormStore("value", prompt.key, value)
}}
/>
<Button class="w-auto" type="submit" size="large" variant="primary" disabled={!valid()}>
{language.t("common.continue")}
</Button>
</Match>
<Match when={item()?.field.options !== undefined}>
<Match when={item()?.prompt.type === "select"}>
<div class="w-full flex flex-col gap-1.5">
<div class="text-14-regular text-text-base">{select()?.title}</div>
<div class="text-14-regular text-text-base">{select()?.message}</div>
<div>
<List
class="px-3"
items={select()?.options ?? []}
key={(x) => x.value}
current={select()?.options?.find((x) => x.value === formStore.value[select()!.key])}
current={select()?.options.find((x) => x.value === formStore.value[select()!.key])}
onSelect={(value) => {
if (!value) return
const field = select()
if (!field) return
const prompt = select()
if (!prompt) return
const nextValue = {
...formStore.value,
[field.key]: value.value,
[prompt.key]: value.value,
}
setFormStore("value", field.key, value.value)
setFormStore("value", prompt.key, value.value)
void next(item()!.index, nextValue)
}}
>
@@ -683,7 +672,7 @@ function ProviderConnection(props: {
<div class="w-2.5 h-0.5 ml-0 bg-icon-strong-base hidden" data-slot="list-item-extra-icon" />
</div>
<span>{option.label}</span>
<span class="text-14-regular text-text-weak">{option.description}</span>
<span class="text-14-regular text-text-weak">{option.hint}</span>
</div>
)}
</List>
@@ -831,7 +820,6 @@ function ProviderConnection(props: {
integrationID: props.provider,
location: location(),
key: apiKey,
...(store.formAnswer ? { answer: store.formAnswer } : {}),
})
await complete()
}
@@ -1155,8 +1143,8 @@ function ProviderConnection(props: {
</div>
</div>
</Match>
<Match when={store.state === "form"}>
<AuthFormView />
<Match when={store.state === "prompt"}>
<AuthPromptsView />
</Match>
<Match when={store.state === "error"}>
<div class="text-14-regular text-text-base">
@@ -123,7 +123,9 @@ export const SettingsProvidersV2: Component<{
const credentials = integration.data?.connections.filter((item) => item.type === "credential") ?? []
if (credentials.length === 0) throw new Error(`No removable credentials found for ${name}`)
await Promise.all(
credentials.map((credential) => serverSdk().api.credential.remove({ credentialID: credential.id, location })),
credentials.map((credential) =>
serverSdk().api.credential.remove({ credentialID: credential.id, location }),
),
)
showToast({
variant: "success",
@@ -5,16 +5,7 @@ import { Switch } from "@opencode-ai/ui/switch"
import { Tabs } from "@opencode-ai/ui/tabs"
import { showToast } from "@/utils/toast"
import { useNavigate } from "@solidjs/router"
import {
type Accessor,
createEffect,
createMemo,
createResource,
For,
type JSXElement,
onCleanup,
Show,
} from "solid-js"
import { type Accessor, createEffect, createMemo, createResource, For, type JSXElement, onCleanup, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
import { useLanguage } from "@/context/language"
@@ -300,10 +291,7 @@ export function StatusPopoverBody(props: { shown: Accessor<boolean> }) {
const lspCount = createMemo(() => lspItems().length)
const [pluginList] = createResource(
() => (props.shown() ? sdk().directory : undefined),
(directory) =>
sdk()
.api.plugin.list({ location: { directory } })
.then((result) => result.data),
(directory) => sdk().api.plugin.list({ location: { directory } }).then((result) => result.data),
)
const plugins = createMemo(() => (pluginList.latest ?? []).map((item) => item.id))
const pluginCount = createMemo(() => plugins().length)
+4 -1
View File
@@ -190,7 +190,10 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
.find((item) => ServerConnection.key(item) === (route.server ?? server.key))
return conn ? { route, sdk: global.ensureServerCtx(conn).sdk } : undefined
},
({ route, sdk }) => sdk.api.session.get({ sessionID: route.sessionId }).catch(() => {}),
({ route, sdk }) =>
sdk.api.session
.get({ sessionID: route.sessionId })
.catch(() => {}),
)
const matchRoute = (route: LayoutRoute) => {
+3 -1
View File
@@ -124,7 +124,9 @@ export const createDirSyncContext = (
const [store, setStore] = current()
setStore("limit", (value) => value + count)
const response = await serverSDK.api.session.list({ directory, limit: store.limit, order: "desc" })
const sessions = response.data.sort((a, b) => cmp(a.id, b.id)).slice(0, store.limit)
const sessions = response.data
.sort((a, b) => cmp(a.id, b.id))
.slice(0, store.limit)
sessions.forEach(serverSync.session.remember)
setStore("session", reconcile(sessions, { key: "id" }))
},
+124 -76
View File
@@ -1,4 +1,9 @@
import type { Config, Path, Project, ProviderAuthResponse } from "@/types"
import type {
Config,
Path,
Project,
ProviderAuthResponse,
} from "@/types"
import type {
AgentListInput,
AgentListOutput,
@@ -26,7 +31,12 @@ import { batch } from "solid-js"
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
import type { State, VcsCache } from "./types"
import type { ServerSession } from "../server-session"
import { cmp, normalizeAgentList, normalizeProjectInfo, normalizeProviderList } from "./utils"
import {
cmp,
normalizeAgentList,
normalizeProjectInfo,
normalizeProviderList,
} from "./utils"
import { formatServerError } from "@/utils/server-errors"
import { QueryClient, queryOptions } from "@tanstack/solid-query"
import { loadMcpQuery, loadMcpResourcesQuery } from "../server-sync"
@@ -138,7 +148,10 @@ export async function bootstrapGlobal(input: {
}) {
const slow = [
() => input.queryClient.fetchQuery(loadGlobalConfigQuery(input.scope)),
() => input.queryClient.fetchQuery(loadProvidersQuery(input.scope, null, input.serverAPI)),
() =>
input.queryClient.fetchQuery(
loadProvidersQuery(input.scope, null, input.serverAPI),
),
() => input.queryClient.fetchQuery(loadPathQuery(input.scope, null, input.serverAPI.location)),
() =>
input.queryClient
@@ -198,7 +211,11 @@ function warmSessions(input: {
).then(() => undefined)
}
export const loadProvidersQuery = (scope: ServerScope, directory: string | null, sdk: CatalogApi) =>
export const loadProvidersQuery = (
scope: ServerScope,
directory: string | null,
sdk: CatalogApi,
) =>
queryOptions({
queryKey: [scope, directory, "providers"],
queryFn: () =>
@@ -225,16 +242,28 @@ type ReferenceListApi = {
readonly list: (input?: ReferenceListInput) => Promise<ReferenceListOutput>
}
export const loadAgentsQuery = (scope: ServerScope, directory: string, sdk: AgentListApi) =>
export const loadAgentsQuery = (
scope: ServerScope,
directory: string,
sdk: AgentListApi,
) =>
queryOptions({
queryKey: [scope, directory, "agents"],
queryFn: () => retry(() => sdk.list({ location: { directory } }).then((result) => normalizeAgentList(result.data))),
queryFn: () =>
retry(() => sdk.list({ location: { directory } }).then((result) => normalizeAgentList(result.data))),
})
export const loadCommands = (directory: string, api: CommandListApi): Promise<CommandInfo[]> =>
export const loadCommands = (
directory: string,
api: CommandListApi,
): Promise<CommandInfo[]> =>
retry(() => api.list({ location: { directory } }).then((result) => result.data))
export const loadPathQuery = (scope: ServerScope, directory: string | null, api: LocationApi) =>
export const loadPathQuery = (
scope: ServerScope,
directory: string | null,
api: LocationApi,
) =>
queryOptions<Path>({
queryKey: [scope, directory, "path"],
queryFn: () =>
@@ -247,10 +276,15 @@ export const loadPathQuery = (scope: ServerScope, directory: string | null, api:
})),
})
export const loadReferencesQuery = (scope: ServerScope, directory: string, api: ReferenceListApi) =>
export const loadReferencesQuery = (
scope: ServerScope,
directory: string,
api: ReferenceListApi,
) =>
queryOptions<ReferenceInfo[]>({
queryKey: [scope, directory, "references"] as const,
queryFn: () => retry(() => api.list({ location: { directory } }).then((result) => result.data)).catch(() => []),
queryFn: () =>
retry(() => api.list({ location: { directory } }).then((result) => result.data)).catch(() => []),
placeholderData: [],
})
@@ -319,41 +353,46 @@ export async function bootstrapDirectory(input: {
})),
input.mcp &&
(() =>
loadCommands(input.directory, input.api.command).then((commands) => input.setStore("command", commands))),
() => input.queryClient.fetchQuery(loadReferencesQuery(input.scope, input.directory, input.api.reference)),
loadCommands(input.directory, input.api.command).then((commands) =>
input.setStore("command", commands),
)),
() =>
input.queryClient.fetchQuery(
loadReferencesQuery(input.scope, input.directory, input.api.reference),
),
() =>
retry(() =>
input.api.permission.request
.list({ location: { directory: input.directory } })
.then((result) => result.data)
.then((permissions) => {
const ids = permissions.map((permission) => permission.sessionID)
const grouped = groupBySession(
permissions.filter((permission) => !!permission.id && !!permission.sessionID),
)
const warm = input.session
? Promise.all(ids.map((sessionID) => input.session!.resolve(sessionID))).then(() => undefined)
: warmSessions({ ids, store: input.store, setStore: input.setStore, api: input.api.session })
return warm.then(() =>
batch(() => {
const current = input.session?.data.permission ?? input.store.permission
for (const sessionID of Object.keys(current)) {
if (grouped[sessionID]) continue
if (input.session?.get(sessionID)?.location.directory !== input.directory) continue
if (input.session) input.session.set("permission", sessionID, [])
if (!input.session) input.setStore("permission", sessionID, [])
}
for (const [sessionID, permissions] of Object.entries(grouped)) {
const value = reconcile(
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
{ key: "id" },
)
if (input.session) input.session.set("permission", sessionID, value)
if (!input.session) input.setStore("permission", sessionID, value)
}
}),
)
}),
const ids = permissions.map((permission) => permission.sessionID)
const grouped = groupBySession(
permissions.filter((permission) => !!permission.id && !!permission.sessionID),
)
const warm = input.session
? Promise.all(ids.map((sessionID) => input.session!.resolve(sessionID))).then(() => undefined)
: warmSessions({ ids, store: input.store, setStore: input.setStore, api: input.api.session })
return warm.then(() =>
batch(() => {
const current = input.session?.data.permission ?? input.store.permission
for (const sessionID of Object.keys(current)) {
if (grouped[sessionID]) continue
if (input.session?.get(sessionID)?.location.directory !== input.directory) continue
if (input.session) input.session.set("permission", sessionID, [])
if (!input.session) input.setStore("permission", sessionID, [])
}
for (const [sessionID, permissions] of Object.entries(grouped)) {
const value = reconcile(
permissions.filter((p) => !!p?.id).sort((a, b) => cmp(a.id, b.id)),
{ key: "id" },
)
if (input.session) input.session.set("permission", sessionID, value)
if (!input.session) input.setStore("permission", sessionID, value)
}
}),
)
}),
),
() =>
retry(() =>
@@ -361,47 +400,56 @@ export async function bootstrapDirectory(input: {
.list({ location: { directory: input.directory } })
.then((result) => result.data)
.then((questions) => {
const ids = questions.map((question) => question.sessionID)
const grouped = groupBySession(
questions.filter((question) => !!question.id && !!question.sessionID) as QuestionRequest[],
)
const warm = input.session
? Promise.all(ids.map((sessionID) => input.session!.resolve(sessionID))).then(() => undefined)
: warmSessions({ ids, store: input.store, setStore: input.setStore, api: input.api.session })
return warm.then(() =>
batch(() => {
const current = input.session?.data.question ?? input.store.question
for (const sessionID of Object.keys(current)) {
if (grouped[sessionID]) continue
if (input.session?.get(sessionID)?.location.directory !== input.directory) continue
if (input.session) input.session.set("question", sessionID, [])
if (!input.session) input.setStore("question", sessionID, [])
}
for (const [sessionID, questions] of Object.entries(grouped)) {
const value = reconcile(
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
{ key: "id" },
)
if (input.session) input.session.set("question", sessionID, value)
if (!input.session) input.setStore("question", sessionID, value)
}
}),
)
}),
const ids = questions.map((question) => question.sessionID)
const grouped = groupBySession(
questions.filter((question) => !!question.id && !!question.sessionID) as QuestionRequest[],
)
const warm = input.session
? Promise.all(ids.map((sessionID) => input.session!.resolve(sessionID))).then(() => undefined)
: warmSessions({ ids, store: input.store, setStore: input.setStore, api: input.api.session })
return warm.then(() =>
batch(() => {
const current = input.session?.data.question ?? input.store.question
for (const sessionID of Object.keys(current)) {
if (grouped[sessionID]) continue
if (input.session?.get(sessionID)?.location.directory !== input.directory) continue
if (input.session) input.session.set("question", sessionID, [])
if (!input.session) input.setStore("question", sessionID, [])
}
for (const [sessionID, questions] of Object.entries(grouped)) {
const value = reconcile(
questions.filter((q) => !!q?.id).sort((a, b) => cmp(a.id, b.id)),
{ key: "id" },
)
if (input.session) input.session.set("question", sessionID, value)
if (!input.session) input.setStore("question", sessionID, value)
}
}),
)
}),
),
() => Promise.resolve(input.loadSessions(input.directory)),
input.mcp && (() => input.queryClient.fetchQuery(loadMcpQuery(input.scope, input.directory, input.api.mcp))),
input.mcp &&
(() => input.queryClient.fetchQuery(loadMcpResourcesQuery(input.scope, input.directory, input.api.mcp))),
(() =>
input.queryClient.fetchQuery(
loadMcpQuery(input.scope, input.directory, input.api.mcp),
)),
input.mcp &&
(() =>
input.queryClient.fetchQuery(
loadMcpResourcesQuery(input.scope, input.directory, input.api.mcp),
)),
() =>
input.queryClient.fetchQuery(loadProvidersQuery(input.scope, input.directory, input.api)).catch((err) => {
const project = getFilename(input.directory)
showToast({
variant: "error",
title: input.translate("toast.project.reloadFailed.title", { project }),
description: formatServerError(err, input.translate),
})
}),
input.queryClient
.fetchQuery(loadProvidersQuery(input.scope, input.directory, input.api))
.catch((err) => {
const project = getFilename(input.directory)
showToast({
variant: "error",
title: input.translate("toast.project.reloadFailed.title", { project }),
description: formatServerError(err, input.translate),
})
}),
].filter(Boolean) as (() => Promise<any>)[]
await waitForPaint()
@@ -1,13 +1,12 @@
import { Binary } from "@opencode-ai/core/util/binary"
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
import type { Message, Part, Project, Todo } from "@/types"
import type {
FileDiffInfo,
PermissionRequest,
QuestionRequest,
SessionInfo,
SessionStatus,
} from "@opencode-ai/client/promise"
Message,
Part,
Project,
Todo,
} from "@/types"
import type { FileDiffInfo, PermissionRequest, QuestionRequest, SessionInfo, SessionStatus } from "@opencode-ai/client/promise"
import type { State, VcsCache } from "./types"
import { trimSessions } from "./session-trim"
import { dropSessionCaches } from "./session-cache"
@@ -81,9 +81,9 @@ describe("Home V2 session index", () => {
])
expect(result).toEqual([
expect.objectContaining({
id: "root",
location: { directory: "/project" },
expect.objectContaining({
id: "root",
location: { directory: "/project" },
projectID: "project",
title: "root",
time: { created: 1, updated: 30 },
+10 -1
View File
@@ -1,4 +1,13 @@
import type { Agent, Config, LspStatus, Message, Part, Path, Todo, VcsInfo } from "@/types"
import type {
Agent,
Config,
LspStatus,
Message,
Part,
Path,
Todo,
VcsInfo,
} from "@/types"
import type {
FileDiffInfo,
PermissionRequest,
+280 -279
View File
@@ -67,74 +67,64 @@ function currentMessages(data: MessageResponse["data"]): SessionMessageInfo[] {
},
]
}
return [
{
id: item.info.id,
type: "assistant",
agent: item.info.agent,
model: { id: item.info.modelID, providerID: item.info.providerID, variant: item.info.variant },
content: item.parts.flatMap((part): SessionMessageAssistant["content"] => {
if (part.type === "text") return [{ type: "text", text: part.text }]
if (part.type === "reasoning")
return [
{
type: "reasoning",
text: part.text,
time: part.time ? { created: part.time.start, completed: part.time.end } : undefined,
},
]
if (part.type !== "tool") return []
const state: SessionMessageAssistantTool["state"] = (() => {
if (part.state.status === "pending")
return { status: "streaming" as const, input: JSON.stringify(part.state.input) }
if (part.state.status === "running")
return {
status: "running" as const,
input: part.state.input as CurrentToolObject,
metadata: (part.state.metadata ?? {}) as CurrentToolObject,
}
if (part.state.status === "error")
return {
status: "error" as const,
input: part.state.input as CurrentToolObject,
error: { type: "tool_error", message: part.state.error },
metadata: part.state.metadata as CurrentToolObject | undefined,
}
return {
status: "completed" as const,
input: part.state.input as CurrentToolObject,
content: [{ type: "text" as const, text: part.state.output }],
metadata: part.state.metadata as CurrentToolObject,
}
})()
return [{
id: item.info.id,
type: "assistant",
agent: item.info.agent,
model: { id: item.info.modelID, providerID: item.info.providerID, variant: item.info.variant },
content: item.parts.flatMap((part): SessionMessageAssistant["content"] => {
if (part.type === "text") return [{ type: "text", text: part.text }]
if (part.type === "reasoning")
return [
{
id: part.id,
type: "tool" as const,
name: part.tool,
state,
time: {
created: part.state.status === "pending" ? item.info.time.created : part.state.time.start,
ran: part.state.status === "pending" ? undefined : part.state.time.start,
completed:
part.state.status === "completed" || part.state.status === "error" ? part.state.time.end : undefined,
},
type: "reasoning",
text: part.text,
time: part.time ? { created: part.time.start, completed: part.time.end } : undefined,
},
]
}),
time: item.info.time,
cost: item.info.cost,
tokens: item.info.tokens,
finish: item.info.finish as
| "stop"
| "length"
| "tool-calls"
| "content-filter"
| "error"
| "unknown"
| undefined,
},
]
if (part.type !== "tool") return []
const state: SessionMessageAssistantTool["state"] = (() => {
if (part.state.status === "pending") return { status: "streaming" as const, input: JSON.stringify(part.state.input) }
if (part.state.status === "running")
return {
status: "running" as const,
input: part.state.input as CurrentToolObject,
metadata: (part.state.metadata ?? {}) as CurrentToolObject,
}
if (part.state.status === "error")
return {
status: "error" as const,
input: part.state.input as CurrentToolObject,
error: { type: "tool_error", message: part.state.error },
metadata: part.state.metadata as CurrentToolObject | undefined,
}
return {
status: "completed" as const,
input: part.state.input as CurrentToolObject,
content: [{ type: "text" as const, text: part.state.output }],
metadata: part.state.metadata as CurrentToolObject,
}
})()
return [
{
id: part.id,
type: "tool" as const,
name: part.tool,
state,
time: {
created: part.state.status === "pending" ? item.info.time.created : part.state.time.start,
ran: part.state.status === "pending" ? undefined : part.state.time.start,
completed:
part.state.status === "completed" || part.state.status === "error" ? part.state.time.end : undefined,
},
},
]
}),
time: item.info.time,
cost: item.info.cost,
tokens: item.info.tokens,
finish: item.info.finish as "stop" | "length" | "tool-calls" | "content-filter" | "error" | "unknown" | undefined,
}]
})
}
@@ -438,259 +428,270 @@ describe("server session", () => {
// V2 messages are ordered projections and do not expose V1 assistant parent IDs.
describe.skip("V1 assistant parent projections", () => {
test("backfills an assistant-only initial page through its user root", async () => {
const user = userMessage("message-1")
const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)]
const client = rootMessageClient(
[response(assistants.map((info) => ({ info, parts: [] })))],
[singleResponse(user)],
)
const store = createServerSession(client)
test("backfills an assistant-only initial page through its user root", async () => {
const user = userMessage("message-1")
const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)]
const client = rootMessageClient(
[
response(assistants.map((info) => ({ info, parts: [] }))),
],
[singleResponse(user)],
)
const store = createServerSession(client)
await store.sync("child")
await store.sync("child")
expect(client.requests).toEqual([{ sessionID: "child", limit: 20, order: "desc" }])
expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: user.id }])
expect(store.data.message.child).toEqual([user, ...assistants])
expect(store.history.more("child")).toBe(false)
})
expect(client.requests).toEqual([{ sessionID: "child", limit: 20, order: "desc" }])
expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: user.id }])
expect(store.data.message.child).toEqual([user, ...assistants])
expect(store.history.more("child")).toBe(false)
})
test("keeps assistant history when its deleted parent cannot be backfilled", async () => {
const missing = Promise.withResolvers<SingleMessageResponse>()
const assistant = assistantMessage("message-2", "message-missing")
const client = rootMessageClient([response([{ info: assistant, parts: [] }])], [missing.promise])
const store = createServerSession(client)
const loading = store.sync("child")
await client.rootRequested(1)
test("keeps assistant history when its deleted parent cannot be backfilled", async () => {
const missing = Promise.withResolvers<SingleMessageResponse>()
const assistant = assistantMessage("message-2", "message-missing")
const client = rootMessageClient([response([{ info: assistant, parts: [] }])], [missing.promise])
const store = createServerSession(client)
const loading = store.sync("child")
await client.rootRequested(1)
missing.reject(new Error("Message not found: message-missing", { cause: { status: 404 } }))
await loading
missing.reject(new Error("Message not found: message-missing", { cause: { status: 404 } }))
await loading
expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: "message-missing" }])
expect(store.data.message.child).toEqual([assistant])
expect(store.history.more("child")).toBe(false)
})
expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: "message-missing" }])
expect(store.data.message.child).toEqual([assistant])
expect(store.history.more("child")).toBe(false)
})
test("drops a cached parent when a forced refresh confirms it was deleted", async () => {
const missing = Promise.withResolvers<SingleMessageResponse>()
const parent = userMessage("message-1")
const part = textPart(parent.id)
const assistant = assistantMessage("message-2", parent.id)
const client = rootMessageClient(
[
response([
{ info: parent, parts: [part] },
{ info: assistant, parts: [] },
]),
response([{ info: assistant, parts: [] }]),
],
[missing.promise],
)
const store = createServerSession(client)
await store.sync("child")
const loading = store.sync("child", { force: true })
await client.rootRequested(1)
test("drops a cached parent when a forced refresh confirms it was deleted", async () => {
const missing = Promise.withResolvers<SingleMessageResponse>()
const parent = userMessage("message-1")
const part = textPart(parent.id)
const assistant = assistantMessage("message-2", parent.id)
const client = rootMessageClient(
[
response([
{ info: parent, parts: [part] },
{ info: assistant, parts: [] },
]),
response([{ info: assistant, parts: [] }]),
],
[missing.promise],
)
const store = createServerSession(client)
await store.sync("child")
const loading = store.sync("child", { force: true })
await client.rootRequested(1)
missing.reject(new Error(`Message not found: ${parent.id}`, { cause: { status: 404 } }))
await loading
missing.reject(new Error(`Message not found: ${parent.id}`, { cause: { status: 404 } }))
await loading
expect(store.data.message.child).toEqual([assistant])
expect(store.data.part[parent.id]).toBeUndefined()
})
expect(store.data.message.child).toEqual([assistant])
expect(store.data.part[parent.id]).toBeUndefined()
})
test("does not let an optimistic user suppress initial root backfill", async () => {
const user = userMessage("message-1")
const part = textPart(user.id)
const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)]
const client = rootMessageClient(
[response(assistants.map((info) => ({ info, parts: [] })))],
[singleResponse(user)],
)
const store = createServerSession(client)
store.optimistic.add({ sessionID: "child", message: user, parts: [part] })
test("does not let an optimistic user suppress initial root backfill", async () => {
const user = userMessage("message-1")
const part = textPart(user.id)
const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)]
const client = rootMessageClient(
[
response(assistants.map((info) => ({ info, parts: [] }))),
],
[singleResponse(user)],
)
const store = createServerSession(client)
store.optimistic.add({ sessionID: "child", message: user, parts: [part] })
await store.sync("child")
store.optimistic.remove({ sessionID: "child", messageID: user.id })
await store.sync("child")
store.optimistic.remove({ sessionID: "child", messageID: user.id })
expect(client.requests).toHaveLength(1)
expect(client.rootRequests).toHaveLength(1)
expect(store.data.message.child).toEqual([user, ...assistants])
})
expect(client.requests).toHaveLength(1)
expect(client.rootRequests).toHaveLength(1)
expect(store.data.message.child).toEqual([user, ...assistants])
})
test("backfills the parent of fetched assistants when another user is cached", async () => {
const unrelated = userMessage("message-0", { time: { created: 0 } })
const user = userMessage("message-1")
const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)]
const client = rootMessageClient(
[response([{ info: unrelated, parts: [] }]), response(assistants.map((info) => ({ info, parts: [] })))],
[singleResponse(user)],
)
const store = createServerSession(client)
await store.sync("child")
test("backfills the parent of fetched assistants when another user is cached", async () => {
const unrelated = userMessage("message-0", { time: { created: 0 } })
const user = userMessage("message-1")
const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)]
const client = rootMessageClient(
[
response([{ info: unrelated, parts: [] }]),
response(assistants.map((info) => ({ info, parts: [] }))),
],
[singleResponse(user)],
)
const store = createServerSession(client)
await store.sync("child")
await store.sync("child", { force: true })
await store.sync("child", { force: true })
expect(client.requests).toHaveLength(2)
expect(client.rootRequests).toHaveLength(1)
expect(store.data.message.child).toEqual([unrelated, user, ...assistants])
})
expect(client.requests).toHaveLength(2)
expect(client.rootRequests).toHaveLength(1)
expect(store.data.message.child).toEqual([unrelated, user, ...assistants])
})
test("preserves cached history between an injected parent and the page boundary", async () => {
const user = userMessage("message-1")
const cached = userMessage("message-3", { time: { created: 3 } })
const assistant = assistantMessage("message-4", user.id)
const client = rootMessageClient(
[response([{ info: cached, parts: [] }]), response([{ info: assistant, parts: [] }])],
[singleResponse(user)],
)
const store = createServerSession(client)
await store.sync("child")
test("preserves cached history between an injected parent and the page boundary", async () => {
const user = userMessage("message-1")
const cached = userMessage("message-3", { time: { created: 3 } })
const assistant = assistantMessage("message-4", user.id)
const client = rootMessageClient(
[response([{ info: cached, parts: [] }]), response([{ info: assistant, parts: [] }])],
[singleResponse(user)],
)
const store = createServerSession(client)
await store.sync("child")
await store.sync("child", { force: true })
await store.sync("child", { force: true })
expect(store.data.message.child).toEqual([user, cached, assistant])
})
expect(store.data.message.child).toEqual([user, cached, assistant])
})
test("refreshes a cached parent omitted by an assistant-only replacement page", async () => {
const stale = userMessage("message-1", { summary: { title: "stale", diffs: [] } })
const fresh = { ...stale, summary: { title: "fresh", diffs: [] } }
const stalePart = textPart(stale.id, { text: "stale" })
const freshPart = { ...stalePart, text: "fresh" }
const assistant = assistantMessage("message-2", stale.id)
const client = rootMessageClient(
[response([{ info: stale, parts: [stalePart] }]), response([{ info: assistant, parts: [] }])],
[singleResponse(fresh, [freshPart])],
)
const store = createServerSession(client)
await store.sync("child")
test("refreshes a cached parent omitted by an assistant-only replacement page", async () => {
const stale = userMessage("message-1", { summary: { title: "stale", diffs: [] } })
const fresh = { ...stale, summary: { title: "fresh", diffs: [] } }
const stalePart = textPart(stale.id, { text: "stale" })
const freshPart = { ...stalePart, text: "fresh" }
const assistant = assistantMessage("message-2", stale.id)
const client = rootMessageClient(
[response([{ info: stale, parts: [stalePart] }]), response([{ info: assistant, parts: [] }])],
[singleResponse(fresh, [freshPart])],
)
const store = createServerSession(client)
await store.sync("child")
await store.sync("child", { force: true })
await store.sync("child", { force: true })
expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: stale.id }])
expect(store.data.message.child).toEqual([fresh, assistant])
expect(store.data.part[stale.id]).toEqual([freshPart])
})
expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: stale.id }])
expect(store.data.message.child).toEqual([fresh, assistant])
expect(store.data.part[stale.id]).toEqual([freshPart])
})
test("refreshes a confirmed optimistic parent while preserving pending parts", async () => {
const stale = userMessage("message-1", { summary: { title: "stale", diffs: [] } })
const fresh = { ...stale, summary: { title: "fresh", diffs: [] } }
const confirmed = textPart(stale.id, { id: "confirmed", text: "stale" })
const refreshed = { ...confirmed, text: "fresh" }
const pending = textPart(stale.id, { id: "pending", text: "pending" })
const assistant = assistantMessage("message-2", stale.id)
const client = rootMessageClient(
[response([{ info: stale, parts: [confirmed] }]), response([{ info: assistant, parts: [] }])],
[singleResponse(fresh, [refreshed])],
)
const store = createServerSession(client)
store.optimistic.add({ sessionID: "child", message: stale, parts: [confirmed, pending] })
await store.sync("child")
test("refreshes a confirmed optimistic parent while preserving pending parts", async () => {
const stale = userMessage("message-1", { summary: { title: "stale", diffs: [] } })
const fresh = { ...stale, summary: { title: "fresh", diffs: [] } }
const confirmed = textPart(stale.id, { id: "confirmed", text: "stale" })
const refreshed = { ...confirmed, text: "fresh" }
const pending = textPart(stale.id, { id: "pending", text: "pending" })
const assistant = assistantMessage("message-2", stale.id)
const client = rootMessageClient(
[response([{ info: stale, parts: [confirmed] }]), response([{ info: assistant, parts: [] }])],
[singleResponse(fresh, [refreshed])],
)
const store = createServerSession(client)
store.optimistic.add({ sessionID: "child", message: stale, parts: [confirmed, pending] })
await store.sync("child")
await store.sync("child", { force: true })
await store.sync("child", { force: true })
expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: stale.id }])
expect(store.data.message.child).toEqual([fresh, assistant])
expect(store.data.part[stale.id]).toEqual([refreshed, pending])
})
expect(client.rootRequests).toEqual([{ sessionID: "child", messageID: stale.id }])
expect(store.data.message.child).toEqual([fresh, assistant])
expect(store.data.part[stale.id]).toEqual([refreshed, pending])
})
test("uses a parent received by SSE during the replacement load", async () => {
const pending = deferredResponse()
const user = userMessage("message-1")
const assistant = assistantMessage("message-2", user.id)
const client = rootMessageClient([pending.promise], [])
const store = createServerSession(client)
const loading = store.sync("child")
test("uses a parent received by SSE during the replacement load", async () => {
const pending = deferredResponse()
const user = userMessage("message-1")
const assistant = assistantMessage("message-2", user.id)
const client = rootMessageClient([pending.promise], [])
const store = createServerSession(client)
const loading = store.sync("child")
store.apply({ type: "message.updated", properties: { info: user } })
pending.resolve(response([{ info: assistant, parts: [] }]))
await loading
store.apply({ type: "message.updated", properties: { info: user } })
pending.resolve(response([{ info: assistant, parts: [] }]))
await loading
expect(client.rootRequests).toEqual([])
expect(store.data.message.child).toEqual([user, assistant])
})
expect(client.rootRequests).toEqual([])
expect(store.data.message.child).toEqual([user, assistant])
})
test("uses a successful retry over events received by a failed backfill attempt", async () => {
const failed = deferredResponse()
const user = userMessage("message-1")
const live = { ...user, agent: "stale" }
const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)]
const client = rootMessageClient(
[response(assistants.map((info) => ({ info, parts: [] })))],
[failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)],
)
const store = createServerSession(client, { retry: retryImmediately })
const loading = store.sync("child")
await client.rootRequested(1)
test("uses a successful retry over events received by a failed backfill attempt", async () => {
const failed = deferredResponse()
const user = userMessage("message-1")
const live = { ...user, agent: "stale" }
const assistants = [assistantMessage("message-2", user.id), assistantMessage("message-3", user.id)]
const client = rootMessageClient(
[
response(
assistants.map((info) => ({ info, parts: [] })),
),
],
[failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)],
)
const store = createServerSession(client, { retry: retryImmediately })
const loading = store.sync("child")
await client.rootRequested(1)
store.apply({ type: "message.updated", properties: { info: live } })
failed.reject(new Error("retry"))
await loading
store.apply({ type: "message.updated", properties: { info: live } })
failed.reject(new Error("retry"))
await loading
expect(client.requests).toHaveLength(1)
expect(client.rootRequests).toHaveLength(2)
expect(store.data.message.child).toEqual([user, ...assistants])
})
expect(client.requests).toHaveLength(1)
expect(client.rootRequests).toHaveLength(2)
expect(store.data.message.child).toEqual([user, ...assistants])
})
test("preserves newer-page events across a failed parent retry", async () => {
const failed = deferredResponse()
const user = userMessage("message-1")
const assistant = assistantMessage("message-2", user.id)
const live = { ...assistant, cost: 1 }
const client = rootMessageClient(
[response([{ info: assistant, parts: [] }])],
[failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)],
)
const store = createServerSession(client, { retry: retryImmediately })
const loading = store.sync("child")
await client.rootRequested(1)
test("preserves newer-page events across a failed parent retry", async () => {
const failed = deferredResponse()
const user = userMessage("message-1")
const assistant = assistantMessage("message-2", user.id)
const live = { ...assistant, cost: 1 }
const client = rootMessageClient(
[response([{ info: assistant, parts: [] }])],
[failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)],
)
const store = createServerSession(client, { retry: retryImmediately })
const loading = store.sync("child")
await client.rootRequested(1)
store.apply({ type: "message.updated", properties: { info: live } })
failed.reject(new Error("retry"))
await loading
store.apply({ type: "message.updated", properties: { info: live } })
failed.reject(new Error("retry"))
await loading
expect(store.data.message.child).toEqual([user, live])
})
expect(store.data.message.child).toEqual([user, live])
})
test("preserves unrelated message events across a failed parent retry", async () => {
const failed = deferredResponse()
const user = userMessage("message-1")
const assistant = assistantMessage("message-2", user.id)
const live = userMessage("message-4", { time: { created: 4 } })
const client = rootMessageClient(
[response([{ info: assistant, parts: [] }])],
[failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)],
)
const store = createServerSession(client, { retry: retryImmediately })
const loading = store.sync("child")
await client.rootRequested(1)
test("preserves unrelated message events across a failed parent retry", async () => {
const failed = deferredResponse()
const user = userMessage("message-1")
const assistant = assistantMessage("message-2", user.id)
const live = userMessage("message-4", { time: { created: 4 } })
const client = rootMessageClient(
[response([{ info: assistant, parts: [] }])],
[failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)],
)
const store = createServerSession(client, { retry: retryImmediately })
const loading = store.sync("child")
await client.rootRequested(1)
store.apply({ type: "message.updated", properties: { info: live } })
failed.reject(new Error("retry"))
await loading
store.apply({ type: "message.updated", properties: { info: live } })
failed.reject(new Error("retry"))
await loading
expect(store.data.message.child).toEqual([user, assistant, live])
})
expect(store.data.message.child).toEqual([user, assistant, live])
})
test("preserves newer-page part events across a failed parent retry", async () => {
const failed = deferredResponse()
const user = userMessage("message-1")
const assistant = assistantMessage("message-2", user.id)
const stale = textPart(assistant.id, { text: "stale" })
const live = { ...stale, text: "live" }
const client = rootMessageClient(
[response([{ info: assistant, parts: [stale] }])],
[failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)],
)
const store = createServerSession(client, { retry: retryImmediately })
const loading = store.sync("child")
await client.rootRequested(1)
test("preserves newer-page part events across a failed parent retry", async () => {
const failed = deferredResponse()
const user = userMessage("message-1")
const assistant = assistantMessage("message-2", user.id)
const stale = textPart(assistant.id, { text: "stale" })
const live = { ...stale, text: "live" }
const client = rootMessageClient(
[response([{ info: assistant, parts: [stale] }])],
[failed.promise.then((result) => ({ data: result.data[0]! })), singleResponse(user)],
)
const store = createServerSession(client, { retry: retryImmediately })
const loading = store.sync("child")
await client.rootRequested(1)
store.apply({ type: "message.part.updated", properties: { sessionID: "child", part: live, time: 2 } })
failed.reject(new Error("retry"))
await loading
store.apply({ type: "message.part.updated", properties: { sessionID: "child", part: live, time: 2 } })
failed.reject(new Error("retry"))
await loading
expect(store.data.part[assistant.id]).toEqual([live])
})
expect(store.data.part[assistant.id]).toEqual([live])
})
})
test("merges live events into the initial page", async () => {
+18 -12
View File
@@ -1,7 +1,11 @@
import { Binary } from "@opencode-ai/core/util/binary"
import { retry } from "@opencode-ai/core/util/retry"
import type { OpenCodeEvent, SessionApi, SessionInfo, SessionMessageInfo } from "@opencode-ai/client/promise"
import type { Message, Part, Todo } from "@/types"
import type {
Message,
Part,
Todo,
} from "@/types"
import type { FileDiffInfo, PermissionRequest, QuestionRequest, SessionStatus } from "@opencode-ai/client/promise"
import { batch } from "solid-js"
import { createStore, produce, reconcile } from "solid-js/store"
@@ -34,16 +38,14 @@ function projectMessageSource(message: Message): SessionMessageInfo[] {
{ id: message.id, type: "user", text: "", time: message.time },
]
}
return [
{
id: message.id,
type: "assistant",
agent: message.agent ?? message.mode,
model: { id: message.modelID, providerID: message.providerID, variant: message.variant },
content: [],
time: message.time,
},
]
return [{
id: message.id,
type: "assistant",
agent: message.agent ?? message.mode,
model: { id: message.modelID, providerID: message.providerID, variant: message.variant },
content: [],
time: message.time,
}]
}
function needsOlderTurnRoot(source: readonly SessionMessageInfo[]) {
@@ -241,7 +243,11 @@ export function createServerSession(
const indexProjectedMessage = (message: Message) => {
const current = data.session_message[message.sessionID] ?? []
if (current.some((item) => item.id === message.id)) return
setData("session_message", message.sessionID, reconcile([...current, ...projectMessageSource(message)]))
setData(
"session_message",
message.sessionID,
reconcile([...current, ...projectMessageSource(message)]),
)
}
const remember = (session: SessionInfo) => {
+14 -4
View File
@@ -1,4 +1,9 @@
import type { Config, Path, Project, ProviderAuthResponse } from "@/types"
import type {
Config,
Path,
Project,
ProviderAuthResponse,
} from "@/types"
import { showToast } from "@/utils/toast"
import { getFilename } from "@opencode-ai/core/util/path"
import { type Accessor, batch, createMemo, getOwner, onCleanup, onMount, untrack } from "solid-js"
@@ -159,11 +164,15 @@ export function seedActiveSessionStatuses(
}
}
function makeQueryOptionsApi(scope: ServerScope, serverAPI: ServerApi) {
function makeQueryOptionsApi(
scope: ServerScope,
serverAPI: ServerApi,
) {
return {
globalConfig: () => loadGlobalConfigQuery(scope),
projects: () => loadProjectsQuery(scope, serverAPI.project),
providers: (directory: PathKey | null) => loadProvidersQuery(scope, directory, serverAPI),
providers: (directory: PathKey | null) =>
loadProvidersQuery(scope, directory, serverAPI),
path: (directory: PathKey | null) => loadPathQuery(scope, directory, serverAPI.location),
agents: (directory: PathKey) => loadAgentsQuery(scope, directory, serverAPI.agent),
references: (directory: PathKey) => loadReferencesQuery(scope, directory, serverAPI.reference),
@@ -662,12 +671,13 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
integrationID: server.integrationID,
location: { directory: key },
})
const method = integration.data?.methods.find((item) => item.type === "oauth" && !item.form?.length)
const method = integration.data?.methods.find((item) => item.type === "oauth" && !item.prompts?.length)
if (!method || method.type !== "oauth")
throw new Error(`MCP server ${name} requires an interactive authentication form`)
const attempt = await serverSDK.api.integration.oauth.connect({
integrationID: server.integrationID,
methodID: method.id,
inputs: {},
location: { directory: key },
})
platform.openLink(attempt.data.url)
+4 -7
View File
@@ -268,13 +268,10 @@ function createWorkspaceTerminalSession(
const index = store.all.findIndex((x) => x.id === id)
const pty = store.all[index]
if (!pty) return
const data = await sdk.api.pty
.create({ location, title: pty.title })
.then((result) => result.data)
.catch((error: unknown) => {
console.error("Failed to clone terminal", error)
return undefined
})
const data = await sdk.api.pty.create({ location, title: pty.title }).then((result) => result.data).catch((error: unknown) => {
console.error("Failed to clone terminal", error)
return undefined
})
if (!data?.id) return
const active = store.active === pty.id
@@ -45,7 +45,13 @@ import { StickyAccordionHeader } from "@opencode-ai/ui/sticky-accordion-header"
import { TextField } from "@opencode-ai/ui/text-field"
import { TextReveal } from "@opencode-ai/ui/text-reveal"
import { TextShimmer } from "@opencode-ai/ui/text-shimmer"
import type { AssistantMessage, Message as MessageType, Part as PartType, ToolPart, UserMessage } from "@/types"
import type {
AssistantMessage,
Message as MessageType,
Part as PartType,
ToolPart,
UserMessage,
} from "@/types"
import { showToast } from "@/utils/toast"
import { getDirectory, getFilename } from "@opencode-ai/core/util/path"
import { Popover as KobaltePopover } from "@kobalte/core/popover"
@@ -54,9 +54,9 @@ describe("reviewDiffNeedsLoad", () => {
patch: "@@ -0,0 +1 @@\n+value",
}),
).toBe(false)
expect(reviewDiffNeedsLoad({ file: "empty.txt", patch: "", additions: 0, deletions: 0, status: "modified" })).toBe(
false,
)
expect(
reviewDiffNeedsLoad({ file: "empty.txt", patch: "", additions: 0, deletions: 0, status: "modified" }),
).toBe(false)
})
})
+5 -1
View File
@@ -1,4 +1,8 @@
import type { EventSubscribeOutput, FileDiffInfo, ProjectListOutput } from "@opencode-ai/client/promise"
import type {
EventSubscribeOutput,
FileDiffInfo,
ProjectListOutput,
} from "@opencode-ai/client/promise"
import type { NormalizedProviderListResponse } from "@opencode-ai/session-ui/context"
export type Project = Omit<ProjectListOutput[number], "canonical"> & { worktree: string }
+2 -12
View File
@@ -119,15 +119,7 @@ function installPackage(name) {
try {
const result = childProcess.spawnSync(
"npm",
[
"install",
"--ignore-scripts",
"--no-save",
"--loglevel=error",
"--prefix",
temp,
`${name}@${dependencies[name]}`,
],
["install", "--ignore-scripts", "--no-save", "--loglevel=error", "--prefix", temp, `${name}@${dependencies[name]}`],
{ stdio: "inherit", windowsHide: true },
)
if (result.status !== 0) return false
@@ -158,9 +150,7 @@ function main() {
}
}
throw new Error(
`Failed to install OpenCode. Try manually installing ${names.map((name) => JSON.stringify(name)).join(" or ")}.`,
)
throw new Error(`Failed to install OpenCode. Try manually installing ${names.map((name) => JSON.stringify(name)).join(" or ")}.`)
}
try {
+13 -11
View File
@@ -40,13 +40,15 @@ try {
const token = encodeURIComponent(credential)
const health = await waitForReady(info.url, headers)
if (health.pid !== info.pid) throw new Error("Health process does not match registration")
const tokenHealth = await fetch(new URL(`/api/health?auth_token=${token}`, info.url), {
signal: AbortSignal.timeout(5_000),
})
const tokenHealth = await fetch(
new URL(`/api/health?auth_token=${token}`, info.url),
{ signal: AbortSignal.timeout(5_000) },
)
if (tokenHealth.status !== 200) throw new Error("Compiled service rejected query authentication")
const tokenOpenApi = await fetch(new URL(`/openapi.json?auth_token=${token}`, info.url), {
signal: AbortSignal.timeout(5_000),
})
const tokenOpenApi = await fetch(
new URL(`/openapi.json?auth_token=${token}`, info.url),
{ signal: AbortSignal.timeout(5_000) },
)
if (tokenOpenApi.status !== 200) throw new Error("Compiled application rejected query authentication")
if ((await pluginIDs(info.url, headers)).includes("smoke")) throw new Error("Smoke plugin existed before creation")
const plugin = path.join(root, ".opencode", "plugins", "smoke.ts")
@@ -61,8 +63,7 @@ try {
const unauthorizedOpenApi = await fetch(new URL("/openapi.json", info.url), {
signal: AbortSignal.timeout(5_000),
})
if (unauthorizedOpenApi.status !== 401)
throw new Error("Compiled service exposed application routes without authentication")
if (unauthorizedOpenApi.status !== 401) throw new Error("Compiled service exposed application routes without authentication")
const unauthorizedStop = await fetch(new URL("/api/service/stop", info.url), {
method: "POST",
headers: { "content-type": "application/json" },
@@ -93,8 +94,7 @@ try {
} finally {
processes.forEach((process) => process.kill())
await Promise.all(processes.map((process) => process.exited))
if (failure)
errors.push(fs.readFile(path.join(root, "data", "opencode", "log", "opencode.log"), "utf8").catch(() => ""))
if (failure) errors.push(fs.readFile(path.join(root, "data", "opencode", "log", "opencode.log"), "utf8").catch(() => ""))
}
const output = await Promise.all(errors)
@@ -154,7 +154,9 @@ async function pluginIDs(url: string, headers: HeadersInit) {
throw new Error("Compiled service returned an invalid plugin list")
}
return body.data.flatMap((plugin) =>
typeof plugin === "object" && plugin !== null && "id" in plugin && typeof plugin.id === "string" ? [plugin.id] : [],
typeof plugin === "object" && plugin !== null && "id" in plugin && typeof plugin.id === "string"
? [plugin.id]
: [],
)
}
@@ -50,7 +50,7 @@ const login = Effect.fn("cli.console.login.run")(function* (timeline: TimelineHo
{
integrationID,
methodID: method.id,
...(server ? { answer: { server } } : {}),
inputs: server ? { server } : {},
location,
},
{ signal },
+4 -5
View File
@@ -20,8 +20,7 @@ export default Runtime.handler(
return response.text()
})
: Bun.file(input.file).text(),
catch: (cause) =>
new Error(`Failed to read session data: ${cause instanceof Error ? cause.message : String(cause)}`),
catch: (cause) => new Error(`Failed to read session data: ${cause instanceof Error ? cause.message : String(cause)}`),
})
const data = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(SessionTransfer.Data))(text)
const encoded = Schema.encodeSync(SessionTransfer.Data)(data)
@@ -53,9 +52,9 @@ export default Runtime.handler(
return
}
if (!response.ok) yield* Effect.fail(new Error(`Failed to import session: ${response.statusText}`))
const imported = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({ data: Session.Info })))(
yield* Effect.promise(() => response.text()),
)
const imported = yield* Schema.decodeUnknownEffect(
Schema.fromJsonString(Schema.Struct({ data: Session.Info })),
)(yield* Effect.promise(() => response.text()))
process.stdout.write(`Imported session: ${imported.data.id}${EOL}`)
}),
)
@@ -32,7 +32,7 @@ export default Runtime.handler(
return yield* Effect.fail(new Error(`MCP server "${input.name}" is not an OAuth-capable remote server`))
const started = yield* Effect.promise(() =>
client.integration.oauth.connect({ integrationID: integration.id, methodID: method.id, location }),
client.integration.oauth.connect({ integrationID: integration.id, methodID: method.id, inputs: {}, location }),
)
const attempt = started.data
if (attempt.mode === "code")
+3 -1
View File
@@ -36,6 +36,8 @@ export default Runtime.handler(
const hostname = new URL(endpoint.url).hostname
if (!["localhost", "127.0.0.1", "[::1]"].includes(hostname)) return
process.stderr.write(` Run \`opencode service set hostname 0.0.0.0\` to access the service remotely.${EOL}${EOL}`)
process.stderr.write(
` Run \`opencode service set hostname 0.0.0.0\` to access the service remotely.${EOL}${EOL}`,
)
}),
)
+3 -7
View File
@@ -48,16 +48,12 @@ export function migrateV1(legacy: TuiConfigV1.Info | undefined, kv: Record<strin
const attentionSoundPack = kv.attention_sound_pack
const diffView = kv.diff_viewer_view ?? (legacy?.diff_style === "stacked" ? "unified" : undefined)
const thinking =
kv.thinking_mode ?? (kv.thinking_visibility === undefined ? undefined : kv.thinking_visibility ? "show" : "hide")
kv.thinking_mode ??
(kv.thinking_visibility === undefined ? undefined : kv.thinking_visibility ? "show" : "hide")
return {
...(themeName !== undefined || themeMode !== undefined
? {
theme: {
...(themeName === undefined ? {} : { name: themeName }),
...(themeMode === undefined ? {} : { mode: themeMode }),
},
}
? { theme: { ...(themeName === undefined ? {} : { name: themeName }), ...(themeMode === undefined ? {} : { mode: themeMode }) } }
: {}),
...(legacy?.keybinds === undefined ? {} : { keybinds: legacy.keybinds }),
...(plugins.length ? { plugins } : {}),
+4 -1
View File
@@ -468,7 +468,10 @@ export async function runNonInteractivePrompt(input: Input) {
continue
}
if (event.type === "session.step.failed") {
if (input.compatibility === "v1" && event.data.error.message === "The provider response ended unexpectedly.") {
if (
input.compatibility === "v1" &&
event.data.error.message === "The provider response ended unexpectedly."
) {
pendingStep = undefined
v1InvalidOutput = true
continue
+4
View File
@@ -92,6 +92,10 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
file: process.env.OPENCODE_MODELS_PATH,
fetch: !truthy(process.env.OPENCODE_DISABLE_MODELS_FETCH),
},
observability: {
endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
headers: process.env.OTEL_EXPORTER_OTLP_HEADERS,
},
config: {
directory: process.env.OPENCODE_CONFIG_DIR,
project: !truthy(
@@ -62,7 +62,10 @@ function managedService(options: EnsureOptions) {
}
}
const resolveManaged = Effect.fnUntraced(function* (options: EnsureOptions, mismatch: NonNullable<Args["mismatch"]>) {
const resolveManaged = Effect.fnUntraced(function* (
options: EnsureOptions,
mismatch: NonNullable<Args["mismatch"]>,
) {
if (mismatch === "replace") return yield* Service.ensure(options)
if (mismatch === "ignore") return yield* Service.ensure({ ...options, version: undefined })
@@ -413,7 +413,9 @@ function UpdateFooter(props: {
const completion = smoothstep(headerFade.progress())
return Array.from({ length: width }, (_, index) => {
const color =
index >= filled ? colors.muted : shade(railRamp, Math.max(0, 1 - Math.abs(index - center) / glowRadius) ** 2)
index >= filled
? colors.muted
: shade(railRamp, Math.max(0, 1 - Math.abs(index - center) / glowRadius) ** 2)
return {
char: success || index < filled ? "━" : "·",
color: success ? blend(color, colors.accent, completion) : color,
+35 -36
View File
@@ -52,10 +52,9 @@ export const layer = Layer.effect(
const readPolicy = Effect.fnUntraced(function* () {
const values = yield* Effect.forEach(["config.json", "opencode.json", "opencode.jsonc"], (name) =>
fs.readFileString(path.join(global.config, name)).pipe(
Effect.map(decodePolicy),
Effect.catch(() => Effect.succeed(undefined)),
),
fs
.readFileString(path.join(global.config, name))
.pipe(Effect.map(decodePolicy), Effect.catch(() => Effect.succeed(undefined))),
)
return values.findLast((value) => value !== undefined) ?? true
})
@@ -95,10 +94,10 @@ export const layer = Layer.effect(
const latest = Effect.fnUntraced(function* () {
const response = yield* Effect.tryPromise({
try: () =>
fetch(`https://update.opencode.ai/api/${encodeURIComponent(channel)}/cli/npm`, {
headers: { "User-Agent": `opencode/${OPENCODE_VERSION}` },
signal: AbortSignal.timeout(10_000),
}),
fetch(
`https://update.opencode.ai/api/${encodeURIComponent(channel)}/cli/npm`,
{ headers: { "User-Agent": `opencode/${OPENCODE_VERSION}` }, signal: AbortSignal.timeout(10_000) },
),
catch: (cause) => new Error("Failed to check for updates", { cause }),
})
if (!response.ok) return yield* Effect.fail(new Error(`Update check failed with status ${response.status}`))
@@ -119,7 +118,7 @@ export const layer = Layer.effect(
pnpm: ["pnpm", "install", "--global", target],
yarn: ["yarn", "global", "add", target],
}
const result = yield* method === "bun"
const result = yield* (method === "bun"
? Effect.scoped(
Effect.gen(function* () {
// Bun does not prune old versions from its shared package cache.
@@ -128,38 +127,38 @@ export const layer = Layer.effect(
return yield* run(["bun", "install", "--global", "--cache-dir", cache, target], "5 minutes")
}),
)
: run(commands[method], "5 minutes")
: run(commands[method], "5 minutes"))
if (result.code === 0) return
return yield* Effect.fail(new Error(result.stderr.trim() || `Failed to update with ${method}`))
})
const check = Effect.fn("cli.updater.check")(
function* () {
if (OPENCODE_LOCAL || ["1", "true"].includes(process.env.OPENCODE_DISABLE_AUTOUPDATE?.toLowerCase() ?? ""))
return yield* Effect.logInfo("update check skipped", {
reason: OPENCODE_LOCAL ? "local-install" : "disabled",
version: OPENCODE_VERSION,
channel: OPENCODE_CHANNEL,
})
const policy = yield* readPolicy()
if (policy === false) return yield* Effect.logInfo("update check skipped", { reason: "policy-disabled" })
return yield* Effect.gen(function* () {
const version = yield* latest()
yield* Effect.logInfo("update check", {
current: OPENCODE_VERSION,
latest: version,
})
const next = action(OPENCODE_VERSION, version, policy)
if (next === "none") return yield* Effect.logInfo("update check done", { action: "up-to-date" })
const detected = yield* method()
if (!detected) return yield* Effect.logWarning("automatic update skipped: installation method not found")
yield* upgrade(detected, version)
yield* Effect.logInfo("updated OpenCode", { from: OPENCODE_VERSION, to: version, method: detected })
const check = Effect.fn("cli.updater.check")(function* () {
if (
OPENCODE_LOCAL ||
["1", "true"].includes(process.env.OPENCODE_DISABLE_AUTOUPDATE?.toLowerCase() ?? "")
)
return yield* Effect.logInfo("update check skipped", {
reason: OPENCODE_LOCAL ? "local-install" : "disabled",
version: OPENCODE_VERSION,
channel: OPENCODE_CHANNEL,
})
},
Effect.catchCause((cause) => Effect.logWarning("automatic update failed", { cause })),
)
const policy = yield* readPolicy()
if (policy === false) return yield* Effect.logInfo("update check skipped", { reason: "policy-disabled" })
return yield* Effect.gen(function* () {
const version = yield* latest()
yield* Effect.logInfo("update check", {
current: OPENCODE_VERSION,
latest: version,
})
const next = action(OPENCODE_VERSION, version, policy)
if (next === "none") return yield* Effect.logInfo("update check done", { action: "up-to-date" })
const detected = yield* method()
if (!detected) return yield* Effect.logWarning("automatic update skipped: installation method not found")
yield* upgrade(detected, version)
yield* Effect.logInfo("updated OpenCode", { from: OPENCODE_VERSION, to: version, method: detected })
})
}, Effect.catchCause((cause) => Effect.logWarning("automatic update failed", { cause })))
return Service.of({ check })
}),
+2 -2
View File
@@ -141,7 +141,7 @@ export async function createTimelineHost(): Promise<TimelineHost> {
if (closeTask) return closeTask
closed = true
closeTask = (async () => {
await active?.catch(() => {})
await active?.catch(() => { })
signals.forEach((signal) => process.off(signal, cancel))
})()
return closeTask
@@ -211,7 +211,7 @@ export async function createTimelineHost(): Promise<TimelineHost> {
if (closeTask) return closeTask
closed = true
closeTask = (async () => {
await active?.catch(() => {})
await active?.catch(() => { })
try {
await shutdown(activeRenderer)
await bounded(renderTask)
+4 -2
View File
@@ -102,7 +102,9 @@ test("migrates before the first update and does not remigrate afterward", async
draft.animations = false
draft.mouse = false
})
yield* Effect.promise(() => Bun.write(path.join(directory, "tui.json"), JSON.stringify({ theme: "changed" })))
yield* Effect.promise(() =>
Bun.write(path.join(directory, "tui.json"), JSON.stringify({ theme: "changed" })),
)
return yield* service.get()
}),
)
@@ -120,7 +122,7 @@ test("migrates before the first update and does not remigrate afterward", async
test("updates a config draft while preserving JSONC comments", async () => {
const directory = await Bun.$`mktemp -d`.text().then((value) => value.trim())
await Bun.write(path.join(directory, "cli.json"), '{\n // Keep this comment\n "animations": true\n}\n')
await Bun.write(path.join(directory, "cli.json"), "{\n // Keep this comment\n \"animations\": true\n}\n")
try {
const config = await run(
@@ -17,9 +17,7 @@ export default defineScript({
const session = `mini-stage2-${process.pid}`
const snapshots = path.join(artifacts, "mini-stage2")
const explicitDirectory = path.join(artifacts, "explicit-model")
yield* Effect.promise(() =>
Promise.all([snapshots, explicitDirectory].map((dir) => mkdir(dir, { recursive: true }))),
)
yield* Effect.promise(() => Promise.all([snapshots, explicitDirectory].map((dir) => mkdir(dir, { recursive: true }))))
/** @param {string} directory @param {string | undefined} model */
const mini = (directory, model) => [
"env",
@@ -171,7 +169,14 @@ export default defineScript({
yield* Effect.promise(() => tmux(["clear-history", "-t", session]))
yield* Effect.promise(() =>
tmux(["respawn-pane", "-k", "-t", session, "--", ...mini(explicitDirectory, "simulation/gpt-sim-model")]),
tmux([
"respawn-pane",
"-k",
"-t",
session,
"--",
...mini(explicitDirectory, "simulation/gpt-sim-model"),
]),
)
const explicitModel = yield* Effect.promise(() => waitForPane(session, "Simulated Model", 15_000))
yield* Effect.promise(() => Bun.write(path.join(snapshots, "07-explicit-model.txt"), explicitModel))
+8 -1
View File
@@ -159,7 +159,14 @@ test("import validates a file and sends it to the resolved location", async () =
})
try {
const [stdout, , exitCode] = await run(["import", file, "--directory", root, "--server", server.url.toString()])
const [stdout, , exitCode] = await run([
"import",
file,
"--directory",
root,
"--server",
server.url.toString(),
])
expect(exitCode).toBe(0)
expect(stdout).toBe(`Imported session: ${info.id}${os.EOL}`)
+7 -1
View File
@@ -3,7 +3,13 @@ import { readFile } from "node:fs/promises"
import { createRequire } from "node:module"
import { defineConfig, type Plugin, type UserConfig } from "vite"
import solid from "vite-plugin-solid"
import { nodeExecArgv, nodeTarget, type NodeTarget, photonWasmAsset, shellParserWasmAssets } from "./src/node/target"
import {
nodeExecArgv,
nodeTarget,
type NodeTarget,
photonWasmAsset,
shellParserWasmAssets,
} from "./src/node/target"
const dir = import.meta.dirname
+6 -1
View File
@@ -1,6 +1,11 @@
import { NodeFileSystem } from "@effect/platform-node"
import { compile, emitEffectImported, emitEffectShape, emitPromise, write } from "@opencode-ai/httpapi-codegen"
import { ClientApi, effectOmitEndpoints, groupNames, promiseOmitEndpoints } from "@opencode-ai/protocol/client"
import {
ClientApi,
effectOmitEndpoints,
groupNames,
promiseOmitEndpoints,
} from "@opencode-ai/protocol/client"
import { Agent } from "@opencode-ai/schema/agent"
import { Command } from "@opencode-ai/schema/command"
import { Config } from "@opencode-ai/schema/config"
+6 -1
View File
@@ -1 +1,6 @@
export { ClientApi, effectOmitEndpoints, groupNames, promiseOmitEndpoints } from "@opencode-ai/protocol/client"
export {
ClientApi,
effectOmitEndpoints,
groupNames,
promiseOmitEndpoints,
} from "@opencode-ai/protocol/client"
@@ -1 +1,3 @@
["api.ts"]
[
"api.ts"
]
+2 -5
View File
@@ -23,9 +23,9 @@ import type { Shell } from "@opencode-ai/schema/shell"
import type { DateTime } from "effect"
import type { Provider } from "@opencode-ai/schema/provider"
import type { Integration } from "@opencode-ai/schema/integration"
import type { Form } from "@opencode-ai/schema/form"
import type { Mcp } from "@opencode-ai/schema/mcp"
import type { Credential } from "@opencode-ai/schema/credential"
import type { Form } from "@opencode-ai/schema/form"
import type { Permission } from "@opencode-ai/schema/permission"
import type { PermissionSaved } from "@opencode-ai/schema/permission-saved"
import type { FileSystem } from "@opencode-ai/schema/filesystem"
@@ -180,7 +180,6 @@ export type Endpoint5_12Input = {
readonly text: string
readonly files?: ReadonlyArray<PromptInput.FileAttachment> | undefined
readonly agents?: ReadonlyArray<AgentAttachment> | undefined
readonly skills?: ReadonlyArray<PromptInput.SkillAttachment> | undefined
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly delivery?: "steer" | "queue" | undefined
readonly resume?: boolean | undefined
@@ -197,7 +196,6 @@ export type Endpoint5_13Input = {
readonly model?: Model.Ref | undefined
readonly files?: ReadonlyArray<PromptInput.FileAttachment> | undefined
readonly agents?: ReadonlyArray<AgentAttachment> | undefined
readonly skills?: ReadonlyArray<PromptInput.SkillAttachment> | undefined
readonly delivery?: "steer" | "queue" | undefined
readonly resume?: boolean | undefined
}
@@ -1054,7 +1052,6 @@ export type Endpoint10_3Input = {
readonly integrationID: Integration.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
readonly key: string
readonly answer?: Form.Answer | undefined
readonly label?: string | undefined
}
export type Endpoint10_3Output = void
@@ -1066,7 +1063,7 @@ export type Endpoint10_4Input = {
readonly integrationID: Integration.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
readonly methodID: Integration.MethodID
readonly answer?: Form.Answer | undefined
readonly inputs: { readonly [x: string]: string }
readonly label?: string | undefined
}
export type Endpoint10_4Output = { readonly location: Location.Info; readonly data: Integration.Attempt }
@@ -1 +1,5 @@
["client-error.ts", "client.ts", "index.ts"]
[
"client-error.ts",
"client.ts",
"index.ts"
]
@@ -412,7 +412,6 @@ const Endpoint5_12 = (raw: RawClient["server.session"]) => (input: Endpoint5_12I
text: input["text"],
files: input["files"],
agents: input["agents"],
skills: input["skills"],
metadata: input["metadata"],
delivery: input["delivery"],
resume: input["resume"],
@@ -435,7 +434,6 @@ const Endpoint5_13 = (raw: RawClient["server.session"]) => (input: Endpoint5_13I
model: input["model"],
files: input["files"],
agents: input["agents"],
skills: input["skills"],
delivery: input["delivery"],
resume: input["resume"],
},
@@ -717,7 +715,7 @@ const Endpoint10_3 = (raw: RawClient["server.integration"]) => (input: Endpoint1
raw["integration.connect.key"]({
params: { integrationID: input["integrationID"] },
query: { location: input["location"] },
payload: { key: input["key"], answer: input["answer"], label: input["label"] },
payload: { key: input["key"], label: input["label"] },
}).pipe(Effect.mapError(mapClientError)),
)
@@ -726,7 +724,7 @@ const Endpoint10_4 = (raw: RawClient["server.integration"]) => (input: Endpoint1
raw["integration.oauth.connect"]({
params: { integrationID: input["integrationID"] },
query: { location: input["location"] },
payload: { methodID: input["methodID"], answer: input["answer"], label: input["label"] },
payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
}).pipe(Effect.mapError(mapClientError)),
)
@@ -1 +1,6 @@
["client-error.ts", "client.ts", "index.ts", "types.ts"]
[
"client-error.ts",
"client.ts",
"index.ts",
"types.ts"
]
@@ -615,7 +615,6 @@ export function make(options: ClientOptions) {
text: input["text"],
files: input["files"],
agents: input["agents"],
skills: input["skills"],
metadata: input["metadata"],
delivery: input["delivery"],
resume: input["resume"],
@@ -639,7 +638,6 @@ export function make(options: ClientOptions) {
model: input["model"],
files: input["files"],
agents: input["agents"],
skills: input["skills"],
delivery: input["delivery"],
resume: input["resume"],
},
@@ -1032,7 +1030,7 @@ export function make(options: ClientOptions) {
method: "POST",
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/key`,
query: { location: input["location"] },
body: { key: input["key"], answer: input["answer"], label: input["label"] },
body: { key: input["key"], label: input["label"] },
successStatus: 204,
declaredStatuses: [400, 401],
empty: true,
@@ -1047,7 +1045,7 @@ export function make(options: ClientOptions) {
method: "POST",
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`,
query: { location: input["location"] },
body: { methodID: input["methodID"], answer: input["answer"], label: input["label"] },
body: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
successStatus: 200,
declaredStatuses: [400, 401],
empty: false,
+80 -205
View File
@@ -41,7 +41,6 @@ export type SessionMessageAgentSelected = {
time: { created: number }
type: "agent-switched"
agent: string
previous?: string
}
export type PromptBase64 = string
@@ -196,18 +195,12 @@ export type ProviderInfo = {
body?: { [x: string]: any }
}
export type FormWhen = {
key: string
op: "eq" | "neq"
value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}
export type FormOption = { value: string; label: string; description?: string }
export type FormExternalField = { key: string; type: "external"; url: string; title?: string; description?: string }
export type IntegrationWhen = { key: string; op: "eq" | "neq"; value: string }
export type IntegrationCommandMethod = { id: string; type: "command"; label: string; command: Array<string> }
export type IntegrationKeyMethod = { type: "key"; label?: string }
export type IntegrationEnvMethod = { type: "env"; names: Array<string> }
export type ConnectionCredentialInfo = { type: "credential"; id: string; label: string }
@@ -292,6 +285,16 @@ export type ProjectDirectory = { directory: string; strategy?: string }
export type FormMetadata = { [x: string]: JsonValue }
export type FormWhen = {
key: string
op: "eq" | "neq"
value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean
}
export type FormOption = { value: string; label: string; description?: string }
export type FormExternalField = { key: string; type: "external"; url: string; title?: string; description?: string }
export type FormValue = string | number | boolean | Array<string>
export type PermissionSource = { type: "tool"; messageID: string; id: string }
@@ -1077,8 +1080,6 @@ export type PromptFileAttachment = {
export type PromptAgentAttachment = { name: string; mention?: PromptMention }
export type PromptSkillAttachment = { id: string; name: string; text: string; mention?: PromptMention }
export type SessionMessageAssistantText = { type: "text"; text: string; state?: SessionMessageProviderState }
export type SessionMessageAssistantReasoning = {
@@ -1274,6 +1275,45 @@ export type ModelCost = {
cache: { read: MoneyUSDPerMillionTokens; write: MoneyUSDPerMillionTokens }
}
export type IntegrationTextPrompt = {
type: "text"
key: string
message: string
placeholder?: string
when?: IntegrationWhen
}
export type IntegrationSelectPrompt = {
type: "select"
key: string
message: string
options: Array<{ label: string; value: string; hint?: string }>
when?: IntegrationWhen
}
export type ConnectionInfo = ConnectionCredentialInfo | ConnectionEnvInfo
export type McpServer = {
name: string
status: McpStatusConnected | McpStatusPending | McpStatusDisabled | McpStatusFailed | McpStatusNeedsAuth
integrationID?: string
}
export type McpResourceCatalog = { resources: Array<McpResource>; templates: Array<McpResourceTemplate> }
export type Project = {
id: string
canonical: string
vcs?: ProjectVcs
name?: string
icon?: ProjectIcon
commands?: ProjectCommands
time: ProjectTime
sandboxes: Array<string>
}
export type ProjectDirectories = Array<ProjectDirectory>
export type FormNumberField = {
key: string
title?: string
@@ -1339,29 +1379,6 @@ export type FormMultiselectField = {
default?: Array<string>
}
export type ConnectionInfo = ConnectionCredentialInfo | ConnectionEnvInfo
export type McpServer = {
name: string
status: McpStatusConnected | McpStatusPending | McpStatusDisabled | McpStatusFailed | McpStatusNeedsAuth
integrationID?: string
}
export type McpResourceCatalog = { resources: Array<McpResource>; templates: Array<McpResourceTemplate> }
export type Project = {
id: string
canonical: string
vcs?: ProjectVcs
name?: string
icon?: ProjectIcon
commands?: ProjectCommands
time: ProjectTime
sandboxes: Array<string>
}
export type ProjectDirectories = Array<ProjectDirectory>
export type FormAnswer = { [x: string]: FormValue }
export type PermissionRequest = {
@@ -1548,7 +1565,6 @@ export type SessionMessageUser = {
text: string
files?: Array<PromptFileAttachment>
agents?: Array<PromptAgentAttachment>
skills?: Array<PromptSkillAttachment>
type: "user"
}
@@ -1556,7 +1572,6 @@ export type SessionPendingUserData = {
text: string
files?: Array<PromptFileAttachment>
agents?: Array<PromptAgentAttachment>
skills?: Array<PromptSkillAttachment>
metadata?: { [x: string]: JsonValue }
}
@@ -1564,7 +1579,6 @@ export type SessionPendingUserData1 = {
text: string
files?: Array<PromptFileAttachment>
agents?: Array<PromptAgentAttachment>
skills?: Array<PromptSkillAttachment>
metadata?: { [x: string]: any }
}
@@ -1645,6 +1659,13 @@ export type ModelInfo = {
limit: { context: number; input?: number; output: number }
}
export type IntegrationOAuthMethod = {
id: string
type: "oauth"
label: string
prompts?: Array<IntegrationTextPrompt | IntegrationSelectPrompt>
}
export type FormField =
| FormStringField
| FormNumberField
@@ -1898,9 +1919,15 @@ export type SessionMessageAssistantTool = {
time: { created: number; ran?: number; completed?: number }
}
export type IntegrationMethod =
| IntegrationOAuthMethod
| IntegrationCommandMethod
| IntegrationKeyMethod
| IntegrationEnvMethod
export type FormFields = [FormField, ...Array<FormField>]
export type FormFields3 = [FormField1, ...Array<FormField1>]
export type FormFields1 = [FormField1, ...Array<FormField1>]
export type SessionPendingInfo = SessionPendingUser | SessionPendingSynthetic | SessionPendingCompaction
@@ -1922,13 +1949,16 @@ export type SessionMessageAssistant = {
retry?: SessionMessageAssistantRetry
}
export type IntegrationOAuthMethod = { id: string; type: "oauth"; label: string; form?: FormFields }
export type IntegrationKeyMethod = { type: "key"; label?: string; form?: FormFields }
export type IntegrationInfo = {
id: string
name: string
methods: Array<IntegrationMethod>
connections: Array<ConnectionInfo>
}
export type FormInfo = { id: string; sessionID: string; title: string; metadata?: FormMetadata; fields: FormFields }
export type FormInfo1 = { id: string; sessionID: string; title: string; metadata?: FormMetadata1; fields: FormFields3 }
export type FormInfo1 = { id: string; sessionID: string; title: string; metadata?: FormMetadata1; fields: FormFields1 }
export type SessionInputAdmitted = {
id: string
@@ -1951,12 +1981,6 @@ export type SessionMessageInfo =
| SessionMessageAssistant
| SessionMessageCompaction
export type IntegrationMethod =
| IntegrationOAuthMethod
| IntegrationCommandMethod
| IntegrationKeyMethod
| IntegrationEnvMethod
export type FormCreated = {
id: string
created: number
@@ -2017,13 +2041,6 @@ export type SessionMessagesResponse = {
cursor: { previous?: string | null; next?: string | null }
}
export type IntegrationInfo = {
id: string
name: string
methods: Array<IntegrationMethod>
connections: Array<ConnectionInfo>
}
export type V2Event =
| ModelsDevRefreshed
| IntegrationUpdated
@@ -2536,7 +2553,6 @@ export type SessionImportInput = {
readonly time: { readonly created: number }
readonly type: "agent-switched"
readonly agent: string
readonly previous?: string
}
| {
readonly id: string
@@ -2563,12 +2579,6 @@ export type SessionImportInput = {
readonly name: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly skills?: ReadonlyArray<{
readonly id: string
readonly name: string
readonly text: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly type: "user"
}
| {
@@ -2788,7 +2798,6 @@ export type SessionImportInput = {
readonly time: { readonly created: number }
readonly type: "agent-switched"
readonly agent: string
readonly previous?: string
}
| {
readonly id: string
@@ -2815,12 +2824,6 @@ export type SessionImportInput = {
readonly name: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly skills?: ReadonlyArray<{
readonly id: string
readonly name: string
readonly text: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly type: "user"
}
| {
@@ -3040,7 +3043,6 @@ export type SessionImportInput = {
readonly time: { readonly created: number }
readonly type: "agent-switched"
readonly agent: string
readonly previous?: string
}
| {
readonly id: string
@@ -3067,12 +3069,6 @@ export type SessionImportInput = {
readonly name: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly skills?: ReadonlyArray<{
readonly id: string
readonly name: string
readonly text: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly type: "user"
}
| {
@@ -3324,10 +3320,6 @@ export type SessionPromptInput = {
readonly name: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly skills?: ReadonlyArray<{
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly metadata?: { readonly [x: string]: JsonValue }
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
@@ -3345,10 +3337,6 @@ export type SessionPromptInput = {
readonly name: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly skills?: ReadonlyArray<{
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly metadata?: { readonly [x: string]: JsonValue }
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
@@ -3366,10 +3354,6 @@ export type SessionPromptInput = {
readonly name: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly skills?: ReadonlyArray<{
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly metadata?: { readonly [x: string]: JsonValue }
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
@@ -3387,35 +3371,10 @@ export type SessionPromptInput = {
readonly name: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly skills?: ReadonlyArray<{
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly metadata?: { readonly [x: string]: JsonValue }
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
}["agents"]
readonly skills?: {
readonly id?: string | null
readonly text: string
readonly files?: ReadonlyArray<{
readonly uri: string
readonly name?: string
readonly description?: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly agents?: ReadonlyArray<{
readonly name: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly skills?: ReadonlyArray<{
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly metadata?: { readonly [x: string]: JsonValue }
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
}["skills"]
readonly metadata?: {
readonly id?: string | null
readonly text: string
@@ -3429,10 +3388,6 @@ export type SessionPromptInput = {
readonly name: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly skills?: ReadonlyArray<{
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly metadata?: { readonly [x: string]: JsonValue }
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
@@ -3450,10 +3405,6 @@ export type SessionPromptInput = {
readonly name: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly skills?: ReadonlyArray<{
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly metadata?: { readonly [x: string]: JsonValue }
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
@@ -3471,10 +3422,6 @@ export type SessionPromptInput = {
readonly name: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly skills?: ReadonlyArray<{
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly metadata?: { readonly [x: string]: JsonValue }
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
@@ -3501,10 +3448,6 @@ export type SessionCommandInput = {
readonly name: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly skills?: ReadonlyArray<{
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
}["id"]
@@ -3524,10 +3467,6 @@ export type SessionCommandInput = {
readonly name: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly skills?: ReadonlyArray<{
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
}["command"]
@@ -3547,10 +3486,6 @@ export type SessionCommandInput = {
readonly name: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly skills?: ReadonlyArray<{
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
}["arguments"]
@@ -3570,10 +3505,6 @@ export type SessionCommandInput = {
readonly name: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly skills?: ReadonlyArray<{
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
}["agent"]
@@ -3593,10 +3524,6 @@ export type SessionCommandInput = {
readonly name: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly skills?: ReadonlyArray<{
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
}["model"]
@@ -3616,10 +3543,6 @@ export type SessionCommandInput = {
readonly name: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly skills?: ReadonlyArray<{
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
}["files"]
@@ -3639,36 +3562,9 @@ export type SessionCommandInput = {
readonly name: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly skills?: ReadonlyArray<{
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
}["agents"]
readonly skills?: {
readonly id?: string | null
readonly command: string
readonly arguments?: string | null
readonly agent?: string | null
readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null
readonly files?: ReadonlyArray<{
readonly uri: string
readonly name?: string
readonly description?: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly agents?: ReadonlyArray<{
readonly name: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly skills?: ReadonlyArray<{
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
}["skills"]
readonly delivery?: {
readonly id?: string | null
readonly command: string
@@ -3685,10 +3581,6 @@ export type SessionCommandInput = {
readonly name: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly skills?: ReadonlyArray<{
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
}["delivery"]
@@ -3708,10 +3600,6 @@ export type SessionCommandInput = {
readonly name: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly skills?: ReadonlyArray<{
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly resume?: boolean | null
}["resume"]
@@ -4026,21 +3914,8 @@ export type IntegrationConnectKeyInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly key: {
readonly key: string
readonly answer?: { readonly [x: string]: string | number | boolean | ReadonlyArray<string> } | undefined
readonly label?: string | undefined
}["key"]
readonly answer?: {
readonly key: string
readonly answer?: { readonly [x: string]: string | number | boolean | ReadonlyArray<string> } | undefined
readonly label?: string | undefined
}["answer"]
readonly label?: {
readonly key: string
readonly answer?: { readonly [x: string]: string | number | boolean | ReadonlyArray<string> } | undefined
readonly label?: string | undefined
}["label"]
readonly key: { readonly key: string; readonly label?: string | undefined }["key"]
readonly label?: { readonly key: string; readonly label?: string | undefined }["label"]
}
export type IntegrationConnectKeyOutput = void
@@ -4052,17 +3927,17 @@ export type IntegrationOauthConnectInput = {
}["location"]
readonly methodID: {
readonly methodID: string
readonly answer?: { readonly [x: string]: string | number | boolean | ReadonlyArray<string> } | undefined
readonly inputs: { readonly [x: string]: string }
readonly label?: string | undefined
}["methodID"]
readonly answer?: {
readonly inputs: {
readonly methodID: string
readonly answer?: { readonly [x: string]: string | number | boolean | ReadonlyArray<string> } | undefined
readonly inputs: { readonly [x: string]: string }
readonly label?: string | undefined
}["answer"]
}["inputs"]
readonly label?: {
readonly methodID: string
readonly answer?: { readonly [x: string]: string | number | boolean | ReadonlyArray<string> } | undefined
readonly inputs: { readonly [x: string]: string }
readonly label?: string | undefined
}["label"]
}
+4 -2
View File
@@ -66,7 +66,8 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
if (registration.timedOut && registration.info !== undefined) {
timeouts = {
info: registration.info,
count: timeouts !== undefined && same(timeouts.info, registration.info) ? timeouts.count + 1 : 1,
count:
timeouts !== undefined && same(timeouts.info, registration.info) ? timeouts.count + 1 : 1,
}
if (timeouts.count >= 3) {
announce("missing")
@@ -187,7 +188,8 @@ async function probeResult(info: Info, allowLegacy = false) {
const body = result.value.body
if (body !== undefined && "version" in body && "pid" in body) {
if (body.pid !== info.pid) return { service: undefined, timedOut: false }
if (info.version !== undefined && body.version !== info.version) return { service: undefined, timedOut: false }
if (info.version !== undefined && body.version !== info.version)
return { service: undefined, timedOut: false }
return {
service: {
info,
+8 -2
View File
@@ -9,7 +9,12 @@ if (mode === "record-start") {
}
if (mode === "signal") process.kill(process.pid, process.platform === "win32" ? "SIGTERM" : "SIGKILL")
if (mode === "delayed" || mode === "delayed-failed" || mode === "coordinated" || mode === "coordinated-failed-loser") {
if (
mode === "delayed" ||
mode === "delayed-failed" ||
mode === "coordinated" ||
mode === "coordinated-failed-loser"
) {
await appendFile(registration + ".starts", process.pid + "\n")
const owner = await writeFile(registration + ".owner", String(process.pid), { flag: "wx" })
.then(() => true)
@@ -54,7 +59,8 @@ const server = Bun.serve({
if (mode === "legacy") return Response.json({ healthy: true })
if (mode === "starting" && !(await Bun.file(registration + ".release").exists()))
return Response.json({ healthy: true, version, pid: process.pid }, { status: 503 })
if (mode === "failed-owner") return Response.json({ healthy: true, version, pid: process.pid }, { status: 500 })
if (mode === "failed-owner")
return Response.json({ healthy: true, version, pid: process.pid }, { status: 500 })
if (mode === "starting" || mode === "graceful" || mode === "reject-stop")
return Response.json({ healthy: true, version, pid: process.pid })
return Response.json({ healthy: true, version, pid: process.pid })
-43
View File
@@ -148,49 +148,6 @@ test("experimental wellknown integration add uses the public HTTP contract", asy
expect(await request?.json()).toEqual({ url: "https://example.com" })
})
test("integration connections optionally submit a form answer", async () => {
const requests: Request[] = []
const client = OpenCode.make({
baseUrl: "http://localhost:3000",
fetch: async (input, init) => {
const request = input instanceof Request ? input : new Request(input, init)
requests.push(request)
if (request.url.endsWith("/connect/key")) return new Response(null, { status: 204 })
return Response.json({
location: { directory: "/tmp/project", project: { id: "proj_test", directory: "/tmp/project" } },
data: {
attemptID: "con_test",
url: "https://example.com/authorize",
instructions: "Authorize",
mode: "auto",
time: { created: 1, expires: 2 },
},
})
},
})
await client.integration.connect.key({
integrationID: "cloudflare-workers-ai",
key: "secret",
answer: { accountId: "account" },
})
await client.integration.oauth.connect({
integrationID: "github-copilot",
methodID: "device",
answer: { deploymentType: "enterprise", enabled: true, scopes: ["read:user"] },
})
await client.integration.connect.key({ integrationID: "openai", key: "secret" })
await client.integration.oauth.connect({ integrationID: "openai", methodID: "device" })
expect(await requests[0].json()).toEqual({ key: "secret", answer: { accountId: "account" } })
expect(await requests[1].json()).toEqual({
methodID: "device",
answer: { deploymentType: "enterprise", enabled: true, scopes: ["read:user"] },
})
expect(await requests[2].json()).toEqual({ key: "secret" })
expect(await requests[3].json()).toEqual({ methodID: "device" })
})
test("health.stop sends exact replacement identity", async () => {
let request: Request | undefined
const client = OpenCode.make({
+20 -5
View File
@@ -93,10 +93,16 @@
},
"role": {
"type": "string",
"enum": ["admin", "member"]
"enum": [
"admin",
"member"
]
}
},
"required": ["name", "email"],
"required": [
"name",
"email"
],
"additionalProperties": false
}
}
@@ -137,7 +143,9 @@
"type": "integer"
}
},
"required": ["query"],
"required": [
"query"
],
"additionalProperties": false
}
},
@@ -208,10 +216,17 @@
},
"role": {
"type": "string",
"enum": ["admin", "member"]
"enum": [
"admin",
"member"
]
}
},
"required": ["id", "name", "email"],
"required": [
"id",
"name",
"email"
],
"additionalProperties": false
}
},
File diff suppressed because it is too large Load Diff
@@ -258,8 +258,7 @@ add("test/built-ins/String/prototype/toUpperCase/supplementary_plane.js", "toUpp
),
])
const whitespace =
"\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF"
const whitespace = "\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF"
const lineTerminators = "\u000A\u000D\u2028\u2029"
const trim = (file: string, input: string, expected: string) =>
add(`test/built-ins/String/prototype/trim/${file}`, "trim", [assertion("upstream assertion", input, expected)])
@@ -335,35 +334,18 @@ add("test/built-ins/String/prototype/trim/u180e.js", "trim", [
assertion("leading U+180E", "\u180E_", "\u180E_"),
])
const directionalWhitespace =
"\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF"
const directionalWhitespace = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF"
add("test/built-ins/String/prototype/trimStart/this-value-whitespace.js", "trimStart", [
assertion(
"all whitespace",
directionalWhitespace + "a" + directionalWhitespace + "b" + directionalWhitespace,
"a" + directionalWhitespace + "b" + directionalWhitespace,
),
assertion("all whitespace", directionalWhitespace + "a" + directionalWhitespace + "b" + directionalWhitespace, "a" + directionalWhitespace + "b" + directionalWhitespace),
])
add("test/built-ins/String/prototype/trimStart/this-value-line-terminator.js", "trimStart", [
assertion(
"all line terminators",
lineTerminators + "a" + lineTerminators + "b" + lineTerminators,
"a" + lineTerminators + "b" + lineTerminators,
),
assertion("all line terminators", lineTerminators + "a" + lineTerminators + "b" + lineTerminators, "a" + lineTerminators + "b" + lineTerminators),
])
add("test/built-ins/String/prototype/trimEnd/this-value-whitespace.js", "trimEnd", [
assertion(
"all whitespace",
directionalWhitespace + "a" + directionalWhitespace + "b" + directionalWhitespace,
directionalWhitespace + "a" + directionalWhitespace + "b",
),
assertion("all whitespace", directionalWhitespace + "a" + directionalWhitespace + "b" + directionalWhitespace, directionalWhitespace + "a" + directionalWhitespace + "b"),
])
add("test/built-ins/String/prototype/trimEnd/this-value-line-terminator.js", "trimEnd", [
assertion(
"all line terminators",
lineTerminators + "a" + lineTerminators + "b" + lineTerminators,
lineTerminators + "a" + lineTerminators + "b",
),
assertion("all line terminators", lineTerminators + "a" + lineTerminators + "b" + lineTerminators, lineTerminators + "a" + lineTerminators + "b"),
])
add("test/built-ins/String/prototype/repeat/repeat-string-n-times.js", "repeat", [
assertion("repeat once", "abc", "abc", [1]),
@@ -381,9 +363,7 @@ add("test/built-ins/String/prototype/repeat/count-is-zero-returns-empty-string.j
add("test/built-ins/String/prototype/repeat/count-coerced-to-zero-returns-empty-string.js", "repeat", [
assertion("fraction truncates to zero", "abc", "", [0.9]),
])
add("test/built-ins/String/prototype/padStart/fill-string-empty.js", "padStart", [
assertion("empty fill", "abc", "abc", [5, ""]),
])
add("test/built-ins/String/prototype/padStart/fill-string-empty.js", "padStart", [assertion("empty fill", "abc", "abc", [5, ""])])
add("test/built-ins/String/prototype/padStart/normal-operation.js", "padStart", [
assertion("truncated multi-character fill", "abc", "defdabc", [7, "def"]),
assertion("single-character fill", "abc", "**abc", [5, "*"]),
@@ -401,9 +381,7 @@ add("test/built-ins/String/prototype/padStart/max-length-not-greater-than-string
assertion("equal length", "abc", "abc", [3, "def"]),
assertion("fraction truncates", "abc", "abc", [3.9999, "def"]),
])
add("test/built-ins/String/prototype/padEnd/fill-string-empty.js", "padEnd", [
assertion("empty fill", "abc", "abc", [5, ""]),
])
add("test/built-ins/String/prototype/padEnd/fill-string-empty.js", "padEnd", [assertion("empty fill", "abc", "abc", [5, ""])])
add("test/built-ins/String/prototype/padEnd/normal-operation.js", "padEnd", [
assertion("truncated multi-character fill", "abc", "abcdefd", [7, "def"]),
assertion("single-character fill", "abc", "abc**", [5, "*"]),
@@ -423,29 +401,11 @@ add("test/built-ins/String/prototype/padEnd/max-length-not-greater-than-string.j
])
add("test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T4.js", "charAt", [assertion("omitted position", "lego", "l")])
add("test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T7.js", "charAt", [
assertion("undefined position", "lego", "l", [undefined]),
])
add("test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T8.js", "charAt", [
assertion("undefined position", "42", "4", [undefined]),
])
add(
"test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T1.js",
"charAt",
["A", "B", "C", "A", "B", "C"].map((expected, position) =>
assertion(`position ${position}`, "ABCABC", expected, [position]),
),
)
add(
"test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T2.js",
"charAt",
[-2, -1].map((position) => assertion(`position ${position}`, "ABCABC", "", [position])),
)
add(
"test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T3.js",
"charAt",
[6, 7].map((position) => assertion(`position ${position}`, "ABCABC", "", [position])),
)
add("test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T7.js", "charAt", [assertion("undefined position", "lego", "l", [undefined])])
add("test/built-ins/String/prototype/charAt/S15.5.4.4_A1_T8.js", "charAt", [assertion("undefined position", "42", "4", [undefined])])
add("test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T1.js", "charAt", ["A", "B", "C", "A", "B", "C"].map((expected, position) => assertion(`position ${position}`, "ABCABC", expected, [position])))
add("test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T2.js", "charAt", [-2, -1].map((position) => assertion(`position ${position}`, "ABCABC", "", [position])))
add("test/built-ins/String/prototype/charAt/S15.5.4.4_A4_T3.js", "charAt", [6, 7].map((position) => assertion(`position ${position}`, "ABCABC", "", [position])))
add("test/built-ins/String/prototype/charAt/S9.4_A1.js", "charAt", [assertion("NaN position", "abc", "a", [NaN])])
add("test/built-ins/String/prototype/charAt/S9.4_A2.js", "charAt", [
assertion("positive zero", "abc", "a", [0]),
@@ -460,15 +420,9 @@ add("test/built-ins/String/prototype/charAt/pos-rounding.js", "charAt", [
assertion("1.99999", "abc", "b", [1.99999]),
])
add("test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T4.js", "charCodeAt", [
assertion("omitted position", "smart", 0x73),
])
add("test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T7.js", "charCodeAt", [
assertion("undefined position", "lego", 0x6c, [undefined]),
])
add("test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T8.js", "charCodeAt", [
assertion("undefined position", "42", 0x34, [undefined]),
])
add("test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T4.js", "charCodeAt", [assertion("omitted position", "smart", 0x73)])
add("test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T7.js", "charCodeAt", [assertion("undefined position", "lego", 0x6c, [undefined])])
add("test/built-ins/String/prototype/charCodeAt/S15.5.4.5_A1_T8.js", "charCodeAt", [assertion("undefined position", "42", 0x34, [undefined])])
add("test/built-ins/String/prototype/charCodeAt/pos-rounding.js", "charCodeAt", [
assertion("-0.99999", "abc", 0x61, [-0.99999]),
assertion("-0.00001", "abc", 0x61, [-0.00001]),
@@ -479,91 +433,55 @@ add("test/built-ins/String/prototype/charCodeAt/pos-rounding.js", "charCodeAt",
])
add("test/built-ins/String/prototype/codePointAt/return-single-code-unit.js", "codePointAt", [
assertion("a", "abc", 97, [0]),
assertion("b", "abc", 98, [1]),
assertion("c", "abc", 99, [2]),
assertion("ordinary BMP", "\uAAAA\uBBBB", 0xaaaa, [0]),
assertion("before high-surrogate range", "\uD7FF\uAAAA", 0xd7ff, [0]),
assertion("low surrogate", "\uDC00\uAAAA", 0xdc00, [0]),
assertion("trailing D800", "123\uD800", 0xd800, [3]),
assertion("trailing DAAA", "123\uDAAA", 0xdaaa, [3]),
assertion("trailing DBFF", "123\uDBFF", 0xdbff, [3]),
assertion("a", "abc", 97, [0]), assertion("b", "abc", 98, [1]), assertion("c", "abc", 99, [2]),
assertion("ordinary BMP", "\uAAAA\uBBBB", 0xaaaa, [0]), assertion("before high-surrogate range", "\uD7FF\uAAAA", 0xd7ff, [0]),
assertion("low surrogate", "\uDC00\uAAAA", 0xdc00, [0]), assertion("trailing D800", "123\uD800", 0xd800, [3]),
assertion("trailing DAAA", "123\uDAAA", 0xdaaa, [3]), assertion("trailing DBFF", "123\uDBFF", 0xdbff, [3]),
])
add("test/built-ins/String/prototype/codePointAt/return-first-code-unit.js", "codePointAt", [
assertion("D800 before DBFF", "\uD800\uDBFF", 0xd800, [0]),
assertion("D800 before E000", "\uD800\uE000", 0xd800, [0]),
assertion("DAAA before DBFF", "\uDAAA\uDBFF", 0xdaaa, [0]),
assertion("DAAA before E000", "\uDAAA\uE000", 0xdaaa, [0]),
assertion("DBFF before DBFF", "\uDBFF\uDBFF", 0xdbff, [0]),
assertion("DBFF before E000", "\uDBFF\uE000", 0xdbff, [0]),
assertion("D800 before NUL", "\uD800\u0000", 0xd800, [0]),
assertion("D800 before FFFF", "\uD800\uFFFF", 0xd800, [0]),
assertion("DAAA before NUL", "\uDAAA\u0000", 0xdaaa, [0]),
assertion("DAAA before FFFF", "\uDAAA\uFFFF", 0xdaaa, [0]),
assertion("D800 before DBFF", "\uD800\uDBFF", 0xd800, [0]), assertion("D800 before E000", "\uD800\uE000", 0xd800, [0]),
assertion("DAAA before DBFF", "\uDAAA\uDBFF", 0xdaaa, [0]), assertion("DAAA before E000", "\uDAAA\uE000", 0xdaaa, [0]),
assertion("DBFF before DBFF", "\uDBFF\uDBFF", 0xdbff, [0]), assertion("DBFF before E000", "\uDBFF\uE000", 0xdbff, [0]),
assertion("D800 before NUL", "\uD800\u0000", 0xd800, [0]), assertion("D800 before FFFF", "\uD800\uFFFF", 0xd800, [0]),
assertion("DAAA before NUL", "\uDAAA\u0000", 0xdaaa, [0]), assertion("DAAA before FFFF", "\uDAAA\uFFFF", 0xdaaa, [0]),
assertion("DBFF before FFFF", "\uDBFF\uFFFF", 0xdbff, [0]),
])
add("test/built-ins/String/prototype/codePointAt/return-utf16-decode.js", "codePointAt", [
assertion("U+10000", "\uD800\uDC00", 65536, [0]),
assertion("U+101D0", "\uD800\uDDD0", 66000, [0]),
assertion("U+103FF", "\uD800\uDFFF", 66559, [0]),
assertion("U+BA800", "\uDAAA\uDC00", 763904, [0]),
assertion("U+BA9D0", "\uDAAA\uDDD0", 764368, [0]),
assertion("U+BABFF", "\uDAAA\uDFFF", 764927, [0]),
assertion("U+10FC00", "\uDBFF\uDC00", 1113088, [0]),
assertion("U+10FDD0", "\uDBFF\uDDD0", 1113552, [0]),
assertion("U+10000", "\uD800\uDC00", 65536, [0]), assertion("U+101D0", "\uD800\uDDD0", 66000, [0]),
assertion("U+103FF", "\uD800\uDFFF", 66559, [0]), assertion("U+BA800", "\uDAAA\uDC00", 763904, [0]),
assertion("U+BA9D0", "\uDAAA\uDDD0", 764368, [0]), assertion("U+BABFF", "\uDAAA\uDFFF", 764927, [0]),
assertion("U+10FC00", "\uDBFF\uDC00", 1113088, [0]), assertion("U+10FDD0", "\uDBFF\uDDD0", 1113552, [0]),
assertion("U+10FFFF", "\uDBFF\uDFFF", 1114111, [0]),
])
add("test/built-ins/String/prototype/codePointAt/return-code-unit-coerced-position.js", "codePointAt", [
assertion("NaN", "\uD800\uDC00", 65536, [NaN]),
assertion("undefined", "\uD800\uDC00", 65536, [undefined]),
assertion("NaN", "\uD800\uDC00", 65536, [NaN]), assertion("undefined", "\uD800\uDC00", 65536, [undefined]),
])
add("test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-less-than-zero.js", "codePointAt", [
{ label: "negative one", input: "abc", args: [-1], outcome: "undefined" },
{ label: "negative infinity", input: "abc", args: [-Infinity], outcome: "undefined" },
])
add(
"test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-equal-or-more-than-size.js",
"codePointAt",
[
{ label: "equal to size", input: "abc", args: [3], outcome: "undefined" },
{ label: "greater than size", input: "abc", args: [4], outcome: "undefined" },
{ label: "positive infinity", input: "abc", args: [Infinity], outcome: "undefined" },
],
)
add("test/built-ins/String/prototype/codePointAt/returns-undefined-on-position-equal-or-more-than-size.js", "codePointAt", [
{ label: "equal to size", input: "abc", args: [3], outcome: "undefined" },
{ label: "greater than size", input: "abc", args: [4], outcome: "undefined" },
{ label: "positive infinity", input: "abc", args: [Infinity], outcome: "undefined" },
])
add("test/built-ins/String/prototype/at/returns-code-unit.js", "at", [
assertion("position 0", "12\uD80034", "1", [0]),
assertion("position 1", "12\uD80034", "2", [1]),
assertion("unpaired surrogate", "12\uD80034", "\uD800", [2]),
assertion("position 3", "12\uD80034", "3", [3]),
assertion("position 0", "12\uD80034", "1", [0]), assertion("position 1", "12\uD80034", "2", [1]),
assertion("unpaired surrogate", "12\uD80034", "\uD800", [2]), assertion("position 3", "12\uD80034", "3", [3]),
assertion("position 4", "12\uD80034", "4", [4]),
])
add(
"test/built-ins/String/prototype/at/returns-item.js",
"at",
["1", "2", "3", "4", "5"].map((expected, position) =>
assertion(`position ${position}`, "12345", expected, [position]),
),
)
add("test/built-ins/String/prototype/at/returns-item.js", "at", ["1", "2", "3", "4", "5"].map((expected, position) => assertion(`position ${position}`, "12345", expected, [position])))
add("test/built-ins/String/prototype/at/returns-item-relative-index.js", "at", [
assertion("zero", "12345", "1", [0]),
assertion("negative one", "12345", "5", [-1]),
assertion("negative three", "12345", "3", [-3]),
assertion("negative four", "12345", "2", [-4]),
])
add(
"test/built-ins/String/prototype/at/returns-undefined-for-out-of-range-index.js",
"at",
[-2, 0, 1].map((position) => ({ label: `position ${position}`, input: "", args: [position], outcome: "undefined" })),
)
add("test/built-ins/String/prototype/at/index-non-numeric-argument-tointeger.js", "at", [
assertion("undefined", "01", "0", [undefined]),
assertion("zero", "12345", "1", [0]), assertion("negative one", "12345", "5", [-1]),
assertion("negative three", "12345", "3", [-3]), assertion("negative four", "12345", "2", [-4]),
])
add("test/built-ins/String/prototype/at/returns-undefined-for-out-of-range-index.js", "at", [-2, 0, 1].map((position) => ({ label: `position ${position}`, input: "", args: [position], outcome: "undefined" })))
add("test/built-ins/String/prototype/at/index-non-numeric-argument-tointeger.js", "at", [assertion("undefined", "01", "0", [undefined])])
add("test/built-ins/String/prototype/concat/S15.5.4.6_A1_T4.js", "concat", [assertion("no arguments", "lego", "lego")])
add("test/built-ins/String/prototype/toString/string-primitive.js", "toString", [
assertion("empty string", "", ""),
assertion("non-empty string", "str", "str"),
assertion("empty string", "", ""), assertion("non-empty string", "str", "str"),
])
add("test/built-ins/String/prototype/normalize/return-normalized-string.js", "normalize", [
@@ -574,15 +492,11 @@ add("test/built-ins/String/prototype/normalize/return-normalized-string.js", "no
assertion("NFC long", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFC"]),
assertion("NFD long", "\u00C5\u2ADC\u0958\u2126\u0344", "A\u030A\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFD"]),
assertion("NFKC long", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFKC"]),
assertion("NFKD long", "\u00C5\u2ADC\u0958\u2126\u0344", "A\u030A\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", [
"NFKD",
]),
assertion("NFKD long", "\u00C5\u2ADC\u0958\u2126\u0344", "A\u030A\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", ["NFKD"]),
])
add("test/built-ins/String/prototype/normalize/return-normalized-string-using-default-parameter.js", "normalize", [
assertion("omitted", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301"),
assertion("undefined", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", [
undefined,
]),
assertion("undefined", "\u00C5\u2ADC\u0958\u2126\u0344", "\xC5\u2ADD\u0338\u0915\u093C\u03A9\u0308\u0301", [undefined]),
])
add("test/built-ins/String/prototype/normalize/form-is-not-valid-throws.js", "normalize", [
{ label: "bar", input: "foo", args: ["bar"], outcome: "RangeError" },
@@ -590,141 +504,51 @@ add("test/built-ins/String/prototype/normalize/form-is-not-valid-throws.js", "no
])
add("test/built-ins/String/prototype/localeCompare/15.5.4.9_CE.js", "localeCompare", [
assertion("D70", "o\u0308", 0, ["ö"]),
assertion("reordered diaeresis", "ä\u0323", 0, ["a\u0323\u0308"]),
assertion("reordered marks", "a\u0308\u0323", 0, ["a\u0323\u0308"]),
assertion("precomposed dot below", "ạ\u0308", 0, ["a\u0323\u0308"]),
assertion("breve after diaeresis", "ä\u0306", 0, ["a\u0308\u0306"]),
assertion("diaeresis after breve", "ă\u0308", 0, ["a\u0306\u0308"]),
assertion("Hangul", "\u1111\u1171\u11B6", 0, [""]),
assertion("angstrom compatibility", "", 0, ["Å"]),
assertion("angstrom decomposed", "", 0, ["A\u030A"]),
assertion("reordered horn and dot", "x\u031B\u0323", 0, ["x\u0323\u031B"]),
assertion("Vietnamese precomposed 1", "", 0, ["\u031B"]),
assertion("Vietnamese decomposed", "ự", 0, ["u\u031B\u0323"]),
assertion("Vietnamese precomposed 2", "ự", 0, ["ư\u0323"]),
assertion("Vietnamese reordered", "ự", 0, ["u\u0323\u031B"]),
assertion("cedilla", "Ç", 0, ["C\u0327"]),
assertion("q reordered", "q\u0307\u0323", 0, ["q\u0323\u0307"]),
assertion("Hangul syllable", "가", 0, ["\u1100\u1161"]),
assertion("ohm", "Ω", 0, ["Ω"]),
assertion("angstrom", "Å", 0, ["A\u030A"]),
assertion("circumflex", "ô", 0, ["o\u0302"]),
assertion("s with marks", "ṩ", 0, ["s\u0323\u0307"]),
assertion("d composed plus dot", "ḋ\u0323", 0, ["d\u0323\u0307"]),
assertion("D70", "o\u0308", 0, ["ö"]), assertion("reordered diaeresis", "ä\u0323", 0, ["a\u0323\u0308"]),
assertion("reordered marks", "a\u0308\u0323", 0, ["a\u0323\u0308"]), assertion("precomposed dot below", "ạ\u0308", 0, ["a\u0323\u0308"]),
assertion("breve after diaeresis", "ä\u0306", 0, ["a\u0308\u0306"]), assertion("diaeresis after breve", "ă\u0308", 0, ["a\u0306\u0308"]),
assertion("Hangul", "\u1111\u1171\u11B6", 0, ["퓛"]), assertion("angstrom compatibility", "", 0, ["Å"]),
assertion("angstrom decomposed", "Å", 0, ["A\u030A"]), assertion("reordered horn and dot", "x\u031B\u0323", 0, ["x\u0323\u031B"]),
assertion("Vietnamese precomposed 1", "ự", 0, ["ụ\u031B"]), assertion("Vietnamese decomposed", "ự", 0, ["u\u031B\u0323"]),
assertion("Vietnamese precomposed 2", "ự", 0, ["ư\u0323"]), assertion("Vietnamese reordered", "ự", 0, ["u\u0323\u031B"]),
assertion("cedilla", "Ç", 0, ["C\u0327"]), assertion("q reordered", "q\u0307\u0323", 0, ["q\u0323\u0307"]),
assertion("Hangul syllable", "", 0, ["\u1100\u1161"]), assertion("ohm", "Ω", 0, ["]),
assertion("angstrom", "Å", 0, ["A\u030A"]), assertion("circumflex", ", 0, ["o\u0302"]),
assertion("s with marks", "ṩ", 0, ["s\u0323\u0307"]), assertion("d composed plus dot", "ḋ\u0323", 0, ["d\u0323\u0307"]),
assertion("d two precompositions", "ḋ\u0323", 0, ["ḍ\u0307"]),
])
add(
"test/built-ins/String/fromCharCode/S15.5.3.2_A2.js",
"fromCharCode",
[{ label: "no arguments", expected: "" }],
true,
)
add(
"test/built-ins/String/fromCharCode/S15.5.3.2_A3_T1.js",
"fromCharCode",
[{ label: "ABBA", args: [65, 66, 66, 65], expected: "ABBA" }],
true,
)
add(
"test/built-ins/String/fromCharCode/S9.7_A1.js",
"fromCharCode",
[
{ label: "NaN", args: [NaN], expected: 0 },
{ label: "zero", args: [0], expected: 0 },
{ label: "negative zero", args: [-0], expected: 0 },
{ label: "positive infinity", args: [Infinity], expected: 0 },
{ label: "negative infinity", args: [-Infinity], expected: 0 },
],
true,
)
add(
"test/built-ins/String/fromCharCode/S9.7_A2.1.js",
"fromCharCode",
[
[0, 0],
[1, 1],
[-1, 65535],
[65535, 65535],
[65534, 65534],
[65536, 0],
[4294967295, 65535],
[4294967294, 65534],
[4294967296, 0],
].map(([input, expected]) => ({ label: String(input), args: [input!], expected })),
true,
)
add(
"test/built-ins/String/fromCharCode/S9.7_A2.2.js",
"fromCharCode",
[
[-32767, 32769],
[-32768, 32768],
[-32769, 32767],
[-65535, 1],
[-65536, 0],
[-65537, 65535],
[65535, 65535],
[65536, 0],
[65537, 1],
[131071, 65535],
[131072, 0],
[131073, 1],
].map(([input, expected]) => ({ label: String(input), args: [input!], expected })),
true,
)
add(
"test/built-ins/String/fromCharCode/S9.7_A3.2_T1.js",
"fromCharCode",
[
{ label: "positive fraction", args: [1.2345], expected: 1 },
{ label: "negative fraction", args: [-5.4321], expected: 65531 },
],
true,
)
add("test/built-ins/String/fromCharCode/S15.5.3.2_A2.js", "fromCharCode", [{ label: "no arguments", expected: "" }], true)
add("test/built-ins/String/fromCharCode/S15.5.3.2_A3_T1.js", "fromCharCode", [{ label: "ABBA", args: [65, 66, 66, 65], expected: "ABBA" }], true)
add("test/built-ins/String/fromCharCode/S9.7_A1.js", "fromCharCode", [
{ label: "NaN", args: [NaN], expected: 0 }, { label: "zero", args: [0], expected: 0 }, { label: "negative zero", args: [-0], expected: 0 },
{ label: "positive infinity", args: [Infinity], expected: 0 }, { label: "negative infinity", args: [-Infinity], expected: 0 },
], true)
add("test/built-ins/String/fromCharCode/S9.7_A2.1.js", "fromCharCode", [
[0, 0], [1, 1], [-1, 65535], [65535, 65535], [65534, 65534], [65536, 0], [4294967295, 65535], [4294967294, 65534], [4294967296, 0],
].map(([input, expected]) => ({ label: String(input), args: [input!], expected })), true)
add("test/built-ins/String/fromCharCode/S9.7_A2.2.js", "fromCharCode", [
[-32767, 32769], [-32768, 32768], [-32769, 32767], [-65535, 1], [-65536, 0], [-65537, 65535], [65535, 65535], [65536, 0], [65537, 1], [131071, 65535], [131072, 0], [131073, 1],
].map(([input, expected]) => ({ label: String(input), args: [input!], expected })), true)
add("test/built-ins/String/fromCharCode/S9.7_A3.2_T1.js", "fromCharCode", [
{ label: "positive fraction", args: [1.2345], expected: 1 }, { label: "negative fraction", args: [-5.4321], expected: 65531 },
], true)
add(
"test/built-ins/String/fromCodePoint/arguments-is-empty.js",
"fromCodePoint",
[{ label: "no arguments", expected: "" }],
true,
)
add(
"test/built-ins/String/fromCodePoint/return-string-value.js",
"fromCodePoint",
[
{ label: "NUL", args: [0], expected: "\x00" },
{ label: "asterisk", args: [42], expected: "*" },
{ label: "AZ", args: [65, 90], expected: "AZ" },
{ label: "Cyrillic", args: [0x404], expected: "\u0404" },
{ label: "hex supplementary", args: [0x2f804], expected: "\uD87E\uDC04" },
{ label: "decimal supplementary", args: [194564], expected: "\uD87E\uDC04" },
{ label: "mixed supplementary", args: [0x1d306, 0x61, 0x1d307], expected: "\uD834\uDF06a\uD834\uDF07" },
{ label: "maximum code point", args: [1114111], expected: "\uDBFF\uDFFF" },
],
true,
)
add(
"test/built-ins/String/fromCodePoint/argument-is-not-integer.js",
"fromCodePoint",
[
{ label: "fraction", args: [3.14], outcome: "RangeError" },
{ label: "fraction after valid", args: [42, 3.14], outcome: "RangeError" },
],
true,
)
add(
"test/built-ins/String/fromCodePoint/number-is-out-of-range.js",
"fromCodePoint",
[
{ label: "negative one", args: [-1], outcome: "RangeError" },
{ label: "negative after valid", args: [1, -1], outcome: "RangeError" },
{ label: "above maximum", args: [1114112], outcome: "RangeError" },
{ label: "infinity", args: [Infinity], outcome: "RangeError" },
],
true,
)
add("test/built-ins/String/fromCodePoint/arguments-is-empty.js", "fromCodePoint", [{ label: "no arguments", expected: "" }], true)
add("test/built-ins/String/fromCodePoint/return-string-value.js", "fromCodePoint", [
{ label: "NUL", args: [0], expected: "\x00" }, { label: "asterisk", args: [42], expected: "*" },
{ label: "AZ", args: [65, 90], expected: "AZ" }, { label: "Cyrillic", args: [0x404], expected: "\u0404" },
{ label: "hex supplementary", args: [0x2f804], expected: "\uD87E\uDC04" }, { label: "decimal supplementary", args: [194564], expected: "\uD87E\uDC04" },
{ label: "mixed supplementary", args: [0x1d306, 0x61, 0x1d307], expected: "\uD834\uDF06a\uD834\uDF07" },
{ label: "maximum code point", args: [1114111], expected: "\uDBFF\uDFFF" },
], true)
add("test/built-ins/String/fromCodePoint/argument-is-not-integer.js", "fromCodePoint", [
{ label: "fraction", args: [3.14], outcome: "RangeError" }, { label: "fraction after valid", args: [42, 3.14], outcome: "RangeError" },
], true)
add("test/built-ins/String/fromCodePoint/number-is-out-of-range.js", "fromCodePoint", [
{ label: "negative one", args: [-1], outcome: "RangeError" }, { label: "negative after valid", args: [1, -1], outcome: "RangeError" },
{ label: "above maximum", args: [1114112], outcome: "RangeError" }, { label: "infinity", args: [Infinity], outcome: "RangeError" },
], true)
describe("Test262-adapted core String behavior", () => {
for (const vector of vectors) {
@@ -734,18 +558,16 @@ describe("Test262-adapted core String behavior", () => {
const expression = vector.static
? `String.${vector.method}(${args})`
: `${JSON.stringify(item.input)}.${vector.method}(${args})`
const observed =
vector.static && vector.method === "fromCharCode" && typeof item.expected === "number"
? `${expression}.charCodeAt(0)`
: expression
const checked =
item.outcome === "undefined"
? `${observed} === undefined`
: item.outcome === "length"
? `${observed}.length`
: item.outcome === "RangeError"
? `(() => { try { ${observed}; return false } catch (error) { return error instanceof RangeError } })()`
: observed
const observed = vector.static && vector.method === "fromCharCode" && typeof item.expected === "number"
? `${expression}.charCodeAt(0)`
: expression
const checked = item.outcome === "undefined"
? `${observed} === undefined`
: item.outcome === "length"
? `${observed}.length`
: item.outcome === "RangeError"
? `(() => { try { ${observed}; return false } catch (error) { return error instanceof RangeError } })()`
: observed
return `{ label: ${JSON.stringify(item.label)}, value: ${checked} }`
})
const expected = vector.assertions.map((item) => ({
@@ -121,35 +121,9 @@ run("Test262-adapted regexp split behavior", [
]
`,
expected: [
["x"],
["x"],
["", ""],
["", ""],
["", ""],
["x"],
["", ""],
["", ""],
["", ""],
["x"],
["", ""],
["x"],
["x"],
["x"],
["x"],
["", ""],
["x"],
["x"],
["x"],
["x"],
["x"],
["", ""],
["x"],
["x"],
["x"],
["", ""],
["x"],
["", ""],
["x"],
["x"], ["x"], ["", ""], ["", ""], ["", ""], ["x"], ["", ""], ["", ""], ["", ""],
["x"], ["", ""], ["x"], ["x"], ["x"], ["x"], ["", ""], ["x"], ["x"], ["x"],
["x"], ["x"], ["", ""], ["x"], ["x"], ["x"], ["", ""], ["x"], ["", ""], ["x"],
],
},
{
@@ -279,71 +253,19 @@ run("Test262-adapted replace behavior", [
return replacements.flatMap((replacement) => patterns.map((pattern) => str.replace(pattern, replacement)))
`,
expected: [
"foo-|$0|-bar",
"foo-|$0|-bar",
"foo-|$0|-bar",
"foo-|$0|-bar",
"foo-|$0|-bar",
"foo-|$00|-bar",
"foo-|$00|-bar",
"foo-|$00|-bar",
"foo-|$00|-bar",
"foo-|$00|-bar",
"foo-|$000|-bar",
"foo-|$000|-bar",
"foo-|$000|-bar",
"foo-|$000|-bar",
"foo-|$000|-bar",
"foo-|$1|-bar",
"foo-|$1|-bar",
"foo-|x|-bar",
"foo-|x|-bar",
"foo-|x|-bar",
"foo-|$01|-bar",
"foo-|$01|-bar",
"foo-|x|-bar",
"foo-|x|-bar",
"foo-|x|-bar",
"foo-|$010|-bar",
"foo-|$010|-bar",
"foo-|x0|-bar",
"foo-|x0|-bar",
"foo-|x0|-bar",
"foo-|$2|-bar",
"foo-|$2|-bar",
"foo-|$2|-bar",
"foo-||-bar",
"foo-|x|-bar",
"foo-|$02|-bar",
"foo-|$02|-bar",
"foo-|$02|-bar",
"foo-||-bar",
"foo-|x|-bar",
"foo-|$020|-bar",
"foo-|$020|-bar",
"foo-|$020|-bar",
"foo-|0|-bar",
"foo-|x0|-bar",
"foo-|$10|-bar",
"foo-|$10|-bar",
"foo-|x0|-bar",
"foo-|x0|-bar",
"foo-|x|-bar",
"foo-|$100|-bar",
"foo-|$100|-bar",
"foo-|x00|-bar",
"foo-|x00|-bar",
"foo-|x0|-bar",
"foo-|$20|-bar",
"foo-|$20|-bar",
"foo-|$20|-bar",
"foo-|0|-bar",
"foo-|x0|-bar",
"foo-|$200|-bar",
"foo-|$200|-bar",
"foo-|$200|-bar",
"foo-|00|-bar",
"foo-|x00|-bar",
"foo-|$0|-bar", "foo-|$0|-bar", "foo-|$0|-bar", "foo-|$0|-bar", "foo-|$0|-bar",
"foo-|$00|-bar", "foo-|$00|-bar", "foo-|$00|-bar", "foo-|$00|-bar", "foo-|$00|-bar",
"foo-|$000|-bar", "foo-|$000|-bar", "foo-|$000|-bar", "foo-|$000|-bar", "foo-|$000|-bar",
"foo-|$1|-bar", "foo-|$1|-bar", "foo-|x|-bar", "foo-|x|-bar", "foo-|x|-bar",
"foo-|$01|-bar", "foo-|$01|-bar", "foo-|x|-bar", "foo-|x|-bar", "foo-|x|-bar",
"foo-|$010|-bar", "foo-|$010|-bar", "foo-|x0|-bar", "foo-|x0|-bar", "foo-|x0|-bar",
"foo-|$2|-bar", "foo-|$2|-bar", "foo-|$2|-bar", "foo-||-bar", "foo-|x|-bar",
"foo-|$02|-bar", "foo-|$02|-bar", "foo-|$02|-bar", "foo-||-bar", "foo-|x|-bar",
"foo-|$020|-bar", "foo-|$020|-bar", "foo-|$020|-bar", "foo-|0|-bar", "foo-|x0|-bar",
"foo-|$10|-bar", "foo-|$10|-bar", "foo-|x0|-bar", "foo-|x0|-bar", "foo-|x|-bar",
"foo-|$100|-bar", "foo-|$100|-bar", "foo-|x00|-bar", "foo-|x00|-bar", "foo-|x0|-bar",
"foo-|$20|-bar", "foo-|$20|-bar", "foo-|$20|-bar", "foo-|0|-bar", "foo-|x0|-bar",
"foo-|$200|-bar", "foo-|$200|-bar", "foo-|$200|-bar", "foo-|00|-bar", "foo-|x00|-bar",
],
},
{
@@ -455,30 +377,12 @@ run("Test262-adapted replaceAll behavior", [
]
`,
expected: [
"azc azc azc",
"abc abc abc",
"abc abc abc",
"o ppercase!",
"o Uppercase?",
" UPPERCASE!",
"ca-bbcca-bbc",
"abb$3cabb$3cabb$3cabb$3c",
"azc azc azc", "abc abc abc", "abc abc abc", "o ppercase!", "o Uppercase?", " UPPERCASE!",
"ca-bbcca-bbc", "abb$3cabb$3cabb$3cabb$3c",
"(aabaca)-(aaba)-(aabacadaea)f(agahai)-(agah)-(agahaiajak)(alaman)-(alam)-(alamano a )azaya",
"acbacaa",
"aacabca",
"a(b)c(b)a",
"a($<named)c($<named)a",
"a()c()a",
"($)bc($)bc",
"($)bc($)bc",
"($$)bc($$)bc",
"($$)bc($$)bc",
"($&)bc($&)bc",
"($1)bc($1)bc",
"($`)bc($`)bc",
"($')bc($')bc",
"($<z>)bc($<z>)bc",
"(abca)bc(abca)bc",
"acbacaa", "aacabca", "a(b)c(b)a", "a($<named)c($<named)a", "a()c()a", "($)bc($)bc",
"($)bc($)bc", "($$)bc($$)bc", "($$)bc($$)bc", "($&)bc($&)bc", "($1)bc($1)bc",
"($`)bc($`)bc", "($')bc($')bc", "($<z>)bc($<z>)bc", "(abca)bc(abca)bc",
],
},
{
@@ -568,17 +472,7 @@ run("Test262-adapted replaceAll behavior", [
const str = "ABC AAA ABC AAA"
return ["$1", "$2", "$3", "$4", "$5", "$6", "$7", "$8", "$9"].map((replacement) => str.replaceAll("ABC", replacement))
`,
expected: [
"$1 AAA $1 AAA",
"$2 AAA $2 AAA",
"$3 AAA $3 AAA",
"$4 AAA $4 AAA",
"$5 AAA $5 AAA",
"$6 AAA $6 AAA",
"$7 AAA $7 AAA",
"$8 AAA $8 AAA",
"$9 AAA $9 AAA",
],
expected: ["$1 AAA $1 AAA", "$2 AAA $2 AAA", "$3 AAA $3 AAA", "$4 AAA $4 AAA", "$5 AAA $5 AAA", "$6 AAA $6 AAA", "$7 AAA $7 AAA", "$8 AAA $8 AAA", "$9 AAA $9 AAA"],
},
{
path: "test/built-ins/String/prototype/replaceAll/getSubstitution-0x0024NN.js",
@@ -248,12 +248,8 @@ const cases = [
code: `const result = "one two three four five".split(" "); return [result.length, ...result]`,
expected: [5, "one", "two", "three", "four", "five"],
labels: [
"The value of __split.length is 5",
'The value of __split[0] is "one"',
'The value of __split[1] is "two"',
'The value of __split[2] is "three"',
'The value of __split[3] is "four"',
'The value of __split[4] is "five"',
"The value of __split.length is 5", 'The value of __split[0] is "one"', 'The value of __split[1] is "two"',
'The value of __split[2] is "three"', 'The value of __split[3] is "four"', 'The value of __split[4] is "five"',
],
},
{
@@ -261,10 +257,8 @@ const cases = [
code: `const result = "one two three".split(""); return [result[0], result[1], result[11], result[12]]`,
expected: ["o", "n", "e", "e"],
labels: [
'The value of __split[0] is "o"',
'The value of __split[1] is "n"',
'The value of __split[11] is "e"',
'The value of __split[12] is "e"',
'The value of __split[0] is "o"', 'The value of __split[1] is "n"',
'The value of __split[11] is "e"', 'The value of __split[12] is "e"',
],
},
{
@@ -320,11 +314,7 @@ const cases = [
path: "test/built-ins/String/prototype/split/separator-undef.js",
code: `const result = "undefined is not a function".split(); return [Array.isArray(result), result.length, result[0]]`,
expected: [true, 1, "undefined is not a function"],
labels: [
"implicit separator, result is array",
"implicit separator, result.length",
"implicit separator, [0] is the same string",
],
labels: ["implicit separator, result is array", "implicit separator, result.length", "implicit separator, [0] is the same string"],
},
{
path: "test/built-ins/String/prototype/slice/S15.5.4.13_A1_T6.js",
@@ -466,33 +456,23 @@ const cases = [
},
{
path: "test/built-ins/String/prototype/includes/String.prototype.includes_FailMissingLetter.js",
code: `return ["word".includes("a", 0)]`,
expected: [false],
labels: ['"word".includes("a", 0)'],
code: `return ["word".includes("a", 0)]`, expected: [false], labels: ['"word".includes("a", 0)'],
},
{
path: "test/built-ins/String/prototype/includes/String.prototype.includes_SuccessNoLocation.js",
code: `return ["word".includes("w")]`,
expected: [true],
labels: ['"word".includes("w")'],
code: `return ["word".includes("w")]`, expected: [true], labels: ['"word".includes("w")'],
},
{
path: "test/built-ins/String/prototype/includes/String.prototype.includes_FailBadLocation.js",
code: `return ["word".includes("w", 5)]`,
expected: [false],
labels: ['"word".includes("w", 5)'],
code: `return ["word".includes("w", 5)]`, expected: [false], labels: ['"word".includes("w", 5)'],
},
{
path: "test/built-ins/String/prototype/includes/String.prototype.includes_FailLocation.js",
code: `return ["word".includes("o", 3)]`,
expected: [false],
labels: ['"word".includes("o", 3)'],
code: `return ["word".includes("o", 3)]`, expected: [false], labels: ['"word".includes("o", 3)'],
},
{
path: "test/built-ins/String/prototype/includes/String.prototype.includes_Success.js",
code: `return ["word".includes("w", 0)]`,
expected: [true],
labels: ['"word".includes("w", 0)'],
code: `return ["word".includes("w", 0)]`, expected: [true], labels: ['"word".includes("w", 0)'],
},
{
path: "test/built-ins/String/prototype/includes/searchstring-found-with-position.js",
@@ -523,8 +503,7 @@ const cases = [
{
path: "test/built-ins/String/prototype/includes/searchstring-not-found-without-position.js",
code: `const text = "The future is cool!"; return [text.includes("Flash"), text.includes("FUTURE")]`,
expected: [false, false],
labels: ["Flash if not included", "includes is case sensitive"],
expected: [false, false], labels: ["Flash if not included", "includes is case sensitive"],
},
{
path: "test/built-ins/String/prototype/includes/return-false-with-out-of-bounds-position.js",
@@ -533,21 +512,15 @@ const cases = [
]`,
expected: [false, false, false, false],
labels: [
'str.includes("!", str.length + 1) returns false',
'str.includes("!", 100) returns false',
'str.includes("!", Infinity) returns false',
'str.includes("!", str.length) returns false',
'str.includes("!", str.length + 1) returns false', 'str.includes("!", 100) returns false',
'str.includes("!", Infinity) returns false', 'str.includes("!", str.length) returns false',
],
},
{
path: "test/built-ins/String/prototype/includes/return-true-if-searchstring-is-empty.js",
code: `const text = "The future is cool!"; return [text.includes("", text.length), text.includes(""), text.includes("", Infinity)]`,
expected: [true, true, true],
labels: [
'str.includes("", str.length) returns true',
'str.includes("") returns true',
'str.includes("", Infinity) returns true',
],
labels: ['str.includes("", str.length) returns true', 'str.includes("") returns true', 'str.includes("", Infinity) returns true'],
},
{
path: "test/built-ins/String/prototype/includes/coerced-values-of-position.js",
@@ -563,8 +536,7 @@ const cases = [
code: `const text = "The future is cool!"; return [text.startsWith("The future", 0), text.startsWith("future", 4), text.startsWith(" is cool!", 10)]`,
expected: [true, true, true],
labels: [
'str.startsWith("The future", 0) === true',
'str.startsWith("future", 4) === true',
'str.startsWith("The future", 0) === true', 'str.startsWith("future", 4) === true',
'str.startsWith(" is cool!", 10) === true',
],
},
@@ -572,11 +544,7 @@ const cases = [
path: "test/built-ins/String/prototype/startsWith/searchstring-found-without-position.js",
code: `const text = "The future is cool!"; return [text.startsWith("The "), text.startsWith("The future"), text.startsWith(text)]`,
expected: [true, true, true],
labels: [
'str.startsWith("The ") === true',
'str.startsWith("The future") === true',
"str.startsWith(str) === true",
],
labels: ['str.startsWith("The ") === true', 'str.startsWith("The future") === true', "str.startsWith(str) === true"],
},
{
path: "test/built-ins/String/prototype/startsWith/searchstring-not-found-with-position.js",
@@ -588,11 +556,7 @@ const cases = [
path: "test/built-ins/String/prototype/startsWith/searchstring-not-found-without-position.js",
code: `const text = "The future is cool!"; return [text.startsWith("Flash"), text.startsWith("THE FUTURE"), text.startsWith("future is cool!")]`,
expected: [false, false, false],
labels: [
'str.startsWith("Flash") === false',
"startsWith is case sensitive",
'str.startsWith("future is cool!") === false',
],
labels: ['str.startsWith("Flash") === false', "startsWith is case sensitive", 'str.startsWith("future is cool!") === false'],
},
{
path: "test/built-ins/String/prototype/startsWith/out-of-bounds-position.js",
@@ -602,10 +566,8 @@ const cases = [
]`,
expected: [false, false, false, true, true],
labels: [
'str.startsWith("!", str.length) returns false',
'str.startsWith("!", 100) returns false',
'str.startsWith("!", Infinity) returns false',
"position argument < 0 will search from the start of the string (-1)",
'str.startsWith("!", str.length) returns false', 'str.startsWith("!", 100) returns false',
'str.startsWith("!", Infinity) returns false', "position argument < 0 will search from the start of the string (-1)",
"position argument < 0 will search from the start of the string (-Infinity)",
],
},
@@ -613,11 +575,7 @@ const cases = [
path: "test/built-ins/String/prototype/startsWith/return-true-if-searchstring-is-empty.js",
code: `const text = "The future is cool!"; return [text.startsWith(""), text.startsWith("", text.length), text.startsWith("", Infinity)]`,
expected: [true, true, true],
labels: [
'str.startsWith("") returns true',
'str.startsWith("", str.length) returns true',
'str.startsWith("", Infinity) returns true',
],
labels: ['str.startsWith("") returns true', 'str.startsWith("", str.length) returns true', 'str.startsWith("", Infinity) returns true'],
},
{
path: "test/built-ins/String/prototype/startsWith/coerced-values-of-position.js",
@@ -630,47 +588,34 @@ const cases = [
},
{
path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success.js",
code: `return ["word".endsWith("d")]`,
expected: [true],
labels: ['"word".endsWith("d")'],
code: `return ["word".endsWith("d")]`, expected: [true], labels: ['"word".endsWith("d")'],
},
{
path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_2.js",
code: `return ["word".endsWith("d", 4)]`,
expected: [true],
labels: ['"word".endsWith("d", 4)'],
code: `return ["word".endsWith("d", 4)]`, expected: [true], labels: ['"word".endsWith("d", 4)'],
},
{
path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_3.js",
code: `return ["word".endsWith("d", 25)]`,
expected: [true],
labels: ['"word".endsWith("d", 25)'],
code: `return ["word".endsWith("d", 25)]`, expected: [true], labels: ['"word".endsWith("d", 25)'],
},
{
path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Success_4.js",
code: `return ["word".endsWith("r", 3)]`,
expected: [true],
labels: ['"word".endsWith("r", 3)'],
code: `return ["word".endsWith("r", 3)]`, expected: [true], labels: ['"word".endsWith("r", 3)'],
},
{
path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Fail.js",
code: `return ["word".endsWith("r")]`,
expected: [false],
labels: ['"word".endsWith("r")'],
code: `return ["word".endsWith("r")]`, expected: [false], labels: ['"word".endsWith("r")'],
},
{
path: "test/built-ins/String/prototype/endsWith/String.prototype.endsWith_Fail_2.js",
code: `return ["word".endsWith("d", 3)]`,
expected: [false],
labels: ['"word".endsWith("d", 3)'],
code: `return ["word".endsWith("d", 3)]`, expected: [false], labels: ['"word".endsWith("d", 3)'],
},
{
path: "test/built-ins/String/prototype/endsWith/searchstring-found-with-position.js",
code: `const text = "The future is cool!"; return [text.endsWith("The future", 10), text.endsWith("future", 10), text.endsWith(" is cool!", text.length)]`,
expected: [true, true, true],
labels: [
'str.endsWith("The future", 10) === true',
'str.endsWith("future", 10) === true',
'str.endsWith("The future", 10) === true', 'str.endsWith("future", 10) === true',
'str.endsWith(" is cool!", str.length) === true',
],
},
@@ -690,11 +635,7 @@ const cases = [
path: "test/built-ins/String/prototype/endsWith/searchstring-not-found-without-position.js",
code: `const text = "The future is cool!"; return [text.endsWith("is Flash!"), text.endsWith("IS COOL!"), text.endsWith("The future")]`,
expected: [false, false, false],
labels: [
'str.endsWith("is Flash!") === false',
"endsWith is case sensitive",
'str.endsWith("The future") === false',
],
labels: ['str.endsWith("is Flash!") === false', "endsWith is case sensitive", 'str.endsWith("The future") === false'],
},
{
path: "test/built-ins/String/prototype/endsWith/return-false-if-search-start-is-less-than-zero.js",
@@ -710,11 +651,8 @@ const cases = [
]`,
expected: [true, true, true, true, true],
labels: [
'str.endsWith("") returns true',
'str.endsWith("", str.length) returns true',
'str.endsWith("", Infinity) returns true',
'str.endsWith("", -1) returns true',
'str.endsWith("", -Infinity) returns true',
'str.endsWith("") returns true', 'str.endsWith("", str.length) returns true', 'str.endsWith("", Infinity) returns true',
'str.endsWith("", -1) returns true', 'str.endsWith("", -Infinity) returns true',
],
},
{
@@ -727,39 +665,27 @@ const cases = [
},
{
path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T1.js",
code: `return ["abcd".indexOf("abcdab")]`,
expected: [-1],
labels: ['#1: "abcd".indexOf("abcdab")===-1'],
code: `return ["abcd".indexOf("abcdab")]`, expected: [-1], labels: ['#1: "abcd".indexOf("abcdab")===-1'],
},
{
path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T2.js",
code: `return ["abcd".indexOf("abcdab", 0)]`,
expected: [-1],
labels: ['#1: "abcd".indexOf("abcdab",0)===-1'],
code: `return ["abcd".indexOf("abcdab", 0)]`, expected: [-1], labels: ['#1: "abcd".indexOf("abcdab",0)===-1'],
},
{
path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T3.js",
code: `return ["abcd".indexOf("abcdab", 99)]`,
expected: [-1],
labels: ['#1: "abcd".indexOf("abcdab",99)===-1'],
code: `return ["abcd".indexOf("abcdab", 99)]`, expected: [-1], labels: ['#1: "abcd".indexOf("abcdab",99)===-1'],
},
{
path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A2_T4.js",
code: `return ["abcd".indexOf("abcdab", NaN)]`,
expected: [-1],
labels: ['#1: "abcd".indexOf("abcdab",NaN)===-1'],
code: `return ["abcd".indexOf("abcdab", NaN)]`, expected: [-1], labels: ['#1: "abcd".indexOf("abcdab",NaN)===-1'],
},
{
path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A3_T1.js",
code: `return ["$$abcdabcd".indexOf("ab", NaN)]`,
expected: [2],
labels: ['#1: "$$abcdabcd".indexOf("ab",NaN)===2'],
code: `return ["$$abcdabcd".indexOf("ab", NaN)]`, expected: [2], labels: ['#1: "$$abcdabcd".indexOf("ab",NaN)===2'],
},
{
path: "test/built-ins/String/prototype/indexOf/S15.5.4.7_A3_T3.js",
code: `return ["$$abcdabcd".indexOf("ab", -Infinity)]`,
expected: [2],
labels: ['#1: "$$abcdabcd".indexOf("ab", function(){return -Infinity;}())===2'],
code: `return ["$$abcdabcd".indexOf("ab", -Infinity)]`, expected: [2], labels: ['#1: "$$abcdabcd".indexOf("ab", function(){return -Infinity;}())===2'],
},
{
path: "test/built-ins/String/prototype/indexOf/position-tointeger.js",
@@ -771,31 +697,21 @@ const cases = [
]`,
expected: [0, 1, 0, 0, 1, 0, -1, 0, 2, 2],
labels: [
"position 0",
"position 1",
"ToInteger: truncate towards 0 (-0.9)",
"ToInteger: truncate towards 0 (0.9)",
"ToInteger: truncate towards 0 (1.9)",
"ToInteger: NaN => 0",
"position Infinity",
"ToInteger: undefined => NaN => 0",
"position 2",
"ToInteger: truncate towards 0 (2.9)",
"position 0", "position 1", "ToInteger: truncate towards 0 (-0.9)", "ToInteger: truncate towards 0 (0.9)",
"ToInteger: truncate towards 0 (1.9)", "ToInteger: NaN => 0", "position Infinity",
"ToInteger: undefined => NaN => 0", "position 2", "ToInteger: truncate towards 0 (2.9)",
],
},
{
path: "test/built-ins/String/prototype/indexOf/searchstring-tostring.js",
code: `return ["foo".indexOf(""), "__foo__".indexOf("foo")]`,
expected: [0, 2],
labels: ['"foo".indexOf("")', '"__foo__".indexOf("foo")'],
expected: [0, 2], labels: ['"foo".indexOf("")', '"__foo__".indexOf("foo")'],
},
{
path: "test/built-ins/String/prototype/lastIndexOf/not-a-substring.js",
code: `return ["abc".lastIndexOf("d")]`,
expected: [-1],
labels: [
"String.prototype.lastIndexOf returns -1 when searchString is shorter than this and searchString is not a substring of this.",
],
labels: ["String.prototype.lastIndexOf returns -1 when searchString is shorter than this and searchString is not a substring of this."],
},
] as const
+213 -93
View File
@@ -1,9 +1,15 @@
{
"version": "7",
"dialect": "sqlite",
"id": "15060ec5-05f7-4b86-b2a5-9108609432b3",
"prevIds": ["1551a157-8959-4ba9-a52b-4ea3b7b28cae"],
"id": "2d214a71-3b0a-48c1-a667-741952c4e188",
"prevIds": [
"f14a9b18-8207-487e-a3d3-227e629ba9ad"
],
"ddl": [
{
"name": "workspace",
"entityType": "tables"
},
{
"name": "account_state",
"entityType": "tables"
@@ -69,8 +75,84 @@
"entityType": "tables"
},
{
"name": "workspace",
"entityType": "tables"
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "type",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": "''",
"generated": null,
"name": "name",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "branch",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "directory",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "extra",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "project_id",
"entityType": "columns",
"table": "workspace"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_used",
"entityType": "columns",
"table": "workspace"
},
{
"type": "integer",
@@ -1303,59 +1385,28 @@
"table": "session_v2"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"columns": [
"project_id"
],
"tableTo": "project",
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_workspace_project_id_project_id_fk",
"entityType": "fks",
"table": "workspace"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "provider",
"entityType": "columns",
"table": "workspace"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "binding",
"entityType": "columns",
"table": "workspace"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "created_at",
"entityType": "columns",
"table": "workspace"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "last_used_at",
"entityType": "columns",
"table": "workspace"
},
{
"columns": ["active_account_id"],
"columns": [
"active_account_id"
],
"tableTo": "account",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "SET NULL",
"nameExplicit": false,
@@ -1364,9 +1415,13 @@
"table": "account_state"
},
{
"columns": ["aggregate_id"],
"columns": [
"aggregate_id"
],
"tableTo": "event_sequence",
"columnsTo": ["aggregate_id"],
"columnsTo": [
"aggregate_id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1375,9 +1430,13 @@
"table": "event"
},
{
"columns": ["project_id"],
"columns": [
"project_id"
],
"tableTo": "project",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1386,9 +1445,13 @@
"table": "permission"
},
{
"columns": ["project_id"],
"columns": [
"project_id"
],
"tableTo": "project",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1397,9 +1460,13 @@
"table": "project_directory"
},
{
"columns": ["session_id"],
"columns": [
"session_id"
],
"tableTo": "session_v2",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1408,9 +1475,13 @@
"table": "instruction_entry"
},
{
"columns": ["session_id"],
"columns": [
"session_id"
],
"tableTo": "session_v2",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1419,9 +1490,13 @@
"table": "instruction_state"
},
{
"columns": ["session_id"],
"columns": [
"session_id"
],
"tableTo": "session_v2",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1430,9 +1505,13 @@
"table": "session_message"
},
{
"columns": ["session_id"],
"columns": [
"session_id"
],
"tableTo": "session_v2",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1441,9 +1520,13 @@
"table": "session_pending"
},
{
"columns": ["project_id"],
"columns": [
"project_id"
],
"tableTo": "project",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1452,124 +1535,161 @@
"table": "session_v2"
},
{
"columns": ["email", "url"],
"columns": [
"email",
"url"
],
"nameExplicit": false,
"name": "control_account_pk",
"entityType": "pks",
"table": "control_account"
},
{
"columns": ["project_id", "directory"],
"columns": [
"project_id",
"directory"
],
"nameExplicit": false,
"name": "project_directory_pk",
"entityType": "pks",
"table": "project_directory"
},
{
"columns": ["session_id", "key"],
"columns": [
"session_id",
"key"
],
"nameExplicit": false,
"name": "instruction_entry_pk",
"entityType": "pks",
"table": "instruction_entry"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "workspace_pk",
"table": "workspace",
"entityType": "pks"
},
{
"columns": [
"id"
],
"nameExplicit": false,
"name": "account_state_pk",
"table": "account_state",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "account_pk",
"table": "account",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "credential_pk",
"table": "credential",
"entityType": "pks"
},
{
"columns": ["aggregate_id"],
"columns": [
"aggregate_id"
],
"nameExplicit": false,
"name": "event_sequence_pk",
"table": "event_sequence",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "event_pk",
"table": "event",
"entityType": "pks"
},
{
"columns": ["key"],
"columns": [
"key"
],
"nameExplicit": false,
"name": "kv_pk",
"table": "kv",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "permission_pk",
"table": "permission",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "project_pk",
"table": "project",
"entityType": "pks"
},
{
"columns": ["hash"],
"columns": [
"hash"
],
"nameExplicit": false,
"name": "instruction_blob_pk",
"table": "instruction_blob",
"entityType": "pks"
},
{
"columns": ["session_id"],
"columns": [
"session_id"
],
"nameExplicit": false,
"name": "instruction_state_pk",
"table": "instruction_state",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "session_message_pk",
"table": "session_message",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "session_pending_pk",
"table": "session_pending",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "session_v2_pk",
"table": "session_v2",
"entityType": "pks"
},
{
"columns": ["id"],
"nameExplicit": false,
"name": "workspace_pk",
"table": "workspace",
"entityType": "pks"
},
{
"columns": [
{
@@ -1820,4 +1940,4 @@
}
],
"renames": []
}
}
+3 -1
View File
@@ -20,7 +20,9 @@ if (!Number.isInteger(iterations) || iterations < 1) {
}
const ref = Location.Ref.make({ directory: AbsolutePath.make(path.resolve(directory)) })
const layer = AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, SdkPlugins.node, LocationServiceMap.node]))
const layer = AppNodeBuilder.build(
LayerNode.group([Database.node, Bus.node, SdkPlugins.node, LocationServiceMap.node]),
)
const measure = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
Effect.gen(function* () {

Some files were not shown because too many files have changed in this diff Show More