mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-08 02:48:44 +00:00
Compare commits
4
Commits
skill-attachments
...
v2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0df6aed6ca | ||
|
|
aa05fd23b3 | ||
|
|
fd09760903 | ||
|
|
3a4772abb4 |
@@ -1,101 +0,0 @@
|
||||
export * as Account from "./account"
|
||||
|
||||
import { Schema } from "effect"
|
||||
import type { HttpClientError } from "effect/unstable/http"
|
||||
|
||||
export const ID = Schema.String.pipe(Schema.brand("AccountID"))
|
||||
export type ID = Schema.Schema.Type<typeof ID>
|
||||
|
||||
export const OrgID = Schema.String.pipe(Schema.brand("OrgID"))
|
||||
export type OrgID = Schema.Schema.Type<typeof OrgID>
|
||||
|
||||
export const AccessToken = Schema.String.pipe(Schema.brand("AccessToken"))
|
||||
export type AccessToken = Schema.Schema.Type<typeof AccessToken>
|
||||
|
||||
export const RefreshToken = Schema.String.pipe(Schema.brand("RefreshToken"))
|
||||
export type RefreshToken = Schema.Schema.Type<typeof RefreshToken>
|
||||
|
||||
export const DeviceCode = Schema.String.pipe(Schema.brand("DeviceCode"))
|
||||
export type DeviceCode = Schema.Schema.Type<typeof DeviceCode>
|
||||
|
||||
export const UserCode = Schema.String.pipe(Schema.brand("UserCode"))
|
||||
export type UserCode = Schema.Schema.Type<typeof UserCode>
|
||||
|
||||
export class Info extends Schema.Class<Info>("Account")({
|
||||
id: ID,
|
||||
email: Schema.String,
|
||||
url: Schema.String,
|
||||
active_org_id: Schema.NullOr(OrgID),
|
||||
}) {}
|
||||
|
||||
export class Org extends Schema.Class<Org>("Org")({
|
||||
id: OrgID,
|
||||
name: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class AccountRepoError extends Schema.TaggedErrorClass<AccountRepoError>()("AccountRepoError", {
|
||||
message: Schema.String,
|
||||
cause: Schema.optional(Schema.Defect()),
|
||||
}) {}
|
||||
|
||||
export class AccountServiceError extends Schema.TaggedErrorClass<AccountServiceError>()("AccountServiceError", {
|
||||
message: Schema.String,
|
||||
cause: Schema.optional(Schema.Defect()),
|
||||
}) {}
|
||||
|
||||
export class AccountTransportError extends Schema.TaggedErrorClass<AccountTransportError>()("AccountTransportError", {
|
||||
method: Schema.String,
|
||||
url: Schema.String,
|
||||
description: Schema.optional(Schema.String),
|
||||
cause: Schema.optional(Schema.Defect()),
|
||||
}) {
|
||||
static fromHttpClientError(error: HttpClientError.TransportError): AccountTransportError {
|
||||
return new AccountTransportError({
|
||||
method: error.request.method,
|
||||
url: error.request.url,
|
||||
description: error.description,
|
||||
cause: error.cause,
|
||||
})
|
||||
}
|
||||
|
||||
override get message(): string {
|
||||
return [
|
||||
`Could not reach ${this.method} ${this.url}.`,
|
||||
`This failed before the server returned an HTTP response.`,
|
||||
this.description,
|
||||
`Check your network, proxy, or VPN configuration and try again.`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
export type AccountError = AccountRepoError | AccountServiceError | AccountTransportError
|
||||
|
||||
export class Login extends Schema.Class<Login>("Login")({
|
||||
code: DeviceCode,
|
||||
user: UserCode,
|
||||
url: Schema.String,
|
||||
server: Schema.String,
|
||||
expiry: Schema.Duration,
|
||||
interval: Schema.Duration,
|
||||
}) {}
|
||||
|
||||
export class PollSuccess extends Schema.TaggedClass<PollSuccess>()("PollSuccess", {
|
||||
email: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class PollPending extends Schema.TaggedClass<PollPending>()("PollPending", {}) {}
|
||||
|
||||
export class PollSlow extends Schema.TaggedClass<PollSlow>()("PollSlow", {}) {}
|
||||
|
||||
export class PollExpired extends Schema.TaggedClass<PollExpired>()("PollExpired", {}) {}
|
||||
|
||||
export class PollDenied extends Schema.TaggedClass<PollDenied>()("PollDenied", {}) {}
|
||||
|
||||
export class PollError extends Schema.TaggedClass<PollError>()("PollError", {
|
||||
cause: Schema.Defect(),
|
||||
}) {}
|
||||
|
||||
export const PollResult = Schema.Union([PollSuccess, PollPending, PollSlow, PollExpired, PollDenied, PollError])
|
||||
export type PollResult = Schema.Schema.Type<typeof PollResult>
|
||||
@@ -1,24 +1,21 @@
|
||||
import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core"
|
||||
|
||||
import { Account } from "../account"
|
||||
import { Timestamps } from "../database/schema.sql"
|
||||
|
||||
export const AccountTable = sqliteTable("account", {
|
||||
id: text().$type<Account.ID>().primaryKey(),
|
||||
id: text().primaryKey(),
|
||||
email: text().notNull(),
|
||||
url: text().notNull(),
|
||||
access_token: text().$type<Account.AccessToken>().notNull(),
|
||||
refresh_token: text().$type<Account.RefreshToken>().notNull(),
|
||||
access_token: text().notNull(),
|
||||
refresh_token: text().notNull(),
|
||||
token_expiry: integer(),
|
||||
...Timestamps,
|
||||
})
|
||||
|
||||
export const AccountStateTable = sqliteTable("account_state", {
|
||||
id: integer().primaryKey(),
|
||||
active_account_id: text()
|
||||
.$type<Account.ID>()
|
||||
.references(() => AccountTable.id, { onDelete: "set null" }),
|
||||
active_org_id: text().$type<Account.OrgID>(),
|
||||
active_account_id: text().references(() => AccountTable.id, { onDelete: "set null" }),
|
||||
active_org_id: text(),
|
||||
})
|
||||
|
||||
// LEGACY
|
||||
@@ -27,8 +24,8 @@ export const ControlAccountTable = sqliteTable(
|
||||
{
|
||||
email: text().notNull(),
|
||||
url: text().notNull(),
|
||||
access_token: text().$type<Account.AccessToken>().notNull(),
|
||||
refresh_token: text().$type<Account.RefreshToken>().notNull(),
|
||||
access_token: text().notNull(),
|
||||
refresh_token: text().notNull(),
|
||||
token_expiry: integer(),
|
||||
active: integer({ mode: "boolean" })
|
||||
.notNull()
|
||||
|
||||
@@ -1017,6 +1017,51 @@ flowchart LR
|
||||
}
|
||||
})
|
||||
|
||||
test("separates cross-dependent top-level subgraphs", () => {
|
||||
const content = `flowchart TD
|
||||
subgraph plugins["Plugins — one verb: attach"]
|
||||
chip["pr-indicator<br/>attach(prompt.footer, { after: 'directory' })"]
|
||||
theme["fancy-footer<br/>attach(prompt.footer, { replace: 'right' })"]
|
||||
end
|
||||
|
||||
subgraph host["Host anatomy tree — published, stable part IDs"]
|
||||
footer["prompt.footer"]
|
||||
left["left"]
|
||||
right["right<br/>(container)"]
|
||||
dir["directory"]
|
||||
model["model"]
|
||||
tokens["tokens"]
|
||||
footer --> left
|
||||
footer --> right
|
||||
right --> dir
|
||||
right --> model
|
||||
right --> tokens
|
||||
end
|
||||
|
||||
chip -- "insert after" --> dir
|
||||
theme == "takeover" ==> right
|
||||
theme -. "suppresses guests<br/>in subtree" .-> chip`
|
||||
const layout = layoutFlowchartDiagram(content)
|
||||
const plugins = layout.subgraphBounds.get("plugins")!
|
||||
const host = layout.subgraphBounds.get("host")!
|
||||
const output = renderFlowchartDiagram(content)
|
||||
const lines = output.split("\n")
|
||||
|
||||
expect(host.top).toBeGreaterThanOrEqual(plugins.top + plugins.height)
|
||||
expect(lines.filter((line) => line.includes("Plugins — one verb: attach"))).toHaveLength(1)
|
||||
expect(lines.filter((line) => line.includes("Host anatomy tree — published, stable part IDs"))).toHaveLength(1)
|
||||
expect(lines.findIndex((line) => line.includes("Host anatomy tree"))).toBeGreaterThan(
|
||||
lines.findIndex((line) => line.includes("Plugins — one verb")),
|
||||
)
|
||||
for (const route of layout.routes) {
|
||||
for (let index = 1; index < route.points.length; index++) {
|
||||
const from = route.points[index - 1]!
|
||||
const to = route.points[index]!
|
||||
expect(from.x === to.x || from.y === to.y).toBe(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test("moves subgraph labels away from crossing routes", () => {
|
||||
const output = renderFlowchartDiagram(`
|
||||
flowchart TD
|
||||
|
||||
@@ -499,10 +499,6 @@ function edgeDirection(diagram: FlowchartDiagram, edge: FlowchartEdge): Flowchar
|
||||
return diagram.direction
|
||||
}
|
||||
|
||||
function hasLocalSubgraphDirection(diagram: FlowchartDiagram): boolean {
|
||||
return (diagram.subgraphs ?? []).some((subgraph) => subgraph.direction && subgraph.direction !== diagram.direction)
|
||||
}
|
||||
|
||||
function collectSubgraphNodeIds(diagram: FlowchartDiagram, subgraphId: string): Set<string> {
|
||||
const nodeIds = new Set<string>()
|
||||
for (const subgraph of diagram.subgraphs ?? []) {
|
||||
@@ -515,47 +511,113 @@ function collectSubgraphNodeIds(diagram: FlowchartDiagram, subgraphId: string):
|
||||
return nodeIds
|
||||
}
|
||||
|
||||
function separateLocalSubgraphItems(
|
||||
function separateTopLevelItems(
|
||||
diagram: FlowchartDiagram,
|
||||
nodeBounds: Map<string, FlowchartNodeBounds>,
|
||||
subgraphBounds: ReadonlyMap<string, FlowchartSubgraphBounds>,
|
||||
gap: number,
|
||||
): void {
|
||||
if (!hasLocalSubgraphDirection(diagram)) return
|
||||
|
||||
const hasLocalDirection = (diagram.subgraphs ?? []).some(
|
||||
(subgraph) => subgraph.direction && subgraph.direction !== diagram.direction,
|
||||
)
|
||||
const coveredNodeIds = new Set<string>()
|
||||
const items: { bounds: FlowchartBounds; nodeIds: Set<string> }[] = []
|
||||
const items: { id: string; bounds: FlowchartBounds; nodeIds: Set<string>; rank: number }[] = []
|
||||
const itemByEndpoint = new Map<string, string>()
|
||||
for (const subgraph of diagram.subgraphs ?? []) {
|
||||
if (subgraph.parentId) continue
|
||||
const bounds = subgraphBounds.get(subgraph.id)
|
||||
const nodeIds = collectSubgraphNodeIds(diagram, subgraph.id)
|
||||
if (!bounds || nodeIds.size === 0) continue
|
||||
items.push({ bounds, nodeIds })
|
||||
for (const nodeId of nodeIds) coveredNodeIds.add(nodeId)
|
||||
items.push({ id: subgraph.id, bounds, nodeIds, rank: 0 })
|
||||
itemByEndpoint.set(subgraph.id, subgraph.id)
|
||||
for (const nodeId of nodeIds) {
|
||||
coveredNodeIds.add(nodeId)
|
||||
itemByEndpoint.set(nodeId, subgraph.id)
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of diagram.nodes) {
|
||||
if (coveredNodeIds.has(node.id)) continue
|
||||
const bounds = nodeBounds.get(node.id)
|
||||
if (bounds) items.push({ bounds, nodeIds: new Set([node.id]) })
|
||||
if (!bounds) continue
|
||||
items.push({ id: node.id, bounds, nodeIds: new Set([node.id]), rank: 0 })
|
||||
itemByEndpoint.set(node.id, node.id)
|
||||
}
|
||||
if (items.length < 2) return
|
||||
|
||||
const horizontal = isHorizontalDirection(diagram.direction)
|
||||
items.sort((a, b) => (horizontal ? a.bounds.left - b.bounds.left : a.bounds.top - b.bounds.top))
|
||||
if (hasLocalDirection) {
|
||||
items.sort((a, b) => (horizontal ? a.bounds.left - b.bounds.left : a.bounds.top - b.bounds.top))
|
||||
let cursor: number | undefined
|
||||
for (const item of items) {
|
||||
const start = horizontal ? item.bounds.left : item.bounds.top
|
||||
const size = horizontal ? item.bounds.width : item.bounds.height
|
||||
if (cursor === undefined) {
|
||||
cursor = start + size + gap
|
||||
continue
|
||||
}
|
||||
const shift = cursor - start
|
||||
for (const nodeId of item.nodeIds) {
|
||||
const bounds = nodeBounds.get(nodeId)
|
||||
if (bounds) translateBounds(bounds, horizontal ? shift : 0, horizontal ? 0 : shift)
|
||||
}
|
||||
cursor = start + shift + size + gap
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const topLevelIds = new Set(
|
||||
(diagram.subgraphs ?? []).filter((subgraph) => !subgraph.parentId).map((subgraph) => subgraph.id),
|
||||
)
|
||||
const rankedItems = items.filter((item) => topLevelIds.has(item.id))
|
||||
if (rankedItems.length < 2) return
|
||||
|
||||
const itemById = new Map(rankedItems.map((item) => [item.id, item]))
|
||||
const outgoing = new Map(rankedItems.map((item) => [item.id, new Set<string>()]))
|
||||
const incoming = new Map(rankedItems.map((item) => [item.id, 0]))
|
||||
for (const edge of diagram.edges) {
|
||||
const from = itemByEndpoint.get(edge.from)
|
||||
const to = itemByEndpoint.get(edge.to)
|
||||
if (!from || !to || from === to || !itemById.has(from) || !itemById.has(to) || outgoing.get(from)!.has(to)) continue
|
||||
outgoing.get(from)!.add(to)
|
||||
incoming.set(to, incoming.get(to)! + 1)
|
||||
}
|
||||
|
||||
const queue = rankedItems.filter((item) => incoming.get(item.id) === 0)
|
||||
for (let index = 0; index < queue.length; index++) {
|
||||
const item = queue[index]!
|
||||
for (const to of outgoing.get(item.id)!) {
|
||||
const downstream = itemById.get(to)!
|
||||
downstream.rank = Math.max(downstream.rank, item.rank + 1)
|
||||
incoming.set(to, incoming.get(to)! - 1)
|
||||
if (incoming.get(to) === 0) queue.push(downstream)
|
||||
}
|
||||
}
|
||||
|
||||
const reversed = diagram.direction === "RL" || diagram.direction === "BT"
|
||||
const primaryStart = (item: (typeof items)[number]): number => {
|
||||
const start = horizontal ? item.bounds.left : item.bounds.top
|
||||
const size = horizontal ? item.bounds.width : item.bounds.height
|
||||
return reversed ? -(start + size) : start
|
||||
}
|
||||
rankedItems.sort((a, b) => a.rank - b.rank || primaryStart(a) - primaryStart(b))
|
||||
|
||||
let cursor: number | undefined
|
||||
for (const item of items) {
|
||||
const start = horizontal ? item.bounds.left : item.bounds.top
|
||||
for (const item of rankedItems) {
|
||||
const start = primaryStart(item)
|
||||
const size = horizontal ? item.bounds.width : item.bounds.height
|
||||
if (cursor === undefined) {
|
||||
cursor = start + size + gap
|
||||
continue
|
||||
}
|
||||
const shift = cursor - start
|
||||
if (shift !== 0) {
|
||||
const shift = Math.max(0, cursor - start)
|
||||
if (shift > 0) {
|
||||
for (const nodeId of item.nodeIds) {
|
||||
const bounds = nodeBounds.get(nodeId)
|
||||
if (bounds) translateBounds(bounds, horizontal ? shift : 0, horizontal ? 0 : shift)
|
||||
if (bounds) {
|
||||
const offset = reversed ? -shift : shift
|
||||
translateBounds(bounds, horizontal ? offset : 0, horizontal ? 0 : offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
cursor = start + shift + size + gap
|
||||
@@ -616,7 +678,7 @@ function layoutFlowchartWithDirection(
|
||||
|
||||
let routes = routeFlowchartEdges(diagram, bounds, (edge) => edgeDirection(diagram, edge))
|
||||
let subgraphBounds = layoutSubgraphs(diagram, bounds, routes)
|
||||
separateLocalSubgraphItems(diagram, bounds, subgraphBounds, Math.max(1, Math.floor(requestedMinRankGap / 2)))
|
||||
separateTopLevelItems(diagram, bounds, subgraphBounds, Math.max(1, Math.floor(requestedMinRankGap / 2)))
|
||||
routes = routeFlowchartEdges(diagram, bounds, (edge) => edgeDirection(diagram, edge))
|
||||
subgraphBounds = layoutSubgraphs(diagram, bounds, routes)
|
||||
routes = routeFlowchartEdges(diagram, bounds, (edge) => edgeDirection(diagram, edge), subgraphBounds)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { RGBA } from "@opentui/core"
|
||||
import { createOpenCodeDiagramPalette } from "./palette.js"
|
||||
|
||||
type Rgb = readonly [number, number, number]
|
||||
|
||||
const rgb = (value: Rgb) => RGBA.fromInts(...value)
|
||||
|
||||
describe("OpenCode diagram palette", () => {
|
||||
test.each(
|
||||
[
|
||||
{
|
||||
name: "dark theme",
|
||||
text: [230, 232, 240],
|
||||
subdued: [114, 120, 138],
|
||||
secondary: [172, 176, 189],
|
||||
muted: [149, 154, 169],
|
||||
},
|
||||
{
|
||||
name: "light theme",
|
||||
text: [32, 35, 43],
|
||||
subdued: [119, 125, 138],
|
||||
secondary: [76, 80, 91],
|
||||
muted: [93, 98, 110],
|
||||
},
|
||||
] satisfies ReadonlyArray<{
|
||||
name: string
|
||||
text: Rgb
|
||||
subdued: Rgb
|
||||
secondary: Rgb
|
||||
muted: Rgb
|
||||
}>,
|
||||
)("derives a controlled neutral ladder for a $name", ({ text, subdued, secondary, muted }) => {
|
||||
const primary = rgb(text)
|
||||
const info = RGBA.fromInts(40, 120, 220)
|
||||
const background = RGBA.fromInts(10, 20, 30)
|
||||
const palette = createOpenCodeDiagramPalette({
|
||||
text: primary,
|
||||
subdued: rgb(subdued),
|
||||
info,
|
||||
background,
|
||||
})
|
||||
|
||||
expect(palette.text).toBe(primary)
|
||||
expect(palette.primary).toBe(primary)
|
||||
expect(palette.secondary.equals(rgb(secondary))).toBe(true)
|
||||
expect(palette.muted.equals(rgb(muted))).toBe(true)
|
||||
expect(palette.warning).toBe(info)
|
||||
expect(palette.background).toBe(background)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { RGBA } from "@opentui/core"
|
||||
import { blendColor } from "./core/color/style.js"
|
||||
|
||||
export interface OpenCodeDiagramPaletteInput {
|
||||
readonly text: RGBA
|
||||
readonly subdued: RGBA
|
||||
readonly info: RGBA
|
||||
readonly background: RGBA
|
||||
}
|
||||
|
||||
export function createOpenCodeDiagramPalette(input: OpenCodeDiagramPaletteInput) {
|
||||
return {
|
||||
text: input.text,
|
||||
primary: input.text,
|
||||
secondary: blendColor(input.text, input.subdued, 0.5),
|
||||
muted: blendColor(input.text, input.subdued, 0.7),
|
||||
warning: input.info,
|
||||
background: input.background,
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Plugin } from "@opencode-ai/plugin/tui"
|
||||
import { createMermaidCodeBlockRenderer } from "./markdown.js"
|
||||
import { createOpenCodeDiagramPalette } from "./palette.js"
|
||||
|
||||
export default Plugin.define({
|
||||
id: "opencode.merman",
|
||||
@@ -7,14 +8,12 @@ export default Plugin.define({
|
||||
context.markdown.registerCodeBlockRenderer(
|
||||
"mermaid",
|
||||
createMermaidCodeBlockRenderer(context.renderer, () => ({
|
||||
colors: {
|
||||
text: context.theme.markdown.text,
|
||||
primary: context.theme.text.default,
|
||||
secondary: context.theme.text.subdued,
|
||||
muted: context.theme.border.default,
|
||||
warning: context.theme.text.feedback.info.default,
|
||||
colors: createOpenCodeDiagramPalette({
|
||||
text: context.theme.text.default,
|
||||
subdued: context.theme.text.subdued,
|
||||
info: context.theme.text.feedback.info.default,
|
||||
background: context.theme.background.default,
|
||||
},
|
||||
}),
|
||||
})),
|
||||
)
|
||||
},
|
||||
|
||||
@@ -51,6 +51,17 @@ export function DialogOpen() {
|
||||
.catch(() => [] as SessionInfo[]),
|
||||
{ initialValue: [] },
|
||||
)
|
||||
const [matched] = createResource(
|
||||
() => {
|
||||
const value = filter().trim()
|
||||
return /^ses_[0-9A-Za-z]{26}$/.test(value) ? value : undefined
|
||||
},
|
||||
(sessionID) =>
|
||||
client.api.session
|
||||
.get({ sessionID })
|
||||
.then((session) => (session.id === sessionID ? session : undefined))
|
||||
.catch(() => undefined),
|
||||
)
|
||||
|
||||
const openTabs = createMemo(
|
||||
() => new Set(sessionTabs.enabled() ? sessionTabs.tabs().map((tab) => tab.sessionID) : []),
|
||||
@@ -60,7 +71,8 @@ export function DialogOpen() {
|
||||
)
|
||||
const sessions = createMemo(() => {
|
||||
const seen = new Set<string>()
|
||||
return [...data.session.list(), ...fetched()]
|
||||
const match = matched()
|
||||
return [...data.session.list(), ...fetched(), ...(match ? [match] : [])]
|
||||
.filter((session) => {
|
||||
if (session.parentID || seen.has(session.id)) return false
|
||||
seen.add(session.id)
|
||||
@@ -87,6 +99,7 @@ export function DialogOpen() {
|
||||
data.session.family(session.id).some((id) => data.session.status(id) === "running")
|
||||
return {
|
||||
title: withTimestampedFallback(session),
|
||||
searchText: session.id,
|
||||
value: { type: "session", sessionID: session.id } as OpenTarget,
|
||||
category: "Sessions",
|
||||
footer: `${name ? `${Locale.truncate(name, 20)} · ` : ""}${timeAgo(session.time.updated)}`,
|
||||
|
||||
@@ -54,6 +54,40 @@ test("selecting an unhydrated session preserves its location", async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("finds and opens an exact session ID outside the recent list", async () => {
|
||||
const sessionID = "ses_04a7a3d82ffeIphUJgd3SnEqiv"
|
||||
const remote = { directory: "/tmp/opencode/archive", workspaceID: "ws_archive" }
|
||||
const fixture = await renderOpen((url) => {
|
||||
if (url.pathname === "/api/session") return json({ data: [], cursor: {} })
|
||||
if (url.pathname !== `/api/session/${sessionID}`) return undefined
|
||||
return json({
|
||||
data: {
|
||||
id: sessionID,
|
||||
projectID: "proj_archive",
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
time: { created: 1, updated: 2 },
|
||||
title: "TUI plugin slot API v2",
|
||||
location: remote,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
try {
|
||||
await fixture.app.waitForFrame((frame) => frame.includes("Search sessions and projects"))
|
||||
await fixture.app.mockInput.typeText(sessionID)
|
||||
await fixture.app.waitForFrame((frame) => frame.includes("TUI plugin slot API v2"))
|
||||
|
||||
fixture.app.mockInput.pressEnter()
|
||||
await fixture.app.waitFor(() => fixture.route.data.type === "session")
|
||||
|
||||
expect(fixture.route.data).toEqual({ type: "session", sessionID })
|
||||
expect(fixture.location.ref).toEqual(remote)
|
||||
} finally {
|
||||
fixture.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
test("shows the current project and opens its root", async () => {
|
||||
const root = "/tmp/opencode/project"
|
||||
const subfolder = `${root}/packages/tui`
|
||||
|
||||
Reference in New Issue
Block a user