Merge branch 'dev' into sqlite2
commit
81b47a44e2
|
|
@ -153,7 +153,7 @@ export function StatusPopover() {
|
|||
"bg-border-weak-base": server.healthy() === undefined,
|
||||
}}
|
||||
/>
|
||||
<span class="text-12-regular text-text-strong">Status</span>
|
||||
<span class="text-12-regular text-text-strong">{language.t("status.popover.trigger")}</span>
|
||||
</div>
|
||||
}
|
||||
class="[&_[data-slot=popover-body]]:p-0 w-[360px] max-w-[calc(100vw-40px)] bg-transparent border-0 shadow-none rounded-xl"
|
||||
|
|
@ -166,7 +166,7 @@ export function StatusPopover() {
|
|||
style={{ "box-shadow": "var(--shadow-lg-border-base)" }}
|
||||
>
|
||||
<Tabs
|
||||
aria-label="Server Configurations"
|
||||
aria-label={language.t("status.popover.ariaLabel")}
|
||||
class="tabs"
|
||||
data-component="tabs"
|
||||
data-active="servers"
|
||||
|
|
@ -189,16 +189,20 @@ export function StatusPopover() {
|
|||
}}
|
||||
>
|
||||
<Tabs.Trigger value="servers" data-slot="tab" class="text-12-regular">
|
||||
{serverCount() > 0 ? `${serverCount()} ` : ""}Servers
|
||||
{serverCount() > 0 ? `${serverCount()} ` : ""}
|
||||
{language.t("status.popover.tab.servers")}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="mcp" data-slot="tab" class="text-12-regular">
|
||||
{mcpConnected() > 0 ? `${mcpConnected()} ` : ""}MCP
|
||||
{mcpConnected() > 0 ? `${mcpConnected()} ` : ""}
|
||||
{language.t("status.popover.tab.mcp")}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="lsp" data-slot="tab" class="text-12-regular">
|
||||
{lspCount() > 0 ? `${lspCount()} ` : ""}LSP
|
||||
{lspCount() > 0 ? `${lspCount()} ` : ""}
|
||||
{language.t("status.popover.tab.lsp")}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="plugins" data-slot="tab" class="text-12-regular">
|
||||
{pluginCount() > 0 ? `${pluginCount()} ` : ""}Plugins
|
||||
{pluginCount() > 0 ? `${pluginCount()} ` : ""}
|
||||
{language.t("status.popover.tab.plugins")}
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
|
|
@ -274,7 +278,7 @@ export function StatusPopover() {
|
|||
</Show>
|
||||
<Show when={isDefault()}>
|
||||
<span class="text-11-regular text-text-base bg-surface-base px-1.5 py-0.5 rounded-md">
|
||||
Default
|
||||
{language.t("common.default")}
|
||||
</span>
|
||||
</Show>
|
||||
<div class="flex-1" />
|
||||
|
|
@ -292,7 +296,7 @@ export function StatusPopover() {
|
|||
class="mt-3 self-start h-8 px-3 py-1.5"
|
||||
onClick={() => dialog.show(() => <DialogSelectServer />)}
|
||||
>
|
||||
Manage servers
|
||||
{language.t("status.popover.action.manageServers")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -304,7 +308,9 @@ export function StatusPopover() {
|
|||
<Show
|
||||
when={mcpItems().length > 0}
|
||||
fallback={
|
||||
<div class="text-14-regular text-text-base text-center my-auto">No MCP servers configured</div>
|
||||
<div class="text-14-regular text-text-base text-center my-auto">
|
||||
{language.t("dialog.mcp.empty")}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<For each={mcpItems()}>
|
||||
|
|
@ -351,7 +357,7 @@ export function StatusPopover() {
|
|||
when={lspItems().length > 0}
|
||||
fallback={
|
||||
<div class="text-14-regular text-text-base text-center my-auto">
|
||||
LSPs auto-detected from file types
|
||||
{language.t("dialog.lsp.empty")}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
|
@ -381,8 +387,19 @@ export function StatusPopover() {
|
|||
when={plugins().length > 0}
|
||||
fallback={
|
||||
<div class="text-14-regular text-text-base text-center my-auto">
|
||||
Plugins configured in{" "}
|
||||
<code class="bg-surface-raised-base px-1.5 py-0.5 rounded-sm text-text-base">opencode.json</code>
|
||||
{(() => {
|
||||
const value = language.t("dialog.plugins.empty")
|
||||
const file = "opencode.json"
|
||||
const parts = value.split(file)
|
||||
if (parts.length === 1) return value
|
||||
return (
|
||||
<>
|
||||
{parts[0]}
|
||||
<code class="bg-surface-raised-base px-1.5 py-0.5 rounded-sm text-text-base">{file}</code>
|
||||
{parts.slice(1).join(file)}
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -426,6 +426,14 @@ export const dict = {
|
|||
"session.header.search.placeholder": "بحث {{project}}",
|
||||
"session.header.searchFiles": "بحث عن الملفات",
|
||||
|
||||
"status.popover.trigger": "الحالة",
|
||||
"status.popover.ariaLabel": "إعدادات الخوادم",
|
||||
"status.popover.tab.servers": "الخوادم",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "الإضافات",
|
||||
"status.popover.action.manageServers": "إدارة الخوادم",
|
||||
|
||||
"session.share.popover.title": "نشر على الويب",
|
||||
"session.share.popover.description.shared": "هذه الجلسة عامة على الويب. يمكن لأي شخص لديه الرابط الوصول إليها.",
|
||||
"session.share.popover.description.unshared": "شارك الجلسة علنًا على الويب. ستكون متاحة لأي شخص لديه الرابط.",
|
||||
|
|
|
|||
|
|
@ -422,6 +422,14 @@ export const dict = {
|
|||
"session.header.search.placeholder": "Buscar {{project}}",
|
||||
"session.header.searchFiles": "Buscar arquivos",
|
||||
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.ariaLabel": "Configurações de servidores",
|
||||
"status.popover.tab.servers": "Servidores",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "Plugins",
|
||||
"status.popover.action.manageServers": "Gerenciar servidores",
|
||||
|
||||
"session.share.popover.title": "Publicar na web",
|
||||
"session.share.popover.description.shared":
|
||||
"Esta sessão é pública na web. Está acessível para qualquer pessoa com o link.",
|
||||
|
|
|
|||
|
|
@ -409,6 +409,14 @@ export const dict = {
|
|||
"session.header.search.placeholder": "Søg {{project}}",
|
||||
"session.header.searchFiles": "Søg efter filer",
|
||||
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.ariaLabel": "Serverkonfigurationer",
|
||||
"status.popover.tab.servers": "Servere",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "Plugins",
|
||||
"status.popover.action.manageServers": "Administrer servere",
|
||||
|
||||
"session.share.popover.title": "Udgiv på nettet",
|
||||
"session.share.popover.description.shared":
|
||||
"Denne session er offentlig på nettet. Den er tilgængelig for alle med linket.",
|
||||
|
|
|
|||
|
|
@ -416,6 +416,14 @@ export const dict = {
|
|||
"session.header.search.placeholder": "{{project}} durchsuchen",
|
||||
"session.header.searchFiles": "Dateien suchen",
|
||||
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.ariaLabel": "Serverkonfigurationen",
|
||||
"status.popover.tab.servers": "Server",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "Plugins",
|
||||
"status.popover.action.manageServers": "Server verwalten",
|
||||
|
||||
"session.share.popover.title": "Im Web veröffentlichen",
|
||||
"session.share.popover.description.shared":
|
||||
"Diese Sitzung ist öffentlich im Web. Sie ist für jeden mit dem Link zugänglich.",
|
||||
|
|
|
|||
|
|
@ -430,6 +430,14 @@ export const dict = {
|
|||
"session.header.search.placeholder": "Search {{project}}",
|
||||
"session.header.searchFiles": "Search files",
|
||||
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.ariaLabel": "Server configurations",
|
||||
"status.popover.tab.servers": "Servers",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "Plugins",
|
||||
"status.popover.action.manageServers": "Manage servers",
|
||||
|
||||
"session.share.popover.title": "Publish on web",
|
||||
"session.share.popover.description.shared":
|
||||
"This session is public on the web. It is accessible to anyone with the link.",
|
||||
|
|
|
|||
|
|
@ -410,6 +410,14 @@ export const dict = {
|
|||
"session.header.search.placeholder": "Buscar {{project}}",
|
||||
"session.header.searchFiles": "Buscar archivos",
|
||||
|
||||
"status.popover.trigger": "Estado",
|
||||
"status.popover.ariaLabel": "Configuraciones del servidor",
|
||||
"status.popover.tab.servers": "Servidores",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "Plugins",
|
||||
"status.popover.action.manageServers": "Administrar servidores",
|
||||
|
||||
"session.share.popover.title": "Publicar en web",
|
||||
"session.share.popover.description.shared":
|
||||
"Esta sesión es pública en la web. Es accesible para cualquiera con el enlace.",
|
||||
|
|
|
|||
|
|
@ -415,6 +415,14 @@ export const dict = {
|
|||
"session.header.search.placeholder": "Rechercher {{project}}",
|
||||
"session.header.searchFiles": "Rechercher des fichiers",
|
||||
|
||||
"status.popover.trigger": "Statut",
|
||||
"status.popover.ariaLabel": "Configurations des serveurs",
|
||||
"status.popover.tab.servers": "Serveurs",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "Plugins",
|
||||
"status.popover.action.manageServers": "Gérer les serveurs",
|
||||
|
||||
"session.share.popover.title": "Publier sur le web",
|
||||
"session.share.popover.description.shared":
|
||||
"Cette session est publique sur le web. Elle est accessible à toute personne disposant du lien.",
|
||||
|
|
|
|||
|
|
@ -407,6 +407,14 @@ export const dict = {
|
|||
"session.header.search.placeholder": "{{project}}を検索",
|
||||
"session.header.searchFiles": "ファイルを検索",
|
||||
|
||||
"status.popover.trigger": "ステータス",
|
||||
"status.popover.ariaLabel": "サーバー設定",
|
||||
"status.popover.tab.servers": "サーバー",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "プラグイン",
|
||||
"status.popover.action.manageServers": "サーバーを管理",
|
||||
|
||||
"session.share.popover.title": "ウェブで公開",
|
||||
"session.share.popover.description.shared":
|
||||
"このセッションはウェブで公開されています。リンクを知っている人なら誰でもアクセスできます。",
|
||||
|
|
|
|||
|
|
@ -410,6 +410,14 @@ export const dict = {
|
|||
"session.header.search.placeholder": "{{project}} 검색",
|
||||
"session.header.searchFiles": "파일 검색",
|
||||
|
||||
"status.popover.trigger": "상태",
|
||||
"status.popover.ariaLabel": "서버 구성",
|
||||
"status.popover.tab.servers": "서버",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "플러그인",
|
||||
"status.popover.action.manageServers": "서버 관리",
|
||||
|
||||
"session.share.popover.title": "웹에 게시",
|
||||
"session.share.popover.description.shared": "이 세션은 웹에 공개되었습니다. 링크가 있는 누구나 액세스할 수 있습니다.",
|
||||
"session.share.popover.description.unshared":
|
||||
|
|
|
|||
|
|
@ -430,6 +430,14 @@ export const dict = {
|
|||
"session.header.search.placeholder": "Søk i {{project}}",
|
||||
"session.header.searchFiles": "Søk etter filer",
|
||||
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.ariaLabel": "Serverkonfigurasjoner",
|
||||
"status.popover.tab.servers": "Servere",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "Plugins",
|
||||
"status.popover.action.manageServers": "Administrer servere",
|
||||
|
||||
"session.share.popover.title": "Publiser på nett",
|
||||
"session.share.popover.description.shared":
|
||||
"Denne sesjonen er offentlig på nettet. Den er tilgjengelig for alle med lenken.",
|
||||
|
|
|
|||
|
|
@ -428,6 +428,14 @@ export const dict = {
|
|||
"session.header.search.placeholder": "Szukaj {{project}}",
|
||||
"session.header.searchFiles": "Szukaj plików",
|
||||
|
||||
"status.popover.trigger": "Status",
|
||||
"status.popover.ariaLabel": "Konfiguracje serwerów",
|
||||
"status.popover.tab.servers": "Serwery",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "Wtyczki",
|
||||
"status.popover.action.manageServers": "Zarządzaj serwerami",
|
||||
|
||||
"session.share.popover.title": "Opublikuj w sieci",
|
||||
"session.share.popover.description.shared":
|
||||
"Ta sesja jest publiczna w sieci. Jest dostępna dla każdego, kto posiada link.",
|
||||
|
|
|
|||
|
|
@ -430,6 +430,14 @@ export const dict = {
|
|||
"session.header.search.placeholder": "Поиск {{project}}",
|
||||
"session.header.searchFiles": "Поиск файлов",
|
||||
|
||||
"status.popover.trigger": "Статус",
|
||||
"status.popover.ariaLabel": "Настройки серверов",
|
||||
"status.popover.tab.servers": "Серверы",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "Плагины",
|
||||
"status.popover.action.manageServers": "Управлять серверами",
|
||||
|
||||
"session.share.popover.title": "Опубликовать в интернете",
|
||||
"session.share.popover.description.shared":
|
||||
"Эта сессия общедоступна. Доступ к ней может получить любой, у кого есть ссылка.",
|
||||
|
|
|
|||
|
|
@ -405,6 +405,14 @@ export const dict = {
|
|||
"session.header.search.placeholder": "搜索 {{project}}",
|
||||
"session.header.searchFiles": "搜索文件",
|
||||
|
||||
"status.popover.trigger": "状态",
|
||||
"status.popover.ariaLabel": "服务器配置",
|
||||
"status.popover.tab.servers": "服务器",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "插件",
|
||||
"status.popover.action.manageServers": "管理服务器",
|
||||
|
||||
"session.share.popover.title": "发布到网页",
|
||||
"session.share.popover.description.shared": "此会话已在网页上公开。任何拥有链接的人都可以访问。",
|
||||
"session.share.popover.description.unshared": "在网页上公开分享此会话。任何拥有链接的人都可以访问。",
|
||||
|
|
|
|||
|
|
@ -407,6 +407,14 @@ export const dict = {
|
|||
"session.header.search.placeholder": "搜尋 {{project}}",
|
||||
"session.header.searchFiles": "搜尋檔案",
|
||||
|
||||
"status.popover.trigger": "狀態",
|
||||
"status.popover.ariaLabel": "伺服器設定",
|
||||
"status.popover.tab.servers": "伺服器",
|
||||
"status.popover.tab.mcp": "MCP",
|
||||
"status.popover.tab.lsp": "LSP",
|
||||
"status.popover.tab.plugins": "外掛程式",
|
||||
"status.popover.action.manageServers": "管理伺服器",
|
||||
|
||||
"session.share.popover.title": "發佈到網頁",
|
||||
"session.share.popover.description.shared": "此工作階段已在網頁上公開。任何擁有連結的人都可以存取。",
|
||||
"session.share.popover.description.unshared": "在網頁上公開分享此工作階段。任何擁有連結的人都可以存取。",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,114 @@
|
|||
type Release = {
|
||||
tag_name: string
|
||||
name: string
|
||||
body: string
|
||||
published_at: string
|
||||
html_url: string
|
||||
}
|
||||
|
||||
type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string }
|
||||
|
||||
type HighlightItem = {
|
||||
title: string
|
||||
description: string
|
||||
shortDescription?: string
|
||||
media: HighlightMedia
|
||||
}
|
||||
|
||||
type HighlightGroup = {
|
||||
source: string
|
||||
items: HighlightItem[]
|
||||
}
|
||||
|
||||
function parseHighlights(body: string): HighlightGroup[] {
|
||||
const groups = new Map<string, HighlightItem[]>()
|
||||
const regex = /<highlight\s+source="([^"]+)">([\s\S]*?)<\/highlight>/g
|
||||
let match
|
||||
|
||||
while ((match = regex.exec(body)) !== null) {
|
||||
const source = match[1]
|
||||
const content = match[2]
|
||||
|
||||
const titleMatch = content.match(/<h2>([^<]+)<\/h2>/)
|
||||
const pMatch = content.match(/<p(?:\s+short="([^"]*)")?>([^<]+)<\/p>/)
|
||||
const imgMatch = content.match(/<img\s+width="([^"]+)"\s+height="([^"]+)"\s+alt="[^"]*"\s+src="([^"]+)"/)
|
||||
const videoMatch = content.match(/^\s*(https:\/\/github\.com\/user-attachments\/assets\/[a-f0-9-]+)\s*$/m)
|
||||
|
||||
let media: HighlightMedia | undefined
|
||||
if (videoMatch) {
|
||||
media = { type: "video", src: videoMatch[1] }
|
||||
} else if (imgMatch) {
|
||||
media = { type: "image", src: imgMatch[3], width: imgMatch[1], height: imgMatch[2] }
|
||||
}
|
||||
|
||||
if (titleMatch && media) {
|
||||
const item: HighlightItem = {
|
||||
title: titleMatch[1],
|
||||
description: pMatch?.[2] || "",
|
||||
shortDescription: pMatch?.[1],
|
||||
media,
|
||||
}
|
||||
|
||||
if (!groups.has(source)) {
|
||||
groups.set(source, [])
|
||||
}
|
||||
groups.get(source)!.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(groups.entries()).map(([source, items]) => ({ source, items }))
|
||||
}
|
||||
|
||||
function parseMarkdown(body: string) {
|
||||
const lines = body.split("\n")
|
||||
const sections: { title: string; items: string[] }[] = []
|
||||
let current: { title: string; items: string[] } | null = null
|
||||
let skip = false
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("## ")) {
|
||||
if (current) sections.push(current)
|
||||
const title = line.slice(3).trim()
|
||||
current = { title, items: [] }
|
||||
skip = false
|
||||
} else if (line.startsWith("**Thank you")) {
|
||||
skip = true
|
||||
} else if (line.startsWith("- ") && !skip) {
|
||||
current?.items.push(line.slice(2).trim())
|
||||
}
|
||||
}
|
||||
if (current) sections.push(current)
|
||||
|
||||
const highlights = parseHighlights(body)
|
||||
|
||||
return { sections, highlights }
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const response = await fetch("https://api.github.com/repos/anomalyco/opencode/releases?per_page=20", {
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"User-Agent": "OpenCode-Console",
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return { releases: [] }
|
||||
}
|
||||
|
||||
const releases = (await response.json()) as Release[]
|
||||
|
||||
return {
|
||||
releases: releases.map((release) => {
|
||||
const parsed = parseMarkdown(release.body || "")
|
||||
return {
|
||||
tag: release.tag_name,
|
||||
name: release.name,
|
||||
date: release.published_at,
|
||||
url: release.html_url,
|
||||
highlights: parsed.highlights,
|
||||
sections: parsed.sections,
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
@ -367,11 +367,18 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
position: sticky;
|
||||
top: 80px;
|
||||
align-self: start;
|
||||
background: var(--color-background);
|
||||
padding: 8px 0;
|
||||
|
||||
@media (max-width: 50rem) {
|
||||
position: static;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
[data-slot="version"] {
|
||||
|
|
@ -402,24 +409,26 @@
|
|||
|
||||
[data-component="section"] {
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-strong);
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding-left: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
gap: 4px;
|
||||
|
||||
li {
|
||||
color: var(--color-text);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
padding-left: 16px;
|
||||
padding-left: 12px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
|
|
@ -431,7 +440,7 @@
|
|||
|
||||
[data-slot="author"] {
|
||||
color: var(--color-text-weak);
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
margin-left: 4px;
|
||||
text-decoration: none;
|
||||
|
||||
|
|
@ -465,6 +474,120 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="highlights"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
[data-component="collapsible-sections"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
[data-component="collapsible-section"] {
|
||||
[data-slot="toggle"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 6px 0;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-weak);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
[data-slot="icon"] {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding-left: 16px;
|
||||
padding-bottom: 8px;
|
||||
|
||||
li {
|
||||
color: var(--color-text);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
padding-left: 12px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "-";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--color-text-weak);
|
||||
}
|
||||
|
||||
[data-slot="author"] {
|
||||
color: var(--color-text-weak);
|
||||
font-size: 12px;
|
||||
margin-left: 4px;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
text-decoration-thickness: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="highlight"] {
|
||||
h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-strong);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--color-border-weak);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
[data-slot="highlight-item"] {
|
||||
margin-bottom: 24px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
p[data-slot="title"] {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { Header } from "~/component/header"
|
|||
import { Footer } from "~/component/footer"
|
||||
import { Legal } from "~/component/legal"
|
||||
import { config } from "~/config"
|
||||
import { For, Show } from "solid-js"
|
||||
import { For, Show, createSignal } from "solid-js"
|
||||
|
||||
type Release = {
|
||||
tag_name: string
|
||||
|
|
@ -40,6 +40,59 @@ function formatDate(dateString: string) {
|
|||
})
|
||||
}
|
||||
|
||||
type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string }
|
||||
|
||||
type HighlightItem = {
|
||||
title: string
|
||||
description: string
|
||||
shortDescription?: string
|
||||
media: HighlightMedia
|
||||
}
|
||||
|
||||
type HighlightGroup = {
|
||||
source: string
|
||||
items: HighlightItem[]
|
||||
}
|
||||
|
||||
function parseHighlights(body: string): HighlightGroup[] {
|
||||
const groups = new Map<string, HighlightItem[]>()
|
||||
const regex = /<highlight\s+source="([^"]+)">([\s\S]*?)<\/highlight>/g
|
||||
let match
|
||||
|
||||
while ((match = regex.exec(body)) !== null) {
|
||||
const source = match[1]
|
||||
const content = match[2]
|
||||
|
||||
const titleMatch = content.match(/<h2>([^<]+)<\/h2>/)
|
||||
const pMatch = content.match(/<p(?:\s+short="([^"]*)")?>([^<]+)<\/p>/)
|
||||
const imgMatch = content.match(/<img\s+width="([^"]+)"\s+height="([^"]+)"\s+alt="[^"]*"\s+src="([^"]+)"/)
|
||||
const videoMatch = content.match(/^\s*(https:\/\/github\.com\/user-attachments\/assets\/[a-f0-9-]+)\s*$/m)
|
||||
|
||||
let media: HighlightMedia | undefined
|
||||
if (videoMatch) {
|
||||
media = { type: "video", src: videoMatch[1] }
|
||||
} else if (imgMatch) {
|
||||
media = { type: "image", src: imgMatch[3], width: imgMatch[1], height: imgMatch[2] }
|
||||
}
|
||||
|
||||
if (titleMatch && media) {
|
||||
const item: HighlightItem = {
|
||||
title: titleMatch[1],
|
||||
description: pMatch?.[2] || "",
|
||||
shortDescription: pMatch?.[1],
|
||||
media,
|
||||
}
|
||||
|
||||
if (!groups.has(source)) {
|
||||
groups.set(source, [])
|
||||
}
|
||||
groups.get(source)!.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(groups.entries()).map(([source, items]) => ({ source, items }))
|
||||
}
|
||||
|
||||
function parseMarkdown(body: string) {
|
||||
const lines = body.split("\n")
|
||||
const sections: { title: string; items: string[] }[] = []
|
||||
|
|
@ -60,7 +113,9 @@ function parseMarkdown(body: string) {
|
|||
}
|
||||
if (current) sections.push(current)
|
||||
|
||||
return { sections }
|
||||
const highlights = parseHighlights(body)
|
||||
|
||||
return { sections, highlights }
|
||||
}
|
||||
|
||||
function ReleaseItem(props: { item: string }) {
|
||||
|
|
@ -87,6 +142,60 @@ function ReleaseItem(props: { item: string }) {
|
|||
)
|
||||
}
|
||||
|
||||
function HighlightSection(props: { group: HighlightGroup }) {
|
||||
return (
|
||||
<div data-component="highlight">
|
||||
<h4>{props.group.source}</h4>
|
||||
<hr />
|
||||
<For each={props.group.items}>
|
||||
{(item) => (
|
||||
<div data-slot="highlight-item">
|
||||
<p data-slot="title">{item.title}</p>
|
||||
<p>{item.description}</p>
|
||||
<Show when={item.media.type === "video"}>
|
||||
<video src={item.media.src} controls autoplay loop muted playsinline />
|
||||
</Show>
|
||||
<Show when={item.media.type === "image"}>
|
||||
<img
|
||||
src={item.media.src}
|
||||
alt={item.title}
|
||||
width={(item.media as { width: string }).width}
|
||||
height={(item.media as { height: string }).height}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CollapsibleSection(props: { section: { title: string; items: string[] } }) {
|
||||
const [open, setOpen] = createSignal(false)
|
||||
|
||||
return (
|
||||
<div data-component="collapsible-section">
|
||||
<button data-slot="toggle" onClick={() => setOpen(!open())}>
|
||||
<span data-slot="icon">{open() ? "▾" : "▸"}</span>
|
||||
<span>{props.section.title}</span>
|
||||
</button>
|
||||
<Show when={open()}>
|
||||
<ul>
|
||||
<For each={props.section.items}>{(item) => <ReleaseItem item={item} />}</For>
|
||||
</ul>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CollapsibleSections(props: { sections: { title: string; items: string[] }[] }) {
|
||||
return (
|
||||
<div data-component="collapsible-sections">
|
||||
<For each={props.sections}>{(section) => <CollapsibleSection section={section} />}</For>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Changelog() {
|
||||
const releases = createAsync(() => getReleases())
|
||||
|
||||
|
|
@ -120,16 +229,26 @@ export default function Changelog() {
|
|||
<time dateTime={release.published_at}>{formatDate(release.published_at)}</time>
|
||||
</header>
|
||||
<div data-slot="content">
|
||||
<For each={parsed().sections}>
|
||||
{(section) => (
|
||||
<div data-component="section">
|
||||
<h3>{section.title}</h3>
|
||||
<ul>
|
||||
<For each={section.items}>{(item) => <ReleaseItem item={item} />}</For>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
<Show when={parsed().highlights.length > 0}>
|
||||
<div data-component="highlights">
|
||||
<For each={parsed().highlights}>{(group) => <HighlightSection group={group} />}</For>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={parsed().highlights.length > 0 && parsed().sections.length > 0}>
|
||||
<CollapsibleSections sections={parsed().sections} />
|
||||
</Show>
|
||||
<Show when={parsed().highlights.length === 0}>
|
||||
<For each={parsed().sections}>
|
||||
{(section) => (
|
||||
<div data-component="section">
|
||||
<h3>{section.title}</h3>
|
||||
<ul>
|
||||
<For each={section.items}>{(item) => <ReleaseItem item={item} />}</For>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -488,15 +488,6 @@ function App() {
|
|||
},
|
||||
category: "System",
|
||||
},
|
||||
{
|
||||
title: "Open WebUI",
|
||||
value: "webui.open",
|
||||
onSelect: () => {
|
||||
open(sdk.url).catch(() => {})
|
||||
dialog.clear()
|
||||
},
|
||||
category: "System",
|
||||
},
|
||||
{
|
||||
title: "Exit the app",
|
||||
value: "app.exit",
|
||||
|
|
|
|||
|
|
@ -333,32 +333,57 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||
const start = Date.now() - 30 * 24 * 60 * 60 * 1000
|
||||
const sessionListPromise = sdk.client.session
|
||||
.list({ start: start })
|
||||
.then((x) => setStore("session", reconcile((x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))))
|
||||
.then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))
|
||||
|
||||
// blocking - include session.list when continuing a session
|
||||
const providersPromise = sdk.client.config.providers({}, { throwOnError: true })
|
||||
const providerListPromise = sdk.client.provider.list({}, { throwOnError: true })
|
||||
const agentsPromise = sdk.client.app.agents({}, { throwOnError: true })
|
||||
const configPromise = sdk.client.config.get({}, { throwOnError: true })
|
||||
const blockingRequests: Promise<unknown>[] = [
|
||||
sdk.client.config.providers({}, { throwOnError: true }).then((x) => {
|
||||
batch(() => {
|
||||
setStore("provider", reconcile(x.data!.providers))
|
||||
setStore("provider_default", reconcile(x.data!.default))
|
||||
})
|
||||
}),
|
||||
sdk.client.provider.list({}, { throwOnError: true }).then((x) => {
|
||||
batch(() => {
|
||||
setStore("provider_next", reconcile(x.data!))
|
||||
})
|
||||
}),
|
||||
sdk.client.app.agents({}, { throwOnError: true }).then((x) => setStore("agent", reconcile(x.data ?? []))),
|
||||
sdk.client.config.get({}, { throwOnError: true }).then((x) => setStore("config", reconcile(x.data!))),
|
||||
providersPromise,
|
||||
providerListPromise,
|
||||
agentsPromise,
|
||||
configPromise,
|
||||
...(args.continue ? [sessionListPromise] : []),
|
||||
]
|
||||
|
||||
await Promise.all(blockingRequests)
|
||||
.then(() => {
|
||||
const providersResponse = providersPromise.then((x) => x.data!)
|
||||
const providerListResponse = providerListPromise.then((x) => x.data!)
|
||||
const agentsResponse = agentsPromise.then((x) => x.data ?? [])
|
||||
const configResponse = configPromise.then((x) => x.data!)
|
||||
const sessionListResponse = args.continue ? sessionListPromise : undefined
|
||||
|
||||
return Promise.all([
|
||||
providersResponse,
|
||||
providerListResponse,
|
||||
agentsResponse,
|
||||
configResponse,
|
||||
...(sessionListResponse ? [sessionListResponse] : []),
|
||||
]).then((responses) => {
|
||||
const providers = responses[0]
|
||||
const providerList = responses[1]
|
||||
const agents = responses[2]
|
||||
const config = responses[3]
|
||||
const sessions = responses[4]
|
||||
|
||||
batch(() => {
|
||||
setStore("provider", reconcile(providers.providers))
|
||||
setStore("provider_default", reconcile(providers.default))
|
||||
setStore("provider_next", reconcile(providerList))
|
||||
setStore("agent", reconcile(agents))
|
||||
setStore("config", reconcile(config))
|
||||
if (sessions !== undefined) setStore("session", reconcile(sessions))
|
||||
})
|
||||
})
|
||||
})
|
||||
.then(() => {
|
||||
if (store.status !== "complete") setStore("status", "partial")
|
||||
// non-blocking
|
||||
Promise.all([
|
||||
...(args.continue ? [] : [sessionListPromise]),
|
||||
...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]),
|
||||
sdk.client.command.list().then((x) => setStore("command", reconcile(x.data ?? []))),
|
||||
sdk.client.lsp.status().then((x) => setStore("lsp", reconcile(x.data!))),
|
||||
sdk.client.mcp.status().then((x) => setStore("mcp", reconcile(x.data!))),
|
||||
|
|
|
|||
Loading…
Reference in New Issue