mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-12 20:50:01 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c2e2e0c62 |
@@ -1,6 +1,6 @@
|
||||
export * as ShellParse from "./parse.js"
|
||||
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Exit } from "effect"
|
||||
import { fileURLToPath } from "url"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
@@ -153,8 +153,15 @@ const ARITY: Record<string, number> = {
|
||||
}
|
||||
|
||||
export const scan = Effect.fn("ShellParse.scan")(function* (command: string, shell: string, cwd: string) {
|
||||
const parsers = yield* Effect.promise(load)
|
||||
const powershell = ShellSelect.ps(shell)
|
||||
const loaded = yield* Effect.promise(load).pipe(Effect.exit)
|
||||
// Workerd has no filesystem-backed tree-sitter assets. Preserve execution
|
||||
// with one conservative permission resource instead of disabling shell.
|
||||
if (Exit.isFailure(loaded)) {
|
||||
const tokens = command.trim().split(/\s+/)
|
||||
return { commands: [{ resource: command, save: `${prefix(tokens).join(" ")} *` }], directories: [] }
|
||||
}
|
||||
const parsers = loaded.value
|
||||
const tree = (powershell ? parsers.ps : parsers.bash).parse(command)
|
||||
if (!tree) return yield* Effect.fail(new Error("Failed to parse shell command"))
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import wrangler from "../wrangler.jsonc"
|
||||
import { assetPath } from "../worker"
|
||||
|
||||
describe("catalog worker", () => {
|
||||
@@ -13,8 +12,4 @@ describe("catalog worker", () => {
|
||||
expect(assetPath("/lab/catalog/catalog.json")).toBe("/catalog.json")
|
||||
expect(assetPath("/lab/catalog/captures/opencode/home.frame.json")).toBe("/captures/opencode/home.frame.json")
|
||||
})
|
||||
|
||||
test("leaves HTML routing to the worker", () => {
|
||||
expect(wrangler.assets.html_handling).toBe("none")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"assets": {
|
||||
"directory": "./dist",
|
||||
"binding": "ASSETS",
|
||||
"html_handling": "none",
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
|
||||
@@ -2,12 +2,10 @@ import type { MermaidDiagramKind } from "./diagnostics.js"
|
||||
import { isMermaidFlowchartDiagram } from "./flowchart/parser.js"
|
||||
import { isMermaidSequenceDiagram } from "./sequence/parser.js"
|
||||
import { isMermaidStateDiagram } from "./state/parser.js"
|
||||
import { isMermaidTimelineDiagram } from "./timeline/parser.js"
|
||||
|
||||
export function detectMermaidDiagram(content: string): MermaidDiagramKind | undefined {
|
||||
if (isMermaidFlowchartDiagram(content)) return "flowchart"
|
||||
if (isMermaidSequenceDiagram(content)) return "sequence"
|
||||
if (isMermaidStateDiagram(content)) return "state"
|
||||
if (isMermaidTimelineDiagram(content)) return "timeline"
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type MermaidDiagramKind = "flowchart" | "sequence" | "state" | "timeline"
|
||||
export type MermaidDiagramKind = "flowchart" | "sequence" | "state"
|
||||
|
||||
/** An otherwise valid diagram contains syntax that this renderer does not support. */
|
||||
export class MermaidSyntaxError extends Error {
|
||||
|
||||
@@ -25,10 +25,6 @@ import { drawStateDiagramGrid } from "./state/drawing.js"
|
||||
import { parseMermaidStateDiagram } from "./state/parser.js"
|
||||
import { renderStateGridStyledText } from "./state/render-grid.js"
|
||||
import { resolveStateStyleColors } from "./state/style.js"
|
||||
import { drawTimelineDiagramGrid } from "./timeline/drawing.js"
|
||||
import { parseMermaidTimelineDiagram } from "./timeline/parser.js"
|
||||
import { renderTimelineGridStyledText } from "./timeline/render-grid.js"
|
||||
import { resolveTimelineStyleColors } from "./timeline/style.js"
|
||||
|
||||
type DiagramKind = NonNullable<ReturnType<typeof detectMermaidDiagram>>
|
||||
|
||||
@@ -184,25 +180,6 @@ function prepareDiagram(
|
||||
height: size.height,
|
||||
}
|
||||
}
|
||||
case "timeline": {
|
||||
const grid = drawTimelineDiagramGrid(parseMermaidTimelineDiagram(source))
|
||||
const size = grid.getTextSize({ trimBottom: true })
|
||||
return {
|
||||
kind,
|
||||
source,
|
||||
text: renderTimelineGridStyledText(
|
||||
grid,
|
||||
resolveTimelineStyleColors({
|
||||
title: color(colors.text),
|
||||
section: color(colors.secondary),
|
||||
period: color(colors.warning),
|
||||
spine: color(colors.muted),
|
||||
event: color(colors.primary),
|
||||
}),
|
||||
),
|
||||
height: size.height,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { MermaidSyntaxError } from "../diagnostics.js"
|
||||
import { parseMermaidFlowchartDiagram } from "../flowchart/parser.js"
|
||||
import { parseMermaidSequenceDiagram } from "../sequence/parser.js"
|
||||
import { parseMermaidStateDiagram } from "../state/parser.js"
|
||||
import { renderTimelineDiagram } from "../timeline/diagram.js"
|
||||
import { renderSequenceDiagram } from "../sequence/diagram.js"
|
||||
|
||||
describe("parser diagnostics", () => {
|
||||
@@ -105,12 +104,6 @@ describe("parser diagnostics", () => {
|
||||
).toThrow('Unexpected "end" without an open block in sequence diagram at line 2: "end"')
|
||||
})
|
||||
|
||||
test("reports malformed timeline continuations with timeline diagnostics", () => {
|
||||
expect(() => renderTimelineDiagram("timeline\n : orphan event")).toThrow(
|
||||
'Timeline continuation requires a preceding period in timeline diagram at line 2: ": orphan event"',
|
||||
)
|
||||
})
|
||||
|
||||
test("does not attach else through an unclosed nested sequence block", () => {
|
||||
expect(() =>
|
||||
parseMermaidSequenceDiagram(`sequenceDiagram
|
||||
|
||||
@@ -333,31 +333,3 @@ stateDiagram-v2
|
||||
expect(frame).toContain("Idle")
|
||||
expect(frame).not.toContain("stateDiagram-v2")
|
||||
})
|
||||
|
||||
test("renders a Mermaid timeline fence inside MarkdownRenderable", async () => {
|
||||
const testRenderer = await createTestRenderer({ width: 80, height: 18 })
|
||||
renderer = testRenderer.renderer
|
||||
const { renderOnce, captureCharFrame } = testRenderer
|
||||
const markdown = new MarkdownRenderable(renderer, {
|
||||
id: "markdown-timeline",
|
||||
content: `\`\`\`mermaid
|
||||
timeline
|
||||
title Product history
|
||||
section Foundation
|
||||
2024 : Prototype
|
||||
: First release
|
||||
\`\`\``,
|
||||
syntaxStyle,
|
||||
treeSitterClient,
|
||||
renderNode: createMermaidMarkdownRenderer(renderer),
|
||||
})
|
||||
|
||||
renderer.root.add(markdown)
|
||||
await renderMarkdown(markdown, renderOnce)
|
||||
|
||||
const frame = captureCharFrame()
|
||||
expect(frame).toContain("Product history")
|
||||
expect(frame).toContain("Foundation")
|
||||
expect(frame).toContain("First release")
|
||||
expect(frame).not.toContain("timeline")
|
||||
})
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { renderTimelineDiagram } from "./diagram.js"
|
||||
import { drawTimelineDiagramGrid } from "./drawing.js"
|
||||
import { parseMermaidTimelineDiagram } from "./parser.js"
|
||||
import { renderTimelineGridText } from "./render-grid.js"
|
||||
import { resolveTimelineStyleColors } from "./style.js"
|
||||
|
||||
describe("TimelineDiagram", () => {
|
||||
test("detects and parses titles, sections, periods, inline events, and continuations", () => {
|
||||
const diagram = parseMermaidTimelineDiagram(`
|
||||
%% product history
|
||||
timeline LR
|
||||
title Product &<br/>Platform
|
||||
|
||||
section Foundation
|
||||
2024 : Prototype : First release
|
||||
: Public beta
|
||||
section Growth
|
||||
2025 : "Scale: ≥ 10k"
|
||||
`)
|
||||
|
||||
expect(diagram.direction).toBe("LR")
|
||||
expect(diagram.title).toBe("Product &<br/>Platform")
|
||||
expect(diagram.sections).toEqual([{ label: "Foundation" }, { label: "Growth" }])
|
||||
expect(diagram.periods).toEqual([
|
||||
{ period: "2024", events: ["Prototype", "First release", "Public beta"] },
|
||||
{ period: "2025", events: ["Scale: ≥ 10k"] },
|
||||
])
|
||||
expect(diagram.entries.map((entry) => entry.type)).toEqual(["section", "period", "section", "period"])
|
||||
})
|
||||
|
||||
test("renders a vertical spine with title, section, periods, events, entities, and line breaks", () => {
|
||||
const output = renderTimelineDiagram(`timeline
|
||||
title Product &<br/>Platform
|
||||
section Foundation<br/>phase
|
||||
2024 : Prototype<br/>ready : First release
|
||||
: Scale ≥ 10k`)
|
||||
|
||||
expect(output).toBe(
|
||||
[
|
||||
" Product &",
|
||||
" Platform",
|
||||
"",
|
||||
"Foundation ───┐",
|
||||
" phase │",
|
||||
" │",
|
||||
" 2024 ───● Prototype",
|
||||
" │ ready",
|
||||
" │ First release",
|
||||
" │ Scale ≥ 10k",
|
||||
" │",
|
||||
].join("\n"),
|
||||
)
|
||||
})
|
||||
|
||||
test.each(["timeline", "timeline TD", "timeline LR"])("uses the vertical terminal layout for %s", (header) => {
|
||||
const output = renderTimelineDiagram(`${header}\n 2024 : One\n 2025 : Two`)
|
||||
const lines = output.split("\n")
|
||||
|
||||
expect(lines.findIndex((line) => line.includes("2024"))).toBeLessThan(
|
||||
lines.findIndex((line) => line.includes("2025")),
|
||||
)
|
||||
expect(output).toContain("│")
|
||||
expect(output).toContain("●")
|
||||
})
|
||||
|
||||
test("preserves Mermaid direction semantics while using vertical terminal layout", () => {
|
||||
expect(parseMermaidTimelineDiagram("timeline\n 2024 : One").direction).toBe("LR")
|
||||
expect(parseMermaidTimelineDiagram("timeline TD\n 2024 : One").direction).toBe("TD")
|
||||
})
|
||||
|
||||
test("keeps ordinary colons in event text", () => {
|
||||
const diagram = parseMermaidTimelineDiagram(`timeline
|
||||
2024 : https://example.com : event:detail : next event`)
|
||||
|
||||
expect(diagram.periods[0]?.events).toEqual(["https://example.com", "event:detail", "next event"])
|
||||
})
|
||||
|
||||
test("does not treat apostrophes in event prose as quotes", () => {
|
||||
const diagram = parseMermaidTimelineDiagram("timeline\n 2024 : Kit's launch : Public beta")
|
||||
|
||||
expect(diagram.periods[0]?.events).toEqual(["Kit's launch", "Public beta"])
|
||||
})
|
||||
|
||||
test("supports standalone periods followed by continuation events", () => {
|
||||
const diagram = parseMermaidTimelineDiagram(`timeline
|
||||
2024
|
||||
: First release
|
||||
: Public beta`)
|
||||
|
||||
expect(diagram.periods).toEqual([{ period: "2024", events: ["First release", "Public beta"] }])
|
||||
})
|
||||
|
||||
test("ignores timeline comments and accessibility directives", () => {
|
||||
const diagram = parseMermaidTimelineDiagram(`timeline
|
||||
# product history
|
||||
accTitle: Product timeline
|
||||
accDescr Product release history
|
||||
2024 : Prototype %% internal note`)
|
||||
|
||||
expect(diagram.periods).toEqual([{ period: "2024", events: ["Prototype"] }])
|
||||
})
|
||||
|
||||
test("ignores multiline accessibility descriptions", () => {
|
||||
const diagram = parseMermaidTimelineDiagram(`timeline
|
||||
accDescr {
|
||||
Product milestones by year.
|
||||
Includes launch and growth.
|
||||
}
|
||||
2024 : Ship`)
|
||||
|
||||
expect(diagram.periods).toEqual([{ period: "2024", events: ["Ship"] }])
|
||||
})
|
||||
|
||||
test("rejects a continuation without a period with source diagnostics", () => {
|
||||
expect(() => parseMermaidTimelineDiagram("timeline\n : orphan event")).toThrow(
|
||||
'Timeline continuation requires a preceding period in timeline diagram at line 2: ": orphan event"',
|
||||
)
|
||||
})
|
||||
|
||||
test("rejects unsupported and empty syntax", () => {
|
||||
expect(() => parseMermaidTimelineDiagram("timeline\n section")).toThrow("Timeline section cannot be empty")
|
||||
expect(() => parseMermaidTimelineDiagram("timeline\n 2024 :")).toThrow("Timeline event cannot be empty")
|
||||
expect(() => parseMermaidTimelineDiagram("timeline\n : unsupported")).toThrow("requires a preceding period")
|
||||
})
|
||||
|
||||
test("draws semantic styles for every timeline role", () => {
|
||||
const grid = drawTimelineDiagramGrid(
|
||||
parseMermaidTimelineDiagram("timeline\n title Roadmap\n section Now\n 2026 : Ship"),
|
||||
)
|
||||
const styles = new Set(grid.rows.flatMap((row) => row.map((cell) => cell.style).filter(Boolean)))
|
||||
|
||||
expect(styles).toEqual(
|
||||
new Set([
|
||||
"title",
|
||||
"section",
|
||||
"sectionFade1",
|
||||
"sectionFade2",
|
||||
"sectionFade3",
|
||||
"spine",
|
||||
"period",
|
||||
"periodFade1",
|
||||
"periodFade2",
|
||||
"periodFade3",
|
||||
"event",
|
||||
]),
|
||||
)
|
||||
expect(Object.keys(resolveTimelineStyleColors()).sort()).toEqual([
|
||||
"event",
|
||||
"period",
|
||||
"periodFade1",
|
||||
"periodFade2",
|
||||
"periodFade3",
|
||||
"section",
|
||||
"sectionFade1",
|
||||
"sectionFade2",
|
||||
"sectionFade3",
|
||||
"spine",
|
||||
"title",
|
||||
])
|
||||
expect(renderTimelineGridText(grid)).toBe(
|
||||
renderTimelineDiagram("timeline\n title Roadmap\n section Now\n 2026 : Ship"),
|
||||
)
|
||||
})
|
||||
|
||||
test("uses section starts and joins with ordered color ramps", () => {
|
||||
const grid = drawTimelineDiagramGrid(
|
||||
parseMermaidTimelineDiagram("timeline\n section Morning\n 09:00 : Start\n section Midday\n 12:00 : Continue"),
|
||||
)
|
||||
const text = renderTimelineGridText(grid)
|
||||
|
||||
expect(text).toContain("Morning ───┐")
|
||||
expect(text).toContain("Midday ───┤")
|
||||
expect(grid.rows[0]?.map((cell) => cell.style).filter(Boolean)).toEqual([
|
||||
"section",
|
||||
"section",
|
||||
"section",
|
||||
"section",
|
||||
"section",
|
||||
"section",
|
||||
"section",
|
||||
"sectionFade1",
|
||||
"sectionFade2",
|
||||
"sectionFade3",
|
||||
"spine",
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -1,8 +0,0 @@
|
||||
import { drawTimelineDiagramGrid } from "./drawing.js"
|
||||
import { parseMermaidTimelineDiagram } from "./parser.js"
|
||||
import { renderTimelineGridText } from "./render-grid.js"
|
||||
import type { TimelineDiagramRenderOptions } from "./types.js"
|
||||
|
||||
export function renderTimelineDiagram(content: string, options: TimelineDiagramRenderOptions = {}): string {
|
||||
return renderTimelineGridText(drawTimelineDiagramGrid(parseMermaidTimelineDiagram(content), options))
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import { DiagramCanvas } from "../core/canvas.js"
|
||||
import { splitDiagramLines } from "../core/text-lines.js"
|
||||
import { diagramTextWidth } from "../core/text.js"
|
||||
import type { TimelineGrid } from "./render-grid.js"
|
||||
import { TIMELINE_PERIOD_FADE_STYLES, TIMELINE_SECTION_FADE_STYLES } from "./style.js"
|
||||
import type { TimelineCellStyle, TimelineDiagram, TimelineDiagramRenderOptions, TimelinePeriod } from "./types.js"
|
||||
|
||||
interface PeriodLayout {
|
||||
period: TimelinePeriod
|
||||
periodLines: string[]
|
||||
eventLines: string[][]
|
||||
height: number
|
||||
}
|
||||
|
||||
const JOIN_WIDTH = TIMELINE_SECTION_FADE_STYLES.length
|
||||
const SPINE_OFFSET = JOIN_WIDTH + 1
|
||||
const EVENT_OFFSET = 3
|
||||
|
||||
export function drawTimelineDiagramGrid(
|
||||
diagram: TimelineDiagram,
|
||||
_options: TimelineDiagramRenderOptions = {},
|
||||
): TimelineGrid {
|
||||
const periodLayouts = new Map<TimelinePeriod, PeriodLayout>()
|
||||
let leftWidth = 0
|
||||
let rightWidth = 0
|
||||
let bodyHeight = 0
|
||||
|
||||
for (const entry of diagram.entries) {
|
||||
if (entry.type === "section") {
|
||||
const lines = splitDiagramLines(entry.section.label)
|
||||
bodyHeight += lines.length + 1
|
||||
for (const line of lines) leftWidth = Math.max(leftWidth, diagramTextWidth(line))
|
||||
continue
|
||||
}
|
||||
const periodLines = splitDiagramLines(entry.period.period)
|
||||
const eventLines = entry.period.events.map(splitDiagramLines)
|
||||
const eventHeight = eventLines.reduce((height, lines) => height + lines.length, 0)
|
||||
const height = Math.max(periodLines.length, eventHeight)
|
||||
periodLayouts.set(entry.period, { period: entry.period, periodLines, eventLines, height })
|
||||
for (const line of periodLines) leftWidth = Math.max(leftWidth, diagramTextWidth(line))
|
||||
for (const lines of eventLines) {
|
||||
for (const line of lines) rightWidth = Math.max(rightWidth, diagramTextWidth(line))
|
||||
}
|
||||
bodyHeight += height + 1
|
||||
}
|
||||
|
||||
const titleLines = diagram.title ? splitDiagramLines(diagram.title) : []
|
||||
const bodyWidth = diagram.entries.length === 0 ? 0 : leftWidth + SPINE_OFFSET + EVENT_OFFSET + rightWidth + 1
|
||||
let titleWidth = 0
|
||||
for (const line of titleLines) titleWidth = Math.max(titleWidth, diagramTextWidth(line))
|
||||
const width = Math.max(bodyWidth, titleWidth)
|
||||
const titleHeight = titleLines.length === 0 ? 0 : titleLines.length + (diagram.entries.length === 0 ? 0 : 1)
|
||||
if (width === 0) return new DiagramCanvas(0, 0)
|
||||
|
||||
const grid: TimelineGrid = new DiagramCanvas(width, titleHeight + bodyHeight)
|
||||
titleLines.forEach((line, index) =>
|
||||
setText(grid, Math.floor((width - diagramTextWidth(line)) / 2), index, line, "title"),
|
||||
)
|
||||
if (diagram.entries.length === 0) return grid
|
||||
|
||||
const spineX = leftWidth + SPINE_OFFSET
|
||||
let y = titleHeight
|
||||
let railStarted = false
|
||||
for (const entry of diagram.entries) {
|
||||
if (entry.type === "section") {
|
||||
const lines = splitDiagramLines(entry.section.label)
|
||||
lines.forEach((line, index) => {
|
||||
setText(grid, leftWidth - diagramTextWidth(line), y + index, line, "section")
|
||||
if (index > 0) setCell(grid, spineX, y + index, "│", "spine")
|
||||
})
|
||||
drawJoin(grid, leftWidth, y, TIMELINE_SECTION_FADE_STYLES)
|
||||
setCell(grid, spineX, y, railStarted ? "┤" : "┐", "spine")
|
||||
setCell(grid, spineX, y + lines.length, "│", "spine")
|
||||
railStarted = true
|
||||
y += lines.length + 1
|
||||
continue
|
||||
}
|
||||
|
||||
const layout = periodLayouts.get(entry.period)!
|
||||
for (let row = 0; row < layout.height + 1; row++) setCell(grid, spineX, y + row, "│", "spine")
|
||||
railStarted = true
|
||||
setCell(grid, spineX, y, "●", "spine")
|
||||
layout.periodLines.forEach((line, index) => {
|
||||
const lineWidth = diagramTextWidth(line)
|
||||
setText(grid, leftWidth - lineWidth, y + index, line, "period")
|
||||
})
|
||||
drawJoin(grid, leftWidth, y, TIMELINE_PERIOD_FADE_STYLES)
|
||||
|
||||
let eventY = y
|
||||
for (const lines of layout.eventLines) {
|
||||
lines.forEach((line, index) => setText(grid, spineX + EVENT_OFFSET, eventY + index, line, "event"))
|
||||
eventY += lines.length
|
||||
}
|
||||
y += layout.height + 1
|
||||
}
|
||||
return grid
|
||||
}
|
||||
|
||||
function drawJoin(grid: TimelineGrid, x: number, y: number, styles: readonly TimelineCellStyle[]): void {
|
||||
styles.forEach((style, index) => setCell(grid, x + index + 1, y, "─", style))
|
||||
}
|
||||
|
||||
function setCell(grid: TimelineGrid, x: number, y: number, char: string, style: TimelineCellStyle): void {
|
||||
grid.setCell(x, y, char, style)
|
||||
}
|
||||
|
||||
function setText(grid: TimelineGrid, x: number, y: number, text: string, style: TimelineCellStyle): void {
|
||||
grid.setText(x, y, text, style)
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import { firstMeaningfulMermaidLine, meaningfulNumberedMermaidLines, stripMermaidQuotes } from "../core/mermaid.js"
|
||||
import { MermaidSyntaxError } from "../diagnostics.js"
|
||||
import type { TimelineDiagram, TimelineDirection, TimelineEntry, TimelinePeriod, TimelineSection } from "./types.js"
|
||||
|
||||
const HEADER_RE = /^timeline(?:\s+(TD|LR))?$/i
|
||||
const TITLE_RE = /^title(?:\s+(.+))?$/i
|
||||
const SECTION_RE = /^section(?:\s+(.+))?$/i
|
||||
const ACCESSIBILITY_RE = /^acc(?:Title|Descr)(?::|\s|$)/i
|
||||
|
||||
export function isMermaidTimelineDiagram(content: string): boolean {
|
||||
return HEADER_RE.test(firstMeaningfulMermaidLine(content) ?? "")
|
||||
}
|
||||
|
||||
export function parseMermaidTimelineDiagram(content: string): TimelineDiagram {
|
||||
const sections: TimelineSection[] = []
|
||||
const periods: TimelinePeriod[] = []
|
||||
const entries: TimelineEntry[] = []
|
||||
let direction: TimelineDirection = "LR"
|
||||
let title: string | undefined
|
||||
let currentPeriod: TimelinePeriod | undefined
|
||||
let inAccessibilityDescription = false
|
||||
|
||||
for (const source of meaningfulNumberedMermaidLines(content)) {
|
||||
const line = stripTimelineComment(source.text)
|
||||
if (inAccessibilityDescription) {
|
||||
if (line === "}") inAccessibilityDescription = false
|
||||
continue
|
||||
}
|
||||
if (/^accDescr\s*\{$/i.test(line)) {
|
||||
inAccessibilityDescription = true
|
||||
continue
|
||||
}
|
||||
if (!line || line.startsWith("#") || ACCESSIBILITY_RE.test(line)) continue
|
||||
const header = line.match(HEADER_RE)
|
||||
if (header) {
|
||||
direction = (header[1]?.toUpperCase() as TimelineDirection | undefined) ?? "LR"
|
||||
continue
|
||||
}
|
||||
|
||||
const titleMatch = line.match(TITLE_RE)
|
||||
if (titleMatch) {
|
||||
if (!titleMatch[1]) throw syntaxError(source.lineNumber, line, "Timeline title cannot be empty")
|
||||
title = stripMermaidQuotes(titleMatch[1])
|
||||
continue
|
||||
}
|
||||
|
||||
const sectionMatch = line.match(SECTION_RE)
|
||||
if (sectionMatch) {
|
||||
if (!sectionMatch[1]) throw syntaxError(source.lineNumber, line, "Timeline section cannot be empty")
|
||||
const section = { label: stripMermaidQuotes(sectionMatch[1]) }
|
||||
sections.push(section)
|
||||
entries.push({ type: "section", section })
|
||||
currentPeriod = undefined
|
||||
continue
|
||||
}
|
||||
|
||||
if (line.startsWith(":")) {
|
||||
if (!currentPeriod) {
|
||||
throw syntaxError(source.lineNumber, line, "Timeline continuation requires a preceding period")
|
||||
}
|
||||
currentPeriod.events.push(...parseEvents(line.slice(1), source.lineNumber, line))
|
||||
continue
|
||||
}
|
||||
|
||||
const fields = splitEventFields(line)
|
||||
const periodLabel = stripMermaidQuotes(fields.shift()!)
|
||||
if (!periodLabel) throw syntaxError(source.lineNumber, line, "Timeline period cannot be empty")
|
||||
const period = {
|
||||
period: periodLabel,
|
||||
events: fields.length === 0 ? [] : parseEventFields(fields, source.lineNumber, line),
|
||||
}
|
||||
periods.push(period)
|
||||
entries.push({ type: "period", period })
|
||||
currentPeriod = period
|
||||
}
|
||||
|
||||
return { direction, ...(title === undefined ? {} : { title }), sections, periods, entries }
|
||||
}
|
||||
|
||||
function parseEvents(value: string, lineNumber: number, sourceLine: string): string[] {
|
||||
return parseEventFields(splitEventFields(value), lineNumber, sourceLine)
|
||||
}
|
||||
|
||||
function parseEventFields(fields: string[], lineNumber: number, sourceLine: string): string[] {
|
||||
const events = fields.map(stripMermaidQuotes)
|
||||
if (events.length === 0 || events.some((event) => event.length === 0)) {
|
||||
throw syntaxError(lineNumber, sourceLine, "Timeline event cannot be empty")
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
function splitEventFields(value: string): string[] {
|
||||
const fields: string[] = []
|
||||
let quote: '"' | "'" | undefined
|
||||
let start = 0
|
||||
for (let index = 0; index < value.length; index++) {
|
||||
const char = value[index]
|
||||
if (char === '"' || char === "'") {
|
||||
if (quote === char) quote = undefined
|
||||
else if (quote === undefined && value.slice(start, index).trim() === "") quote = char
|
||||
continue
|
||||
}
|
||||
const next = value[index + 1]
|
||||
if (char !== ":" || quote !== undefined || (next !== undefined && !/\s/.test(next))) continue
|
||||
fields.push(value.slice(start, index))
|
||||
start = index + 1
|
||||
}
|
||||
fields.push(value.slice(start))
|
||||
return fields
|
||||
}
|
||||
|
||||
function stripTimelineComment(value: string): string {
|
||||
const comment = value.indexOf("%%")
|
||||
return (comment < 0 ? value : value.slice(0, comment)).trim()
|
||||
}
|
||||
|
||||
function syntaxError(lineNumber: number, sourceLine: string, reason?: string): MermaidSyntaxError {
|
||||
return new MermaidSyntaxError("timeline", lineNumber, sourceLine, reason)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import type { StyledText } from "@opentui/core"
|
||||
import type { DiagramCanvas } from "../core/canvas.js"
|
||||
import { renderDiagramGridStyledText } from "../core/render-grid.js"
|
||||
import type { TimelineStyleColors } from "./style.js"
|
||||
import type { TimelineCellStyle } from "./types.js"
|
||||
|
||||
export type TimelineGrid = DiagramCanvas<TimelineCellStyle>
|
||||
|
||||
export function renderTimelineGridText(grid: TimelineGrid): string {
|
||||
return grid.toString({ trimBottom: true })
|
||||
}
|
||||
|
||||
export function renderTimelineGridStyledText(grid: TimelineGrid, colors: TimelineStyleColors): StyledText {
|
||||
return renderDiagramGridStyledText(grid, (run) => (run.style ? colors[run.style] : undefined), undefined, {
|
||||
trimBottom: true,
|
||||
})
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { RGBA } from "@opentui/core"
|
||||
import { blendColor, numberedStyleKeys, rgba, type DiagramRgb } from "../core/color/style.js"
|
||||
import type { TimelineBaseCellStyle, TimelineCellStyle } from "./types.js"
|
||||
|
||||
const DEFAULT_THEME_RGB = {
|
||||
title: [228, 239, 232],
|
||||
section: [154, 184, 169],
|
||||
period: [230, 177, 126],
|
||||
spine: [111, 138, 126],
|
||||
event: [134, 225, 200],
|
||||
} as const satisfies Record<TimelineBaseCellStyle, DiagramRgb>
|
||||
|
||||
export type TimelineStyleColors = Required<Record<TimelineCellStyle, RGBA>>
|
||||
export const TIMELINE_SECTION_FADE_STYLES = numberedStyleKeys("sectionFade", [1, 2, 3] as const)
|
||||
export const TIMELINE_PERIOD_FADE_STYLES = numberedStyleKeys("periodFade", [1, 2, 3] as const)
|
||||
|
||||
export function resolveTimelineStyleColors(
|
||||
colors: Partial<Record<TimelineBaseCellStyle, RGBA | undefined>> = {},
|
||||
): TimelineStyleColors {
|
||||
const section = colors.section ?? rgba(DEFAULT_THEME_RGB.section)
|
||||
const period = colors.period ?? rgba(DEFAULT_THEME_RGB.period)
|
||||
const spine = colors.spine ?? rgba(DEFAULT_THEME_RGB.spine)
|
||||
return {
|
||||
title: colors.title ?? rgba(DEFAULT_THEME_RGB.title),
|
||||
section,
|
||||
period,
|
||||
spine,
|
||||
event: colors.event ?? rgba(DEFAULT_THEME_RGB.event),
|
||||
sectionFade1: blendColor(section, spine, 0.5),
|
||||
sectionFade2: blendColor(section, spine, 0.67),
|
||||
sectionFade3: blendColor(section, spine, 0.83),
|
||||
periodFade1: blendColor(period, spine, 0.5),
|
||||
periodFade2: blendColor(period, spine, 0.67),
|
||||
periodFade3: blendColor(period, spine, 0.83),
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
export type TimelineDirection = "TD" | "LR"
|
||||
|
||||
export interface TimelineSection {
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface TimelinePeriod {
|
||||
period: string
|
||||
events: string[]
|
||||
}
|
||||
|
||||
export type TimelineEntry = { type: "section"; section: TimelineSection } | { type: "period"; period: TimelinePeriod }
|
||||
|
||||
export interface TimelineDiagram {
|
||||
direction: TimelineDirection
|
||||
title?: string
|
||||
sections: TimelineSection[]
|
||||
periods: TimelinePeriod[]
|
||||
entries: TimelineEntry[]
|
||||
}
|
||||
|
||||
export interface TimelineDiagramRenderOptions {
|
||||
/** Parsed for Mermaid compatibility. Timeline diagrams always use a vertical terminal layout. */
|
||||
direction?: TimelineDirection
|
||||
}
|
||||
|
||||
export type TimelineBaseCellStyle = "title" | "section" | "period" | "spine" | "event"
|
||||
export type TimelineFadeStep = 1 | 2 | 3
|
||||
export type TimelineSectionFadeStyle = `sectionFade${TimelineFadeStep}`
|
||||
export type TimelinePeriodFadeStyle = `periodFade${TimelineFadeStep}`
|
||||
export type TimelineCellStyle = TimelineBaseCellStyle | TimelineSectionFadeStyle | TimelinePeriodFadeStyle
|
||||
@@ -7,7 +7,6 @@ export { Agent } from "@opencode-ai/schema/agent"
|
||||
export { Command } from "@opencode-ai/schema/command"
|
||||
export { Config } from "@opencode-ai/schema/config"
|
||||
export { Credential } from "@opencode-ai/schema/credential"
|
||||
export { Event } from "@opencode-ai/schema/event"
|
||||
export { FileSystem } from "@opencode-ai/schema/filesystem"
|
||||
export { Integration } from "@opencode-ai/schema/integration"
|
||||
export { Location } from "@opencode-ai/schema/location"
|
||||
|
||||
@@ -4,7 +4,6 @@ import { SessionInbox as CoreSessionInbox } from "@opencode-ai/core/session/inbo
|
||||
import { SessionMessage as CoreSessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { Agent } from "@opencode-ai/schema/agent"
|
||||
import { Config } from "@opencode-ai/schema/config"
|
||||
import { Event } from "@opencode-ai/schema/event"
|
||||
import { Location } from "@opencode-ai/schema/location"
|
||||
import { Model } from "@opencode-ai/schema/model"
|
||||
import { Project } from "@opencode-ai/schema/project"
|
||||
@@ -27,7 +26,6 @@ const CoreSession = await import("@opencode-ai/core/session")
|
||||
test("re-exports canonical contracts directly from Schema", () => {
|
||||
expect(SDK.Agent).toBe(Agent)
|
||||
expect(SDK.Config).toBe(Config)
|
||||
expect(SDK.Event).toBe(Event)
|
||||
expect(SDK.Model).toBe(Model)
|
||||
expect(SDK.WebSearch).toBe(WebSearch)
|
||||
expect(SDK.Session).toBe(Session)
|
||||
@@ -39,7 +37,6 @@ test("re-exports canonical contracts directly from Schema", () => {
|
||||
"Command",
|
||||
"Config",
|
||||
"Credential",
|
||||
"Event",
|
||||
"FileSystem",
|
||||
"Integration",
|
||||
"Location",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ClientError } from "@opencode-ai/client"
|
||||
import { createSignal, onCleanup, onMount, Show } from "solid-js"
|
||||
import { useClient } from "../context/client"
|
||||
import { useTheme } from "../context/theme"
|
||||
@@ -17,23 +16,9 @@ export function MigrationOverlay() {
|
||||
|
||||
onMount(async () => {
|
||||
await Bun.sleep(1_000)
|
||||
if (abort.signal.aborted) return
|
||||
void (async () => {
|
||||
while (true) {
|
||||
const result = await client.api.migration.v1.status({ signal: abort.signal }).then(
|
||||
(status) => ({ status }),
|
||||
(error: unknown) => ({ error }),
|
||||
)
|
||||
if ("error" in result) {
|
||||
if (result.error instanceof ClientError && result.error.reason === "Transport") {
|
||||
if (abort.signal.aborted) return
|
||||
await Bun.sleep(1_000)
|
||||
if (abort.signal.aborted) return
|
||||
continue
|
||||
}
|
||||
throw result.error
|
||||
}
|
||||
const status = result.status
|
||||
const status = await client.api.migration.v1.status({ signal: abort.signal })
|
||||
setProgress(status.status === "running" ? status.progress : undefined)
|
||||
if (status.status === "completed") return
|
||||
if (status.status === "error") throw new Error(status.error)
|
||||
|
||||
Reference in New Issue
Block a user