mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-08 10:09:52 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a48e68fc30 |
@@ -4,7 +4,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- v2
|
||||
|
||||
jobs:
|
||||
generate:
|
||||
|
||||
@@ -251,3 +251,4 @@ opencode-drive stop --name demo
|
||||
```bash
|
||||
opencode-drive dir --name demo
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -709,7 +709,9 @@ 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")
|
||||
@@ -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)
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -40,39 +40,40 @@ export const MediaPart = Schema.Struct({
|
||||
}).annotate({ identifier: "LLM.Content.Media" })
|
||||
export type MediaPart = Schema.Schema.Type<typeof MediaPart>
|
||||
|
||||
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>
|
||||
|
||||
const isToolResultValue = (value: unknown): value is ToolResultValue =>
|
||||
isRecord(value) &&
|
||||
(value.type === "text" || value.type === "json" || value.type === "error" || value.type === "content") &&
|
||||
"value" in value
|
||||
|
||||
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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
+7
-1
@@ -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"
|
||||
},
|
||||
|
||||
+6
-1
@@ -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"
|
||||
},
|
||||
|
||||
+5
-1
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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" }],
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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 [
|
||||
{
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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["forms"]>[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"],
|
||||
formAnswers: 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.answers"; answers: FormAnswer }
|
||||
| { 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.formAnswers = 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.formAnswers = 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.answers") {
|
||||
draft.formAnswers = action.answers
|
||||
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, answers?: 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.forms?.length && !answers) {
|
||||
dispatch({ type: "auth.form" })
|
||||
return
|
||||
}
|
||||
if (method.type === "key") {
|
||||
dispatch({ type: "auth.answers", answers: answers ?? {} })
|
||||
return
|
||||
}
|
||||
if (method.type === "oauth") {
|
||||
if (method.forms?.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,
|
||||
answers: answers ?? {},
|
||||
inputs: inputs ?? {},
|
||||
location: location(),
|
||||
})
|
||||
.then((x) => {
|
||||
@@ -574,42 +564,41 @@ function ProviderConnection(props: {
|
||||
}
|
||||
}
|
||||
|
||||
function AuthFormsView() {
|
||||
function AuthPromptsView() {
|
||||
const [formStore, setFormStore] = createStore({
|
||||
value: {} as Record<string, string>,
|
||||
index: 0,
|
||||
})
|
||||
|
||||
const forms = createMemo<StringForm[]>(() => {
|
||||
const prompts = createMemo(() => {
|
||||
const value = method()
|
||||
return (value?.forms ?? []).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 = forms()
|
||||
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 = forms().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,
|
||||
answers: store.formAnswers ?? {},
|
||||
})
|
||||
await complete()
|
||||
}
|
||||
@@ -1155,8 +1143,8 @@ function ProviderConnection(props: {
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={store.state === "form"}>
|
||||
<AuthFormsView />
|
||||
<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)
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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" }))
|
||||
},
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,13 +671,13 @@ export function createServerSyncContextInner(serverSDK: ServerSDK) {
|
||||
integrationID: server.integrationID,
|
||||
location: { directory: key },
|
||||
})
|
||||
const method = integration.data?.methods.find((item) => item.type === "oauth" && !item.forms?.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,
|
||||
answers: {},
|
||||
inputs: {},
|
||||
location: { directory: key },
|
||||
})
|
||||
platform.openLink(attempt.data.url)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
answers: server ? { server } : {},
|
||||
inputs: server ? { server } : {},
|
||||
location,
|
||||
},
|
||||
{ signal },
|
||||
|
||||
@@ -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, answers: {}, location }),
|
||||
client.integration.oauth.connect({ integrationID: integration.id, methodID: method.id, inputs: {}, location }),
|
||||
)
|
||||
const attempt = started.data
|
||||
if (attempt.mode === "code")
|
||||
|
||||
@@ -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}`,
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -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 } : {}),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 })
|
||||
}),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
export { ClientApi, effectOmitEndpoints, groupNames, promiseOmitEndpoints } from "@opencode-ai/protocol/client"
|
||||
export {
|
||||
ClientApi,
|
||||
effectOmitEndpoints,
|
||||
groupNames,
|
||||
promiseOmitEndpoints,
|
||||
} from "@opencode-ai/protocol/client"
|
||||
|
||||
@@ -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"
|
||||
@@ -1052,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 answers: Form.Answer
|
||||
readonly label?: string | undefined
|
||||
}
|
||||
export type Endpoint10_3Output = void
|
||||
@@ -1064,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 answers: Form.Answer
|
||||
readonly inputs: { readonly [x: string]: string }
|
||||
readonly label?: string | undefined
|
||||
}
|
||||
export type Endpoint10_4Output = { readonly location: Location.Info; readonly data: Integration.Attempt }
|
||||
|
||||
@@ -715,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"], answers: input["answers"], label: input["label"] },
|
||||
payload: { key: input["key"], label: input["label"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
@@ -724,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"], answers: input["answers"], label: input["label"] },
|
||||
payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
|
||||
@@ -1030,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"], answers: input["answers"], label: input["label"] },
|
||||
body: { key: input["key"], label: input["label"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
empty: true,
|
||||
@@ -1045,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"], answers: input["answers"], label: input["label"] },
|
||||
body: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
empty: false,
|
||||
|
||||
@@ -195,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 }
|
||||
@@ -291,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 }
|
||||
@@ -1271,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
|
||||
@@ -1336,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 = {
|
||||
@@ -1639,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
|
||||
@@ -1892,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
|
||||
|
||||
@@ -1916,13 +1949,16 @@ export type SessionMessageAssistant = {
|
||||
retry?: SessionMessageAssistantRetry
|
||||
}
|
||||
|
||||
export type IntegrationOAuthMethod = { id: string; type: "oauth"; label: string; forms?: FormFields }
|
||||
|
||||
export type IntegrationKeyMethod = { type: "key"; label?: string; forms?: 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
|
||||
@@ -1945,12 +1981,6 @@ export type SessionMessageInfo =
|
||||
| SessionMessageAssistant
|
||||
| SessionMessageCompaction
|
||||
|
||||
export type IntegrationMethod =
|
||||
| IntegrationOAuthMethod
|
||||
| IntegrationCommandMethod
|
||||
| IntegrationKeyMethod
|
||||
| IntegrationEnvMethod
|
||||
|
||||
export type FormCreated = {
|
||||
id: string
|
||||
created: number
|
||||
@@ -2011,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
|
||||
@@ -3891,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 answers: { readonly [x: string]: string | number | boolean | ReadonlyArray<string> }
|
||||
readonly label?: string | undefined
|
||||
}["key"]
|
||||
readonly answers: {
|
||||
readonly key: string
|
||||
readonly answers: { readonly [x: string]: string | number | boolean | ReadonlyArray<string> }
|
||||
readonly label?: string | undefined
|
||||
}["answers"]
|
||||
readonly label?: {
|
||||
readonly key: string
|
||||
readonly answers: { readonly [x: string]: string | number | boolean | ReadonlyArray<string> }
|
||||
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
|
||||
@@ -3917,17 +3927,17 @@ export type IntegrationOauthConnectInput = {
|
||||
}["location"]
|
||||
readonly methodID: {
|
||||
readonly methodID: string
|
||||
readonly answers: { readonly [x: string]: string | number | boolean | ReadonlyArray<string> }
|
||||
readonly inputs: { readonly [x: string]: string }
|
||||
readonly label?: string | undefined
|
||||
}["methodID"]
|
||||
readonly answers: {
|
||||
readonly inputs: {
|
||||
readonly methodID: string
|
||||
readonly answers: { readonly [x: string]: string | number | boolean | ReadonlyArray<string> }
|
||||
readonly inputs: { readonly [x: string]: string }
|
||||
readonly label?: string | undefined
|
||||
}["answers"]
|
||||
}["inputs"]
|
||||
readonly label?: {
|
||||
readonly methodID: string
|
||||
readonly answers: { readonly [x: string]: string | number | boolean | ReadonlyArray<string> }
|
||||
readonly inputs: { readonly [x: string]: string }
|
||||
readonly label?: string | undefined
|
||||
}["label"]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -148,45 +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 submit form answers", 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",
|
||||
answers: { accountId: "account" },
|
||||
})
|
||||
await client.integration.oauth.connect({
|
||||
integrationID: "github-copilot",
|
||||
methodID: "device",
|
||||
answers: { deploymentType: "enterprise", enabled: true, scopes: ["read:user"] },
|
||||
})
|
||||
|
||||
expect(await requests[0].json()).toEqual({ key: "secret", answers: { accountId: "account" } })
|
||||
expect(await requests[1].json()).toEqual({
|
||||
methodID: "device",
|
||||
answers: { deploymentType: "enterprise", enabled: true, scopes: ["read:user"] },
|
||||
})
|
||||
})
|
||||
|
||||
test("health.stop sends exact replacement identity", async () => {
|
||||
let request: Request | undefined
|
||||
const client = OpenCode.make({
|
||||
|
||||
+20
-5
@@ -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
|
||||
}
|
||||
},
|
||||
|
||||
+4475
-1104
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
|
||||
|
||||
|
||||
+118
-39
@@ -2,7 +2,9 @@
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"id": "2d214a71-3b0a-48c1-a667-741952c4e188",
|
||||
"prevIds": ["f14a9b18-8207-487e-a3d3-227e629ba9ad"],
|
||||
"prevIds": [
|
||||
"f14a9b18-8207-487e-a3d3-227e629ba9ad"
|
||||
],
|
||||
"ddl": [
|
||||
{
|
||||
"name": "workspace",
|
||||
@@ -1383,9 +1385,13 @@
|
||||
"table": "session_v2"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1394,9 +1400,13 @@
|
||||
"table": "workspace"
|
||||
},
|
||||
{
|
||||
"columns": ["active_account_id"],
|
||||
"columns": [
|
||||
"active_account_id"
|
||||
],
|
||||
"tableTo": "account",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "SET NULL",
|
||||
"nameExplicit": false,
|
||||
@@ -1405,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,
|
||||
@@ -1416,9 +1430,13 @@
|
||||
"table": "event"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1427,9 +1445,13 @@
|
||||
"table": "permission"
|
||||
},
|
||||
{
|
||||
"columns": ["project_id"],
|
||||
"columns": [
|
||||
"project_id"
|
||||
],
|
||||
"tableTo": "project",
|
||||
"columnsTo": ["id"],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onUpdate": "NO ACTION",
|
||||
"onDelete": "CASCADE",
|
||||
"nameExplicit": false,
|
||||
@@ -1438,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,
|
||||
@@ -1449,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,
|
||||
@@ -1460,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,
|
||||
@@ -1471,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,
|
||||
@@ -1482,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,
|
||||
@@ -1493,119 +1535,156 @@
|
||||
"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"],
|
||||
"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",
|
||||
@@ -1861,4 +1940,4 @@
|
||||
}
|
||||
],
|
||||
"renames": []
|
||||
}
|
||||
}
|
||||
@@ -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* () {
|
||||
|
||||
+494
-505
File diff suppressed because it is too large
Load Diff
@@ -115,7 +115,8 @@ const layer = Layer.effect(
|
||||
}
|
||||
draft.providers.set(providerID, record)
|
||||
}
|
||||
const model = record.models.get(modelID) ?? (Model.Info.default(providerID, modelID) as Model.MutableInfo)
|
||||
const model =
|
||||
record.models.get(modelID) ?? (Model.Info.default(providerID, modelID) as Model.MutableInfo)
|
||||
if (!record.models.has(modelID)) record.models.set(modelID, model)
|
||||
fn(model)
|
||||
model.id = modelID
|
||||
@@ -248,7 +249,8 @@ const layer = Layer.effect(
|
||||
items,
|
||||
Order.mapInput(
|
||||
Order.Number,
|
||||
(item: (typeof candidates)[number]) => (item.cost / maxCost) * 0.8 + (item.age / maxAge) * 0.2,
|
||||
(item: (typeof candidates)[number]) =>
|
||||
(item.cost / maxCost) * 0.8 + (item.age / maxAge) * 0.2,
|
||||
),
|
||||
)
|
||||
return projectModel(selected.model, provider)
|
||||
|
||||
@@ -46,7 +46,9 @@ export function summarize(entries: ReadonlyArray<Entry>, budget = INLINE_BUDGET)
|
||||
.map((entry) => {
|
||||
const firstLine = entry.description.split("\n", 1)[0]?.trim() ?? ""
|
||||
const description =
|
||||
firstLine.length > DESCRIPTION_LIMIT ? firstLine.slice(0, DESCRIPTION_LIMIT - 3) + "..." : firstLine
|
||||
firstLine.length > DESCRIPTION_LIMIT
|
||||
? firstLine.slice(0, DESCRIPTION_LIMIT - 3) + "..."
|
||||
: firstLine
|
||||
const suffix = description.length === 0 ? "" : ` // ${description}`
|
||||
return { path: entry.path, line: ` - ${entry.signature}${suffix}` }
|
||||
})
|
||||
|
||||
@@ -43,9 +43,14 @@ const description = [
|
||||
|
||||
export const create = (
|
||||
registrations: ReadonlyMap<string, Info>,
|
||||
executeTool: (name: string, tool: Info, input: unknown, context: Context) => Effect.Effect<Result, Error>,
|
||||
executeTool: (
|
||||
name: string,
|
||||
tool: Info,
|
||||
input: unknown,
|
||||
context: Context,
|
||||
) => Effect.Effect<Result, Error>,
|
||||
) => {
|
||||
return {
|
||||
return ({
|
||||
name: "execute",
|
||||
description,
|
||||
input: CodeMode.Input,
|
||||
@@ -71,7 +76,7 @@ export const create = (
|
||||
const content =
|
||||
typeof executed.content === "string"
|
||||
? [{ type: "text" as const, text: executed.content }]
|
||||
: (executed.content ?? [])
|
||||
: executed.content ?? []
|
||||
const outputFileParts = outputFiles(content)
|
||||
if (outputFileParts.length > 0)
|
||||
yield* Ref.update(files, (items) => [...items, { index, files: outputFileParts }])
|
||||
@@ -129,7 +134,7 @@ export const create = (
|
||||
metadata,
|
||||
}
|
||||
}),
|
||||
} satisfies Info
|
||||
}) satisfies Info
|
||||
}
|
||||
|
||||
export const catalog = (registrations: ReadonlyMap<string, Info>) => {
|
||||
|
||||
+93
-108
@@ -52,108 +52,103 @@ export interface Interface extends State.Transformable<Draft> {
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Command") {}
|
||||
|
||||
export const layer = (options?: ShellSelect.Options) =>
|
||||
Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const mcp = yield* MCP.Service
|
||||
const bus = yield* Bus.Service
|
||||
const processes = yield* AppProcess.Service
|
||||
const config = yield* Config.Service
|
||||
const location = yield* Location.Service
|
||||
const state = State.create<Data, Draft>({
|
||||
name: "command",
|
||||
initial: () => ({ commands: new Map() }),
|
||||
draft: (draft) => ({
|
||||
list: () => Array.from(draft.commands.values()) as Info[],
|
||||
get: (name) => draft.commands.get(name),
|
||||
update: (name, update) => {
|
||||
const current = draft.commands.get(name) ?? ({ name, template: "" } as Types.DeepMutable<Info>)
|
||||
if (!draft.commands.has(name)) draft.commands.set(name, current)
|
||||
update(current)
|
||||
current.name = name
|
||||
},
|
||||
remove: (name) => {
|
||||
draft.commands.delete(name)
|
||||
},
|
||||
export const layer = (options?: ShellSelect.Options) => Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const mcp = yield* MCP.Service
|
||||
const bus = yield* Bus.Service
|
||||
const processes = yield* AppProcess.Service
|
||||
const config = yield* Config.Service
|
||||
const location = yield* Location.Service
|
||||
const state = State.create<Data, Draft>({
|
||||
name: "command",
|
||||
initial: () => ({ commands: new Map() }),
|
||||
draft: (draft) => ({
|
||||
list: () => Array.from(draft.commands.values()) as Info[],
|
||||
get: (name) => draft.commands.get(name),
|
||||
update: (name, update) => {
|
||||
const current = draft.commands.get(name) ?? ({ name, template: "" } as Types.DeepMutable<Info>)
|
||||
if (!draft.commands.has(name)) draft.commands.set(name, current)
|
||||
update(current)
|
||||
current.name = name
|
||||
},
|
||||
remove: (name) => {
|
||||
draft.commands.delete(name)
|
||||
},
|
||||
}),
|
||||
finalize: () => bus.publish(Command.Event.Updated, {}).pipe(Effect.asVoid),
|
||||
})
|
||||
const staticCommand = (name: string) => state.get().commands.get(name) as Info | undefined
|
||||
const mcpCommands = Effect.fnUntraced(function* () {
|
||||
return (yield* mcp.prompts()).map((prompt) =>
|
||||
Info.make({
|
||||
name: mcpCommandName(prompt.server, prompt.name),
|
||||
template: "",
|
||||
description: prompt.description,
|
||||
}),
|
||||
finalize: () => bus.publish(Command.Event.Updated, {}).pipe(Effect.asVoid),
|
||||
})
|
||||
const staticCommand = (name: string) => state.get().commands.get(name) as Info | undefined
|
||||
const mcpCommands = Effect.fnUntraced(function* () {
|
||||
return (yield* mcp.prompts()).map((prompt) =>
|
||||
Info.make({
|
||||
name: mcpCommandName(prompt.server, prompt.name),
|
||||
template: "",
|
||||
description: prompt.description,
|
||||
}),
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
return Service.of({
|
||||
reload: state.reload,
|
||||
transform: state.transform,
|
||||
get: Effect.fn("Command.get")(function* (name) {
|
||||
const command = staticCommand(name)
|
||||
if (command) return command
|
||||
return (yield* mcpCommands()).find((command) => command.name === name)
|
||||
}),
|
||||
list: Effect.fn("Command.list")(function* () {
|
||||
const commands = Array.from(state.get().commands.values()) as Info[]
|
||||
const names = new Set(commands.map((command) => command.name))
|
||||
return [...commands, ...(yield* mcpCommands()).filter((command) => !names.has(command.name))]
|
||||
}),
|
||||
evaluate: Effect.fn("Command.evaluate")(function* (input) {
|
||||
const command = staticCommand(input.name)
|
||||
if (command)
|
||||
return yield* evaluateTemplate(input.name, command.template, input.arguments ?? "", {
|
||||
config,
|
||||
location,
|
||||
processes,
|
||||
shell: options,
|
||||
})
|
||||
return Service.of({
|
||||
reload: state.reload,
|
||||
transform: state.transform,
|
||||
get: Effect.fn("Command.get")(function* (name) {
|
||||
const command = staticCommand(name)
|
||||
if (command) return command
|
||||
return (yield* mcpCommands()).find((command) => command.name === name)
|
||||
}),
|
||||
list: Effect.fn("Command.list")(function* () {
|
||||
const commands = Array.from(state.get().commands.values()) as Info[]
|
||||
const names = new Set(commands.map((command) => command.name))
|
||||
return [
|
||||
...commands,
|
||||
...(yield* mcpCommands()).filter((command) => !names.has(command.name)),
|
||||
]
|
||||
}),
|
||||
evaluate: Effect.fn("Command.evaluate")(function* (input) {
|
||||
const command = staticCommand(input.name)
|
||||
if (command) return yield* evaluateTemplate(input.name, command.template, input.arguments ?? "", {
|
||||
config,
|
||||
location,
|
||||
processes,
|
||||
shell: options,
|
||||
})
|
||||
|
||||
const prompt = (yield* mcp.prompts()).find(
|
||||
(prompt) => mcpCommandName(prompt.server, prompt.name) === input.name,
|
||||
)
|
||||
if (!prompt)
|
||||
return yield* new NotFoundError({ command: input.name, message: `Command not found: ${input.name}` })
|
||||
const result = yield* mcp
|
||||
.prompt({
|
||||
server: prompt.server,
|
||||
name: prompt.name,
|
||||
args: Object.fromEntries(
|
||||
(prompt.arguments ?? []).map((argument, index) => [
|
||||
argument.name,
|
||||
parseArguments(input.arguments ?? "")[index] ?? "",
|
||||
]),
|
||||
),
|
||||
})
|
||||
.pipe(
|
||||
Effect.catchTag("MCP.NotFoundError", () =>
|
||||
const prompt = (yield* mcp.prompts()).find((prompt) => mcpCommandName(prompt.server, prompt.name) === input.name)
|
||||
if (!prompt) return yield* new NotFoundError({ command: input.name, message: `Command not found: ${input.name}` })
|
||||
const result = yield* mcp
|
||||
.prompt({
|
||||
server: prompt.server,
|
||||
name: prompt.name,
|
||||
args: Object.fromEntries(
|
||||
(prompt.arguments ?? []).map((argument, index) => [
|
||||
argument.name,
|
||||
parseArguments(input.arguments ?? "")[index] ?? "",
|
||||
]),
|
||||
),
|
||||
})
|
||||
.pipe(
|
||||
Effect.catchTag(
|
||||
"MCP.NotFoundError",
|
||||
() =>
|
||||
Effect.fail(
|
||||
new EvaluationError({
|
||||
command: input.name,
|
||||
message: `MCP server could not be found while evaluating prompt: ${prompt.server}`,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
if (!result)
|
||||
return yield* new EvaluationError({
|
||||
command: input.name,
|
||||
message: `MCP prompt could not be evaluated: ${prompt.server}:${prompt.name}`,
|
||||
})
|
||||
return {
|
||||
text: result.messages
|
||||
.map((message) => promptMessageText(message.content))
|
||||
.join("\n")
|
||||
.trim(),
|
||||
}
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
),
|
||||
)
|
||||
if (!result)
|
||||
return yield* new EvaluationError({
|
||||
command: input.name,
|
||||
message: `MCP prompt could not be evaluated: ${prompt.server}:${prompt.name}`,
|
||||
})
|
||||
return { text: result.messages.map((message) => promptMessageText(message.content)).join("\n").trim() }
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
function evaluateTemplate(
|
||||
command: string,
|
||||
@@ -184,8 +179,7 @@ function evaluateArguments(template: string, input: string) {
|
||||
return args[argIndex]
|
||||
})
|
||||
const withArguments = expanded.replaceAll("$ARGUMENTS", input)
|
||||
if (placeholders.length === 0 && !template.includes("$ARGUMENTS") && input.trim())
|
||||
return `${withArguments}\n\n${input}`.trim()
|
||||
if (placeholders.length === 0 && !template.includes("$ARGUMENTS") && input.trim()) return `${withArguments}\n\n${input}`.trim()
|
||||
return withArguments.trim()
|
||||
}
|
||||
|
||||
@@ -207,23 +201,14 @@ const evaluateShell = Effect.fnUntraced(function* (
|
||||
(match) => {
|
||||
const source = match[1] ?? ""
|
||||
return services.processes
|
||||
.run(
|
||||
ChildProcess.make(shell, ShellSelect.args(shell, source), {
|
||||
cwd: services.location.directory,
|
||||
stdin: "ignore",
|
||||
}),
|
||||
{
|
||||
combineOutput: true,
|
||||
},
|
||||
)
|
||||
.run(ChildProcess.make(shell, ShellSelect.args(shell, source), { cwd: services.location.directory, stdin: "ignore" }), {
|
||||
combineOutput: true,
|
||||
})
|
||||
.pipe(
|
||||
Effect.map((result) => (result.output ?? Buffer.concat([result.stdout, result.stderr])).toString("utf8")),
|
||||
Effect.mapError(
|
||||
(error) =>
|
||||
new EvaluationError({
|
||||
command,
|
||||
message: `Shell interpolation failed for ${JSON.stringify(source)}: ${error.message}`,
|
||||
}),
|
||||
new EvaluationError({ command, message: `Shell interpolation failed for ${JSON.stringify(source)}: ${error.message}` }),
|
||||
),
|
||||
)
|
||||
},
|
||||
|
||||
+259
-264
@@ -78,292 +78,287 @@ export const testLayer = (initial: Entry[] = []) =>
|
||||
}),
|
||||
)
|
||||
|
||||
export const layer = (options?: Options) =>
|
||||
Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* FSUtil.Service
|
||||
const global = yield* Global.Service
|
||||
const location = yield* Location.Service
|
||||
const watcher = yield* Watcher.Service
|
||||
const bus = yield* Bus.Service
|
||||
const credentials = yield* Credential.Service
|
||||
const wellknown = yield* WellKnown.Service
|
||||
const names = ["opencode.json", "opencode.jsonc"]
|
||||
const reloadLock = Semaphore.makeUnsafe(1)
|
||||
const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const
|
||||
const decodeInfo = Schema.decodeUnknownOption(Info, decodeOptions)
|
||||
const parseInfo = Effect.fn("Config.parseInfo")(function* (text: string, source: string) {
|
||||
const errors: ParseError[] = []
|
||||
const input: unknown = parse(text, errors, { allowTrailingComma: true })
|
||||
if (errors.length) {
|
||||
yield* Effect.logWarning("configuration normalization diagnostic", {
|
||||
source,
|
||||
path: "$",
|
||||
kind: "invalid",
|
||||
action: "rejected malformed JSON or JSONC document",
|
||||
})
|
||||
return
|
||||
}
|
||||
const result = ConfigNormalize.normalize(input)
|
||||
yield* Effect.forEach(result.diagnostics, (diagnostic) =>
|
||||
Effect.logWarning("configuration normalization diagnostic", {
|
||||
source,
|
||||
path: diagnostic.path[0] === "$" ? "$" : `$.${diagnostic.path.join(".")}`,
|
||||
kind: diagnostic.kind,
|
||||
action: diagnostic.message,
|
||||
}),
|
||||
)
|
||||
if (result.type === "rejected") return
|
||||
const info = Option.getOrUndefined(decodeInfo(result.encoded))
|
||||
if (info) return info
|
||||
export const layer = (options?: Options) => Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const fs = yield* FSUtil.Service
|
||||
const global = yield* Global.Service
|
||||
const location = yield* Location.Service
|
||||
const watcher = yield* Watcher.Service
|
||||
const bus = yield* Bus.Service
|
||||
const credentials = yield* Credential.Service
|
||||
const wellknown = yield* WellKnown.Service
|
||||
const names = ["opencode.json", "opencode.jsonc"]
|
||||
const reloadLock = Semaphore.makeUnsafe(1)
|
||||
const decodeOptions = { errors: "all", onExcessProperty: "ignore", propertyOrder: "original" } as const
|
||||
const decodeInfo = Schema.decodeUnknownOption(Info, decodeOptions)
|
||||
const parseInfo = Effect.fn("Config.parseInfo")(function* (text: string, source: string) {
|
||||
const errors: ParseError[] = []
|
||||
const input: unknown = parse(text, errors, { allowTrailingComma: true })
|
||||
if (errors.length) {
|
||||
yield* Effect.logWarning("configuration normalization diagnostic", {
|
||||
source,
|
||||
path: "$",
|
||||
kind: "invalid",
|
||||
action: "rejected canonical configuration after final validation",
|
||||
action: "rejected malformed JSON or JSONC document",
|
||||
})
|
||||
return
|
||||
}
|
||||
const result = ConfigNormalize.normalize(input)
|
||||
yield* Effect.forEach(result.diagnostics, (diagnostic) =>
|
||||
Effect.logWarning("configuration normalization diagnostic", {
|
||||
source,
|
||||
path: diagnostic.path[0] === "$" ? "$" : `$.${diagnostic.path.join(".")}`,
|
||||
kind: diagnostic.kind,
|
||||
action: diagnostic.message,
|
||||
}),
|
||||
)
|
||||
if (result.type === "rejected") return
|
||||
const info = Option.getOrUndefined(decodeInfo(result.encoded))
|
||||
if (info) return info
|
||||
yield* Effect.logWarning("configuration normalization diagnostic", {
|
||||
source,
|
||||
path: "$",
|
||||
kind: "invalid",
|
||||
action: "rejected canonical configuration after final validation",
|
||||
})
|
||||
})
|
||||
|
||||
const loadFile = Effect.fnUntraced(function* (filepath: string) {
|
||||
const text = yield* fs.readFileStringSafe(filepath)
|
||||
if (text === undefined) return
|
||||
const substituted = yield* ConfigVariable.substitute({ type: "path", path: filepath, text })
|
||||
const info = yield* parseInfo(substituted, filepath)
|
||||
if (!info) return
|
||||
return new Document({ type: "document", path: filepath, info })
|
||||
})
|
||||
const loadFile = Effect.fnUntraced(function* (filepath: string) {
|
||||
const text = yield* fs.readFileStringSafe(filepath)
|
||||
if (text === undefined) return
|
||||
const substituted = yield* ConfigVariable.substitute({ type: "path", path: filepath, text })
|
||||
const info = yield* parseInfo(substituted, filepath)
|
||||
if (!info) return
|
||||
return new Document({ type: "document", path: filepath, info })
|
||||
})
|
||||
|
||||
const loadWellknown = Effect.fn("Config.loadWellknown")(function* () {
|
||||
const entries = yield* wellknown
|
||||
.entries()
|
||||
.pipe(
|
||||
Effect.catch((error) =>
|
||||
Effect.logWarning("failed to discover wellknown config", { error }).pipe(Effect.as([] as const)),
|
||||
),
|
||||
const loadWellknown = Effect.fn("Config.loadWellknown")(function* () {
|
||||
const entries = yield* wellknown
|
||||
.entries()
|
||||
.pipe(
|
||||
Effect.catch((error) =>
|
||||
Effect.logWarning("failed to discover wellknown config", { error }).pipe(Effect.as([] as const)),
|
||||
),
|
||||
)
|
||||
return yield* Effect.forEach(entries, (entry) =>
|
||||
Effect.gen(function* () {
|
||||
const auth = entry.manifest.auth
|
||||
if (!auth) return []
|
||||
const credential = (yield* credentials.list(entry.integrationID)).findLast(
|
||||
(credential) => credential.value.type === "key",
|
||||
)
|
||||
return yield* Effect.forEach(entries, (entry) =>
|
||||
Effect.gen(function* () {
|
||||
const auth = entry.manifest.auth
|
||||
if (!auth) return []
|
||||
const credential = (yield* credentials.list(entry.integrationID)).findLast(
|
||||
(credential) => credential.value.type === "key",
|
||||
)
|
||||
if (!credential || credential.value.type !== "key") return []
|
||||
const variables = { [auth.env]: credential.value.key }
|
||||
const configs = yield* wellknown.resolve(entry, variables).pipe(Effect.orDie)
|
||||
return yield* Effect.forEach(configs, (config) =>
|
||||
ConfigVariable.substitute({
|
||||
type: "virtual",
|
||||
source: entry.origin,
|
||||
dir: entry.origin,
|
||||
text: JSON.stringify(config),
|
||||
env: variables,
|
||||
}).pipe(
|
||||
Effect.flatMap((text) => parseInfo(text, entry.origin)),
|
||||
Effect.map((info) => (info ? new Document({ type: "document", info }) : undefined)),
|
||||
),
|
||||
).pipe(Effect.map((documents) => documents.filter((document) => document !== undefined)))
|
||||
}),
|
||||
).pipe(Effect.map((documents) => documents.flat()))
|
||||
})
|
||||
if (!credential || credential.value.type !== "key") return []
|
||||
const variables = { [auth.env]: credential.value.key }
|
||||
const configs = yield* wellknown.resolve(entry, variables).pipe(Effect.orDie)
|
||||
return yield* Effect.forEach(configs, (config) =>
|
||||
ConfigVariable.substitute({
|
||||
type: "virtual",
|
||||
source: entry.origin,
|
||||
dir: entry.origin,
|
||||
text: JSON.stringify(config),
|
||||
env: variables,
|
||||
}).pipe(
|
||||
Effect.flatMap((text) => parseInfo(text, entry.origin)),
|
||||
Effect.map((info) => (info ? new Document({ type: "document", info }) : undefined)),
|
||||
),
|
||||
).pipe(Effect.map((documents) => documents.filter((document) => document !== undefined)))
|
||||
}),
|
||||
).pipe(Effect.map((documents) => documents.flat()))
|
||||
})
|
||||
|
||||
const loadDirectory = Effect.fnUntraced(function* (directory: AbsolutePath) {
|
||||
return [
|
||||
...(yield* Effect.forEach(names, (file) => loadFile(path.join(directory, file))).pipe(
|
||||
Effect.map((configs) => configs.filter((config): config is Document => config !== undefined)),
|
||||
)),
|
||||
new Directory({ type: "directory", path: directory }),
|
||||
]
|
||||
})
|
||||
const loadDirectory = Effect.fnUntraced(function* (directory: AbsolutePath) {
|
||||
return [
|
||||
...(yield* Effect.forEach(names, (file) => loadFile(path.join(directory, file))).pipe(
|
||||
Effect.map((configs) => configs.filter((config): config is Document => config !== undefined)),
|
||||
)),
|
||||
new Directory({ type: "directory", path: directory }),
|
||||
]
|
||||
})
|
||||
|
||||
const discover = Effect.fn("Config.discover")(function* () {
|
||||
const globalDirectory = AbsolutePath.make(global.config)
|
||||
const globalAgentsDirectory = AbsolutePath.make(path.join(global.home, ".agents"))
|
||||
const globalClaudeDirectory = AbsolutePath.make(path.join(global.home, ".claude"))
|
||||
const locationIsGlobal = path.resolve(location.directory) === path.resolve(global.config)
|
||||
const discovered =
|
||||
locationIsGlobal || options?.project === false
|
||||
? []
|
||||
: yield* fs
|
||||
.up({
|
||||
targets: [".opencode", ".claude", ".agents", ...names.toReversed()],
|
||||
start: location.directory,
|
||||
})
|
||||
.pipe(Effect.orDie)
|
||||
const discover = Effect.fn("Config.discover")(function* () {
|
||||
const globalDirectory = AbsolutePath.make(global.config)
|
||||
const globalAgentsDirectory = AbsolutePath.make(path.join(global.home, ".agents"))
|
||||
const globalClaudeDirectory = AbsolutePath.make(path.join(global.home, ".claude"))
|
||||
const locationIsGlobal = path.resolve(location.directory) === path.resolve(global.config)
|
||||
const discovered = locationIsGlobal || options?.project === false
|
||||
? []
|
||||
: yield* fs
|
||||
.up({
|
||||
targets: [".opencode", ".claude", ".agents", ...names.toReversed()],
|
||||
start: location.directory,
|
||||
})
|
||||
.pipe(Effect.orDie)
|
||||
|
||||
// We load certain files from a few other folders in the ecosystem
|
||||
const claude = [
|
||||
...new Set([
|
||||
...((yield* fs.isDir(globalClaudeDirectory)) ? [globalClaudeDirectory] : []),
|
||||
...discovered.filter((item) => path.basename(item) === ".claude"),
|
||||
]),
|
||||
].map((directory) => new ClaudeDirectory({ type: "claude", path: AbsolutePath.make(directory) }))
|
||||
const agents = [
|
||||
...new Set([
|
||||
...((yield* fs.isDir(globalAgentsDirectory)) ? [globalAgentsDirectory] : []),
|
||||
...discovered.filter((item) => path.basename(item) === ".agents"),
|
||||
]),
|
||||
].map((directory) => new AgentsDirectory({ type: "agents", path: AbsolutePath.make(directory) }))
|
||||
// We load certain files from a few other folders in the ecosystem
|
||||
const claude = [
|
||||
...new Set([
|
||||
...((yield* fs.isDir(globalClaudeDirectory)) ? [globalClaudeDirectory] : []),
|
||||
...discovered.filter((item) => path.basename(item) === ".claude"),
|
||||
]),
|
||||
].map((directory) => new ClaudeDirectory({ type: "claude", path: AbsolutePath.make(directory) }))
|
||||
const agents = [
|
||||
...new Set([
|
||||
...((yield* fs.isDir(globalAgentsDirectory)) ? [globalAgentsDirectory] : []),
|
||||
...discovered.filter((item) => path.basename(item) === ".agents"),
|
||||
]),
|
||||
].map((directory) => new AgentsDirectory({ type: "agents", path: AbsolutePath.make(directory) }))
|
||||
|
||||
const directories = [
|
||||
globalDirectory,
|
||||
...discovered
|
||||
.filter((item) => path.basename(item) === ".opencode")
|
||||
.toReversed()
|
||||
.map((directory) => AbsolutePath.make(directory)),
|
||||
]
|
||||
const directPaths = discovered
|
||||
.filter((item) => ![".agents", ".claude", ".opencode"].includes(path.basename(item)))
|
||||
const directories = [
|
||||
globalDirectory,
|
||||
...discovered
|
||||
.filter((item) => path.basename(item) === ".opencode")
|
||||
.toReversed()
|
||||
const direct = yield* Effect.forEach(directPaths, (filepath) =>
|
||||
loadFile(filepath).pipe(
|
||||
.map((directory) => AbsolutePath.make(directory)),
|
||||
]
|
||||
const directPaths = discovered
|
||||
.filter((item) => ![".agents", ".claude", ".opencode"].includes(path.basename(item)))
|
||||
.toReversed()
|
||||
const direct = yield* Effect.forEach(directPaths, (filepath) =>
|
||||
loadFile(filepath).pipe(
|
||||
Effect.map((config) => [
|
||||
...(config ? [config] : []),
|
||||
new File({ type: "file", path: AbsolutePath.make(filepath) }),
|
||||
]),
|
||||
),
|
||||
).pipe(
|
||||
Effect.orDie,
|
||||
Effect.map((entries) => entries.flat()),
|
||||
)
|
||||
|
||||
const file = options?.file
|
||||
const explicit = file
|
||||
? yield* loadFile(path.resolve(file)).pipe(
|
||||
Effect.map((config) => [
|
||||
...(config ? [config] : []),
|
||||
new File({ type: "file", path: AbsolutePath.make(filepath) }),
|
||||
new File({ type: "file", path: AbsolutePath.make(path.resolve(file)) }),
|
||||
]),
|
||||
),
|
||||
).pipe(
|
||||
Effect.orDie,
|
||||
Effect.map((entries) => entries.flat()),
|
||||
)
|
||||
|
||||
const file = options?.file
|
||||
const explicit = file
|
||||
? yield* loadFile(path.resolve(file)).pipe(
|
||||
Effect.map((config) => [
|
||||
...(config ? [config] : []),
|
||||
new File({ type: "file", path: AbsolutePath.make(path.resolve(file)) }),
|
||||
]),
|
||||
Effect.orDie,
|
||||
)
|
||||
: []
|
||||
const content =
|
||||
options?.content !== undefined
|
||||
? yield* ConfigVariable.substitute({
|
||||
type: "virtual",
|
||||
source: "OPENCODE_CONFIG_CONTENT",
|
||||
dir: location.directory,
|
||||
text: options.content,
|
||||
}).pipe(
|
||||
Effect.flatMap((text) => parseInfo(text, "OPENCODE_CONFIG_CONTENT")),
|
||||
Effect.map((info) => (info ? [new Document({ type: "document", info })] : [])),
|
||||
Effect.orDie,
|
||||
)
|
||||
: []
|
||||
|
||||
const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie)
|
||||
return [
|
||||
...(yield* loadWellknown().pipe(Effect.orDie)),
|
||||
...claude,
|
||||
...agents,
|
||||
...(supplementary[0] ?? []),
|
||||
...explicit,
|
||||
...direct,
|
||||
...supplementary.slice(1).flat(),
|
||||
...content,
|
||||
]
|
||||
})
|
||||
|
||||
const initial = yield* discover()
|
||||
let configs = initial
|
||||
const updates = yield* PubSub.unbounded<Watcher.Update>()
|
||||
// Vendored trees inside config roots (a plugin's node_modules, a nested
|
||||
// .git) produce event blizzards that can never change discovery output.
|
||||
const ignore = ["node_modules", ".git", "**/{node_modules,.git}/**"]
|
||||
// Watch-once: roots leave discovery only by deletion, so a stale watch is
|
||||
// inert, bounded, and dies with this layer — and keeping a deleted root's
|
||||
// watch alive is exactly what makes its recreation observable.
|
||||
const watched = new Set<string>()
|
||||
const reconcile = Effect.fn("Config.reconcileWatches")(function* (entries: readonly Entry[]) {
|
||||
const directories = entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : []))
|
||||
const files = entries.flatMap((entry) => (entry.type === "file" ? [entry.path] : []))
|
||||
const targets = [
|
||||
...directories.map((path) => ({ path, type: "directory" as const, ignore })),
|
||||
...files
|
||||
.filter((file) => !directories.some((directory) => FSUtil.contains(directory, file)))
|
||||
.map((path) => ({ path, type: "file" as const })),
|
||||
]
|
||||
for (const target of targets) {
|
||||
const key = JSON.stringify(target)
|
||||
if (watched.has(key)) continue
|
||||
watched.add(key)
|
||||
const stream = yield* watcher.subscribe(target)
|
||||
yield* stream.pipe(
|
||||
Stream.runForEach((update) => PubSub.publish(updates, update)),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
Effect.orDie,
|
||||
)
|
||||
}
|
||||
})
|
||||
: []
|
||||
const content = options?.content !== undefined
|
||||
? yield* ConfigVariable.substitute({
|
||||
type: "virtual",
|
||||
source: "OPENCODE_CONFIG_CONTENT",
|
||||
dir: location.directory,
|
||||
text: options.content,
|
||||
}).pipe(
|
||||
Effect.flatMap((text) => parseInfo(text, "OPENCODE_CONFIG_CONTENT")),
|
||||
Effect.map((info) => (info ? [new Document({ type: "document", info })] : [])),
|
||||
Effect.orDie,
|
||||
)
|
||||
: []
|
||||
|
||||
const reload = Effect.fn("Config.reload")(() =>
|
||||
reloadLock.withPermit(
|
||||
Effect.gen(function* () {
|
||||
const next = yield* discover()
|
||||
if (isDeepStrictEqual(configs, next)) return
|
||||
configs = next
|
||||
yield* reconcile(next)
|
||||
yield* bus.publish(Event.Updated, {})
|
||||
}),
|
||||
),
|
||||
)
|
||||
const supplementary = yield* Effect.forEach(directories, loadDirectory).pipe(Effect.orDie)
|
||||
return [
|
||||
...(yield* loadWellknown().pipe(Effect.orDie)),
|
||||
...claude,
|
||||
...agents,
|
||||
...(supplementary[0] ?? []),
|
||||
...explicit,
|
||||
...direct,
|
||||
...supplementary.slice(1).flat(),
|
||||
...content,
|
||||
]
|
||||
})
|
||||
|
||||
yield* Stream.fromPubSub(updates).pipe(
|
||||
Stream.debounce("100 millis"),
|
||||
Stream.runForEach((update) =>
|
||||
reload().pipe(
|
||||
Effect.catchCause((cause) => Effect.logError("failed to reload config", { path: update.path, cause })),
|
||||
),
|
||||
),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe(
|
||||
Stream.filterEffect((event) =>
|
||||
wellknown.entries().pipe(
|
||||
Effect.map((entries) => entries.some((entry) => entry.integrationID === event.data.integrationID)),
|
||||
Effect.catch(() => Effect.succeed(false)),
|
||||
),
|
||||
),
|
||||
Stream.runForEach(() =>
|
||||
reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload wellknown config", { cause }))),
|
||||
),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
yield* bus.subscribe(WellKnown.Event.Updated).pipe(
|
||||
Stream.runForEach(() =>
|
||||
reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload wellknown sources", { cause }))),
|
||||
),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
yield* Effect.sleep("10 minutes").pipe(
|
||||
Effect.andThen(
|
||||
Effect.suspend(() => {
|
||||
if (!wellknown.snapshot().length) return Effect.void
|
||||
return Effect.gen(function* () {
|
||||
const changed = yield* wellknown
|
||||
.refresh()
|
||||
.pipe(
|
||||
Effect.catch((error) =>
|
||||
Effect.logWarning("failed to refresh wellknown manifests", { error }).pipe(Effect.as(false)),
|
||||
),
|
||||
)
|
||||
if (!changed) yield* reload()
|
||||
}).pipe(Effect.catchCause((cause) => Effect.logWarning("failed to refresh wellknown config", { cause })))
|
||||
}),
|
||||
),
|
||||
Effect.forever,
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
yield* reconcile(initial)
|
||||
const initial = yield* discover()
|
||||
let configs = initial
|
||||
const updates = yield* PubSub.unbounded<Watcher.Update>()
|
||||
// Vendored trees inside config roots (a plugin's node_modules, a nested
|
||||
// .git) produce event blizzards that can never change discovery output.
|
||||
const ignore = ["node_modules", ".git", "**/{node_modules,.git}/**"]
|
||||
// Watch-once: roots leave discovery only by deletion, so a stale watch is
|
||||
// inert, bounded, and dies with this layer — and keeping a deleted root's
|
||||
// watch alive is exactly what makes its recreation observable.
|
||||
const watched = new Set<string>()
|
||||
const reconcile = Effect.fn("Config.reconcileWatches")(function* (entries: readonly Entry[]) {
|
||||
const directories = entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : []))
|
||||
const files = entries.flatMap((entry) => (entry.type === "file" ? [entry.path] : []))
|
||||
const targets = [
|
||||
...directories.map((path) => ({ path, type: "directory" as const, ignore })),
|
||||
...files
|
||||
.filter((file) => !directories.some((directory) => FSUtil.contains(directory, file)))
|
||||
.map((path) => ({ path, type: "file" as const })),
|
||||
]
|
||||
for (const target of targets) {
|
||||
const key = JSON.stringify(target)
|
||||
if (watched.has(key)) continue
|
||||
watched.add(key)
|
||||
const stream = yield* watcher.subscribe(target)
|
||||
yield* stream.pipe(
|
||||
Stream.runForEach((update) => PubSub.publish(updates, update)),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return Service.of({
|
||||
entries: Effect.fn("Config.entries")(function* () {
|
||||
return configs
|
||||
const reload = Effect.fn("Config.reload")(() =>
|
||||
reloadLock.withPermit(
|
||||
Effect.gen(function* () {
|
||||
const next = yield* discover()
|
||||
if (isDeepStrictEqual(configs, next)) return
|
||||
configs = next
|
||||
yield* reconcile(next)
|
||||
yield* bus.publish(Event.Updated, {})
|
||||
}),
|
||||
changes: () => Stream.fromPubSub(updates),
|
||||
})
|
||||
}),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
yield* Stream.fromPubSub(updates).pipe(
|
||||
Stream.debounce("100 millis"),
|
||||
Stream.runForEach((update) =>
|
||||
reload().pipe(
|
||||
Effect.catchCause((cause) => Effect.logError("failed to reload config", { path: update.path, cause })),
|
||||
),
|
||||
),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
yield* bus.subscribe(Integration.Event.ConnectionUpdated).pipe(
|
||||
Stream.filterEffect((event) =>
|
||||
wellknown.entries().pipe(
|
||||
Effect.map((entries) => entries.some((entry) => entry.integrationID === event.data.integrationID)),
|
||||
Effect.catch(() => Effect.succeed(false)),
|
||||
),
|
||||
),
|
||||
Stream.runForEach(() =>
|
||||
reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload wellknown config", { cause }))),
|
||||
),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
yield* bus.subscribe(WellKnown.Event.Updated).pipe(
|
||||
Stream.runForEach(() =>
|
||||
reload().pipe(Effect.catchCause((cause) => Effect.logError("failed to reload wellknown sources", { cause }))),
|
||||
),
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
yield* Effect.sleep("10 minutes").pipe(
|
||||
Effect.andThen(
|
||||
Effect.suspend(() => {
|
||||
if (!wellknown.snapshot().length) return Effect.void
|
||||
return Effect.gen(function* () {
|
||||
const changed = yield* wellknown.refresh().pipe(
|
||||
Effect.catch((error) =>
|
||||
Effect.logWarning("failed to refresh wellknown manifests", { error }).pipe(Effect.as(false)),
|
||||
),
|
||||
)
|
||||
if (!changed) yield* reload()
|
||||
}).pipe(Effect.catchCause((cause) => Effect.logWarning("failed to refresh wellknown config", { cause })))
|
||||
}),
|
||||
),
|
||||
Effect.forever,
|
||||
Effect.forkScoped({ startImmediately: true }),
|
||||
)
|
||||
yield* reconcile(initial)
|
||||
|
||||
return Service.of({
|
||||
entries: Effect.fn("Config.entries")(function* () {
|
||||
return configs
|
||||
}),
|
||||
changes: () => Stream.fromPubSub(updates),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export function configured(options?: Options) {
|
||||
return makeLocationNode({
|
||||
|
||||
@@ -63,7 +63,9 @@ export const Plugin = define({
|
||||
Effect.catch(() => Effect.succeed(undefined)),
|
||||
),
|
||||
).pipe(
|
||||
Effect.map((documents) => documents.filter((document): document is Document => document !== undefined)),
|
||||
Effect.map((documents) =>
|
||||
documents.filter((document): document is Document => document !== undefined),
|
||||
),
|
||||
)
|
||||
})
|
||||
}).pipe(Effect.map((documents) => documents.flat()))
|
||||
|
||||
@@ -92,7 +92,9 @@ type Options = {
|
||||
readonly nextDatabasePath?: string
|
||||
}
|
||||
|
||||
type MigrationState = { readonly phase: "sessions"; readonly cursor?: string } | { readonly phase: "completed" }
|
||||
type MigrationState =
|
||||
| { readonly phase: "sessions"; readonly cursor?: string }
|
||||
| { readonly phase: "completed" }
|
||||
|
||||
type RuntimeState =
|
||||
| { readonly status: "idle" }
|
||||
@@ -485,10 +487,7 @@ export function run(options: Options = {}): Effect.Effect<RunResult, never, Data
|
||||
.transaction((tx) =>
|
||||
Effect.gen(function* () {
|
||||
yield* tx.delete(EventTable).run()
|
||||
yield* tx
|
||||
.insert(KVTable)
|
||||
.values({ key: MIGRATION_STATE_KEY, value: { phase: "sessions" } })
|
||||
.run()
|
||||
yield* tx.insert(KVTable).values({ key: MIGRATION_STATE_KEY, value: { phase: "sessions" } }).run()
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
@@ -506,9 +505,7 @@ export function run(options: Options = {}): Effect.Effect<RunResult, never, Data
|
||||
updateProgress({ label: "Migrating sessions", numerator: migrated + completed, denominator })
|
||||
})
|
||||
updateProgress({ label: "Migrating sessions", numerator: migrated + sourceTotal, denominator })
|
||||
const projects = new Set(
|
||||
(yield* db.all<{ id: string }>(sql`SELECT id FROM project`)).map((project) => project.id),
|
||||
)
|
||||
const projects = new Set((yield* db.all<{ id: string }>(sql`SELECT id FROM project`)).map((project) => project.id))
|
||||
while (true) {
|
||||
const state = yield* readState(db)
|
||||
const cursorValue = state?.phase === "sessions" ? state.cursor : undefined
|
||||
@@ -597,7 +594,7 @@ export function run(options: Options = {}): Effect.Effect<RunResult, never, Data
|
||||
numerator: (runtimeState.progress.numerator ?? 0) + 1,
|
||||
denominator,
|
||||
},
|
||||
}
|
||||
}
|
||||
yield* Effect.yieldNow
|
||||
}
|
||||
yield* db
|
||||
@@ -684,10 +681,9 @@ function importNextDatabase(
|
||||
})
|
||||
}
|
||||
const messages = source
|
||||
.query<
|
||||
NextMessage,
|
||||
[string]
|
||||
>("SELECT id, session_id, type, seq, time_created, time_updated, data FROM session_message WHERE session_id = ? ORDER BY seq")
|
||||
.query<NextMessage, [string]>(
|
||||
"SELECT id, session_id, type, seq, time_created, time_updated, data FROM session_message WHERE session_id = ? ORDER BY seq",
|
||||
)
|
||||
.all(session.id)
|
||||
yield* db
|
||||
.transaction((tx) =>
|
||||
|
||||
@@ -4,7 +4,12 @@ import { Effect, Layer } from "effect"
|
||||
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
|
||||
import { Bus } from "./bus"
|
||||
|
||||
const Types = new Set(["agent.updated", "catalog.updated", "command.updated", "config.updated"])
|
||||
const Types = new Set([
|
||||
"agent.updated",
|
||||
"catalog.updated",
|
||||
"command.updated",
|
||||
"config.updated",
|
||||
])
|
||||
|
||||
export const layer = Layer.effectDiscard(
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -92,7 +92,9 @@ export const layer = (options?: Options) =>
|
||||
const watchers = yield* RcMap.make({
|
||||
lookup: (key: Key) =>
|
||||
Effect.gen(function* () {
|
||||
const pubsub = yield* Effect.acquireRelease(PubSub.unbounded<Update>(), (pubsub) => PubSub.shutdown(pubsub))
|
||||
const pubsub = yield* Effect.acquireRelease(PubSub.unbounded<Update>(), (pubsub) =>
|
||||
PubSub.shutdown(pubsub),
|
||||
)
|
||||
const subscription = yield* Effect.acquireRelease(
|
||||
native.subscribe({
|
||||
type: key.type,
|
||||
|
||||
@@ -180,14 +180,10 @@ export const layer = Layer.effect(
|
||||
Effect.gen(function* () {
|
||||
const entry = yield* find(input.id)
|
||||
if (entry.state.status !== "pending") return yield* new AlreadySettledError({ id: input.id })
|
||||
const invalid = validateAnswer(entry.form.fields, input.answer)
|
||||
const invalid = validateAnswer(entry.form, input.answer)
|
||||
if (invalid) return yield* new InvalidAnswerError({ id: input.id, message: invalid })
|
||||
const next: TerminalState = { status: "answered", answer: input.answer }
|
||||
yield* bus.publish(Form.Event.Replied, {
|
||||
id: input.id,
|
||||
sessionID: entry.form.sessionID,
|
||||
answer: input.answer,
|
||||
})
|
||||
yield* bus.publish(Form.Event.Replied, { id: input.id, sessionID: entry.form.sessionID, answer: input.answer })
|
||||
yield* Cache.set(forms, input.id, { ...entry, state: next })
|
||||
yield* Deferred.succeed(entry.deferred, next)
|
||||
}),
|
||||
@@ -227,12 +223,12 @@ export const locationLayer = layer
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [Bus.node] })
|
||||
|
||||
export function validateAnswer(forms: ReadonlyArray<Form.Field>, answer: Answer) {
|
||||
const fields = new Map(forms.map((field) => [field.key, field] as const))
|
||||
function validateAnswer(form: Info, answer: Answer) {
|
||||
const fields = new Map(form.fields.map((field) => [field.key, field] as const))
|
||||
for (const key of Object.keys(answer)) {
|
||||
if (!fields.has(key)) return `Unknown form field: ${key}`
|
||||
}
|
||||
for (const field of forms) {
|
||||
for (const field of form.fields) {
|
||||
const value = answer[field.key]
|
||||
if (field.type === "external") {
|
||||
if (value !== true) return `External form field must be acknowledged: ${field.key}`
|
||||
@@ -268,7 +264,7 @@ function matches(when: Form.When, value: Form.Value | undefined) {
|
||||
// carry a value matching that field's type, and use a declared option when the field's options
|
||||
// are closed. Rejecting these at creation surfaces authoring mistakes to the caller instead of
|
||||
// silently never matching.
|
||||
export function validateFields(fields: ReadonlyArray<Form.Field>) {
|
||||
function validateFields(fields: ReadonlyArray<Form.Field>) {
|
||||
if (fields.length === 0) return "Form must have at least one field"
|
||||
const earlier = new Map<string, InputField>()
|
||||
const keys = new Set<string>()
|
||||
|
||||
@@ -77,7 +77,9 @@ const layer = Layer.effect(
|
||||
|
||||
const file = Effect.fn("Formatter.file")(function* (filepath: string) {
|
||||
yield* load
|
||||
const matching = formatters.filter((formatter) => formatter.extensions.includes(path.extname(filepath)))
|
||||
const matching = formatters.filter((formatter) =>
|
||||
formatter.extensions.includes(path.extname(filepath)),
|
||||
)
|
||||
|
||||
for (const formatter of matching) {
|
||||
const enabled = yield* command(formatter)
|
||||
|
||||
@@ -213,7 +213,9 @@ export function make(input: {
|
||||
const bin = which("uv")
|
||||
if (!bin) return disabled
|
||||
const output = yield* commandOutput([bin, "format", "--help"])
|
||||
return output._tag === "Some" && output.value.exitCode === 0 ? [bin, "format", "--", "$FILE"] : disabled
|
||||
return output._tag === "Some" && output.value.exitCode === 0
|
||||
? [bin, "format", "--", "$FILE"]
|
||||
: disabled
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user