diff --git a/desktop/frontend/dock/moon.pkg b/desktop/frontend/dock/moon.pkg
index e73723d10..5cf8b68bf 100644
--- a/desktop/frontend/dock/moon.pkg
+++ b/desktop/frontend/dock/moon.pkg
@@ -1,6 +1,7 @@
import {
"moonbit-community/rabbita/cmd",
"moonbit-community/rabbita/html",
+ "moonbit-community/rabbita/svg",
"openseek_desktop/frontend/pathx",
"openseek_desktop/frontend/interop",
}
diff --git a/desktop/frontend/dock/view.mbt b/desktop/frontend/dock/view.mbt
index ec2381bba..6f79f279f 100644
--- a/desktop/frontend/dock/view.mbt
+++ b/desktop/frontend/dock/view.mbt
@@ -6,42 +6,91 @@
///|
/// The sidebar-right codicon for the panel toggle, from Microsoft's
-/// vscode-codicons (CC BY 4.0), embedded unmodified.
-let icon_layout_sidebar_right : String =
- #|
+/// vscode-codicons (CC BY 4.0); the path data is unmodified, transcribed
+/// into `@svg` builders (an innerHTML-bearing element cannot be safely
+/// morphed by the positional child diff).
+fn icon_layout_sidebar_right() -> @html.Html {
+ codicon(
+ "M12.5 1C13.881 1 15 2.119 15 3.5V12.5C15 13.881 13.881 15 12.5 15H3.5C2.119 15 1 13.881 1 12.5V3.5C1 2.119 2.119 1 3.5 1H12.5ZM9 14V2H3.5C2.672 2 2 2.672 2 3.5V12.5C2 13.328 2.672 14 3.5 14H9Z",
+ )
+}
///|
/// The add codicon (CC BY 4.0): the strip's new-tab button.
-let icon_add : String =
- #|
+fn icon_add() -> @html.Html {
+ codicon(
+ "M8 1.5C8 1.22386 7.77614 1 7.5 1C7.22386 1 7 1.22386 7 1.5V7H1.5C1.22386 7 1 7.22386 1 7.5C1 7.77614 1.22386 8 1.5 8H7V13.5C7 13.7761 7.22386 14 7.5 14C7.77614 14 8 13.7761 8 13.5V8H13.5C13.7761 8 14 7.77614 14 7.5C14 7.22386 13.7761 7 13.5 7H8V1.5Z",
+ )
+}
///|
/// The folder codicon (CC BY 4.0): the launcher's Files row.
-let icon_folder : String =
- #|
+fn icon_folder() -> @html.Html {
+ codicon(
+ "M2 4.5V6H5.58579C5.71839 6 5.84557 5.94732 5.93934 5.85355L7.29289 4.5L5.93934 3.14645C5.84557 3.05268 5.71839 3 5.58579 3H3.5C2.67157 3 2 3.67157 2 4.5ZM1 4.5C1 3.11929 2.11929 2 3.5 2H5.58579C5.98361 2 6.36514 2.15804 6.64645 2.43934L8.20711 4H12.5C13.8807 4 15 5.11929 15 6.5V11.5C15 12.8807 13.8807 14 12.5 14H3.5C2.11929 14 1 12.8807 1 11.5V4.5ZM2 7V11.5C2 12.3284 2.67157 13 3.5 13H12.5C13.3284 13 14 12.3284 14 11.5V6.5C14 5.67157 13.3284 5 12.5 5H8.20711L6.64645 6.56066C6.36514 6.84197 5.98361 7 5.58579 7H2Z",
+ )
+}
+
+///|
+/// A 16×16 codicon: one filled path tinted by `currentColor`.
+fn codicon(d : String) -> @html.Html {
+ @svg.svg(
+ width=16,
+ height=16,
+ view_box="0 0 16 16",
+ attrs=@svg.Attrs::build().fill("currentColor"),
+ [@svg.path(d~)],
+ )
+}
///|
/// A simple globe for the launcher's Browse row (drawn here, not a
/// codicon): a circle with one meridian and the equator.
-let icon_globe : String =
- #|
+fn icon_globe() -> @html.Html {
+ @svg.svg(
+ width=16,
+ height=16,
+ view_box="0 0 16 16",
+ attrs=@svg.Attrs::build().fill("none").stroke("currentColor"),
+ [
+ @svg.circle(cx=8, cy=8, r=6),
+ @svg.node(
+ "ellipse",
+ @svg.Attrs::build().cx("8").cy("8").rx("2.6").ry("6"),
+ [],
+ ),
+ @svg.path(d="M2 8h12"),
+ ],
+ )
+}
///|
/// A compact diff document for the launcher's Review row.
-let icon_review : String =
- #|
+fn icon_review() -> @html.Html {
+ @svg.svg(
+ width=16,
+ height=16,
+ view_box="0 0 16 16",
+ attrs=@svg.Attrs::build()
+ .fill("none")
+ .stroke("currentColor")
+ .stroke_linecap("round"),
+ [
+ @svg.path(d="M5.5 2.5h7v11h-9v-7"),
+ @svg.path(d="M1 3.5h4M3 1.5v4M1 9.5h4"),
+ @svg.path(d="M7 6.5h3.5M7 9.5h3.5"),
+ ],
+ )
+}
///|
-/// An icon's SVG markup injected into a fixed-size span. The markup is a
-/// trusted compile-time constant, never user input, so the XSS alert on
-/// `inner_html` does not apply.
-#warnings("-alert_xss_vulnerable")
-fn icon(markup : String) -> @html.Html {
- @html.span(
- class="icon",
- attrs=@html.Attrs::build().inner_html(markup).aria_hidden("true"),
- ([] : Array[@html.Html]),
- )
+/// An icon in a fixed-size span. Icons are functions, not shared constants:
+/// each render site gets its own virtual-DOM value, so no Props instance is
+/// ever aliased between two mounted elements.
+fn icon(image : () -> @html.Html) -> @html.Html {
+ @html.span(class="icon", attrs=@html.Attrs::build().aria_hidden("true"), [
+ image(),
+ ])
}
///|
diff --git a/desktop/frontend/fileeditor/moon.pkg b/desktop/frontend/fileeditor/moon.pkg
index 653e7bb0a..e60d03476 100644
--- a/desktop/frontend/fileeditor/moon.pkg
+++ b/desktop/frontend/fileeditor/moon.pkg
@@ -3,6 +3,7 @@ import {
"moonbit-community/rabbita/dom",
"moonbit-community/rabbita/html",
"moonbit-community/rabbita/js",
+ "moonbit-community/rabbita/svg",
"moonbitlang/editor/base/browser" @base_browser,
"moonbitlang/editor/base/common",
"moonbitlang/editor/language",
diff --git a/desktop/frontend/fileeditor/view.mbt b/desktop/frontend/fileeditor/view.mbt
index 22b4ce584..4bd45452d 100644
--- a/desktop/frontend/fileeditor/view.mbt
+++ b/desktop/frontend/fileeditor/view.mbt
@@ -5,10 +5,23 @@
///|
/// The folder codicon for the tree-pane toggle, from Microsoft's
-/// vscode-codicons (CC BY 4.0), embedded unmodified — mirrors the main
-/// package's icon set, which is not importable from here.
-let icon_folder : String =
- #|
+/// vscode-codicons (CC BY 4.0); the path data is unmodified, transcribed
+/// into `@svg` builders (an innerHTML-bearing element cannot be safely
+/// morphed by the positional child diff) — mirrors the main package's icon
+/// set, which is not importable from here.
+fn icon_folder() -> @html.Html {
+ @svg.svg(
+ width=16,
+ height=16,
+ view_box="0 0 16 16",
+ attrs=@svg.Attrs::build().fill("currentColor"),
+ [
+ @svg.path(
+ d="M2 4.5V6H5.58579C5.71839 6 5.84557 5.94732 5.93934 5.85355L7.29289 4.5L5.93934 3.14645C5.84557 3.05268 5.71839 3 5.58579 3H3.5C2.67157 3 2 3.67157 2 4.5ZM1 4.5C1 3.11929 2.11929 2 3.5 2H5.58579C5.98361 2 6.36514 2.15804 6.64645 2.43934L8.20711 4H12.5C13.8807 4 15 5.11929 15 6.5V11.5C15 12.8807 13.8807 14 12.5 14H3.5C2.11929 14 1 12.8807 1 11.5V4.5ZM2 7V11.5C2 12.3284 2.67157 13 3.5 13H12.5C13.3284 13 14 12.3284 14 11.5V6.5C14 5.67157 13.3284 5 12.5 5H8.20711L6.64645 6.56066C6.36514 6.84197 5.98361 7 5.58579 7H2Z",
+ ),
+ ],
+ )
+}
///|
const ExplorerFilesTabId = "explorer-files-tab"
@@ -58,16 +71,13 @@ fn focus_changed_file_after_render(index : Int) -> @cmd.Cmd {
}
///|
-/// An icon's SVG markup injected into a fixed-size span. The markup is a
-/// trusted compile-time constant, never user input, so the XSS alert on
-/// `inner_html` does not apply.
-#warnings("-alert_xss_vulnerable")
-fn icon(markup : String) -> @html.Html {
- @html.span(
- class="icon",
- attrs=@html.Attrs::build().inner_html(markup).aria_hidden("true"),
- ([] : Array[@html.Html]),
- )
+/// An icon in a fixed-size span. Icons are functions, not shared constants:
+/// each render site gets its own virtual-DOM value, so no Props instance is
+/// ever aliased between two mounted elements.
+fn icon(image : () -> @html.Html) -> @html.Html {
+ @html.span(class="icon", attrs=@html.Attrs::build().aria_hidden("true"), [
+ image(),
+ ])
}
///|
diff --git a/desktop/frontend/icons.mbt b/desktop/frontend/icons.mbt
index 78f4e4b9b..3fef87dfc 100644
--- a/desktop/frontend/icons.mbt
+++ b/desktop/frontend/icons.mbt
@@ -1,121 +1,308 @@
// Inline SVG icons for the app chrome, taken from Microsoft's
// vscode-codicons (https://github.com/microsoft/vscode-codicons),
// licensed CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/);
-// unmodified except for being embedded as strings. They render through
-// `icon`, sized by the surrounding control and tinted via `currentColor`.
+// the path data is unmodified, transcribed into `@svg` element builders.
+// They render through `icon`, sized by the surrounding control and tinted
+// via `currentColor`. Built as virtual-DOM nodes rather than `inner_html`
+// markup: an innerHTML-bearing element cannot be safely morphed by the
+// positional child diff (assigning the property silently drops the real
+// children the runtime still tracks).
///|
-/// An icon's SVG markup injected into a fixed-size span. The markup is a
-/// trusted compile-time constant (the `let`s below), never user input, so
-/// the XSS alert on `inner_html` does not apply.
-#warnings("-alert_xss_vulnerable")
-fn icon(markup : String) -> @html.Html {
- @html.span(
- class="icon",
- attrs=@html.Attrs::build().inner_html(markup).aria_hidden("true"),
- ([] : Array[@html.Html]),
+/// An icon in a fixed-size span. Icons are functions, not shared constants:
+/// each render site gets its own virtual-DOM value, so no Props instance is
+/// ever aliased between two mounted elements.
+fn icon(image : () -> @html.Html) -> @html.Html {
+ @html.span(class="icon", attrs=@html.Attrs::build().aria_hidden("true"), [
+ image(),
+ ])
+}
+
+///|
+/// A 16×16 codicon: one filled path tinted by `currentColor`.
+fn codicon(d : String) -> @html.Html {
+ @svg.svg(
+ width=16,
+ height=16,
+ view_box="0 0 16 16",
+ attrs=@svg.Attrs::build().fill("currentColor"),
+ [@svg.path(d~)],
)
}
///|
-let icon_add : String =
- #|
+/// A circle whose geometry needs fractional coordinates; `@svg.circle`
+/// takes integers only.
+fn svg_circle(cx : String, cy : String, r : String) -> @svg.Svg {
+ @svg.node("circle", @svg.Attrs::build().cx(cx).cy(cy).r(r), [])
+}
///|
-let icon_archive : String =
- #|
+fn icon_add() -> @html.Html {
+ codicon(
+ "M8 1.5C8 1.22386 7.77614 1 7.5 1C7.22386 1 7 1.22386 7 1.5V7H1.5C1.22386 7 1 7.22386 1 7.5C1 7.77614 1.22386 8 1.5 8H7V13.5C7 13.7761 7.22386 14 7.5 14C7.77614 14 8 13.7761 8 13.5V8H13.5C13.7761 8 14 7.77614 14 7.5C14 7.22386 13.7761 7 13.5 7H8V1.5Z",
+ )
+}
///|
-let icon_arrow_down : String =
- #|
+fn icon_archive() -> @html.Html {
+ codicon(
+ "M6.5 8C6.22386 8 6 8.22386 6 8.5C6 8.77614 6.22386 9 6.5 9H9.5C9.77614 9 10 8.77614 10 8.5C10 8.22386 9.77614 8 9.5 8H6.5ZM1 3.5C1 2.67157 1.67157 2 2.5 2H13.5C14.3284 2 15 2.67157 15 3.5V4.5C15 5.15311 14.5826 5.70873 14 5.91465V11.5C14 12.8807 12.8807 14 11.5 14H4.5C3.11929 14 2 12.8807 2 11.5V5.91465C1.4174 5.70873 1 5.15311 1 4.5V3.5ZM2.5 3C2.22386 3 2 3.22386 2 3.5V4.5C2 4.77614 2.22386 5 2.5 5H13.5C13.7761 5 14 4.77614 14 4.5V3.5C14 3.22386 13.7761 3 13.5 3H2.5ZM3 6V11.5C3 12.3284 3.67157 13 4.5 13H11.5C12.3284 13 13 12.3284 13 11.5V6H3Z",
+ )
+}
///|
-let icon_arrow_up : String =
- #|
+fn icon_arrow_down() -> @html.Html {
+ codicon(
+ "M13.854 8.146C13.659 7.951 13.342 7.951 13.147 8.146L9.00096 12.292V2.5C9.00096 2.224 8.77696 2 8.50096 2C8.22496 2 8.00096 2.224 8.00096 2.5V12.293L3.85496 8.147C3.65996 7.952 3.34296 7.952 3.14796 8.147C2.95296 8.342 2.95296 8.659 3.14796 8.854L8.14796 13.854C8.24596 13.952 8.37396 14 8.50196 14C8.62996 14 8.75796 13.951 8.85596 13.854L13.856 8.854C14.051 8.659 14.051 8.342 13.856 8.147L13.854 8.146Z",
+ )
+}
///|
-let icon_chevron_down : String =
- #|
+fn icon_arrow_up() -> @html.Html {
+ codicon(
+ "M13.854 7.14576L8.85401 2.14576C8.65901 1.95076 8.34201 1.95076 8.14701 2.14576L3.14601 7.14576C2.95101 7.34076 2.95101 7.65776 3.14601 7.85276C3.34101 8.04776 3.65801 8.04776 3.85301 7.85276L7.99901 3.70676V13.4998C7.99901 13.7758 8.22301 13.9998 8.49901 13.9998C8.77501 13.9998 8.99901 13.7758 8.99901 13.4998V3.70676L13.145 7.85276C13.243 7.95076 13.371 7.99876 13.499 7.99876C13.627 7.99876 13.755 7.94976 13.853 7.85276C14.048 7.65776 14.048 7.34076 13.853 7.14576H13.854Z",
+ )
+}
///|
-let icon_close : String =
- #|
+fn icon_chevron_down() -> @html.Html {
+ codicon(
+ "M3.14598 5.85423L7.64598 10.3542C7.84098 10.5492 8.15798 10.5492 8.35298 10.3542L12.853 5.85423C13.048 5.65923 13.048 5.34223 12.853 5.14723C12.658 4.95223 12.341 4.95223 12.146 5.14723L7.99998 9.29323L3.85398 5.14723C3.65898 4.95223 3.34198 4.95223 3.14698 5.14723C2.95198 5.34223 2.95098 5.65923 3.14598 5.85423Z",
+ )
+}
///|
-let icon_debug_stop : String =
- #|
+fn icon_close() -> @html.Html {
+ codicon(
+ "M8.70701 8.00001L12.353 4.35401C12.548 4.15901 12.548 3.84201 12.353 3.64701C12.158 3.45201 11.841 3.45201 11.646 3.64701L8.00001 7.29301L4.35401 3.64701C4.15901 3.45201 3.84201 3.45201 3.64701 3.64701C3.45201 3.84201 3.45201 4.15901 3.64701 4.35401L7.29301 8.00001L3.64701 11.646C3.45201 11.841 3.45201 12.158 3.64701 12.353C3.74501 12.451 3.87301 12.499 4.00101 12.499C4.12901 12.499 4.25701 12.45 4.35501 12.353L8.00101 8.70701L11.647 12.353C11.745 12.451 11.873 12.499 12.001 12.499C12.129 12.499 12.257 12.45 12.355 12.353C12.55 12.158 12.55 11.841 12.355 11.646L8.70901 8.00001H8.70701Z",
+ )
+}
///|
-let icon_extensions : String =
- #|
+fn icon_debug_stop() -> @html.Html {
+ codicon(
+ "M12.5 3.5V12.5H3.5V3.5H12.5ZM12.5 2H3.5C2.672 2 2 2.672 2 3.5V12.5C2 13.328 2.672 14 3.5 14H12.5C13.328 14 14 13.328 14 12.5V3.5C14 2.672 13.328 2 12.5 2Z",
+ )
+}
+
+///|
+fn icon_extensions() -> @html.Html {
+ codicon(
+ "M15 4.95703C15 4.58711 14.8563 4.24054 14.5949 3.97992L12.0096 1.39234C11.4879 0.86922 10.5788 0.86922 10.0571 1.39234L8 3.45119V3.32321C8 2.55068 7.37187 1.922 6.6 1.922H2.4C1.62813 1.922 1 2.55068 1 3.32321V13.5988C1 14.3713 1.62813 15 2.4 15H12.6667C13.4385 15 14.0667 14.3713 14.0667 13.5988V9.39514C14.0667 8.62261 13.4385 7.99393 12.6667 7.99393H12.5379L14.5949 5.93508C14.8553 5.67445 15 5.32602 15 4.95703ZM2.4 2.85521H6.6C6.85667 2.85521 7.06667 3.06446 7.06667 3.32228V7.99299H1.93333V3.32228C1.93333 3.06446 2.14333 2.85521 2.4 2.85521ZM1.93333 13.5979V8.92714H7.06667V14.0649H2.4C2.14333 14.0649 1.93333 13.8547 1.93333 13.5979ZM13.1333 9.39421V13.5979C13.1333 13.8547 12.9233 14.0649 12.6667 14.0649H8V8.92714H12.6667C12.9233 8.92714 13.1333 9.13638 13.1333 9.39421ZM8 7.99299V6.46287L9.5288 7.99299H8ZM13.9351 5.2737L11.3488 7.86221C11.1789 8.03223 10.8859 8.03223 10.716 7.86221L8.12973 5.2737C8.0448 5.18963 7.99813 5.07753 7.99813 4.95796C7.99813 4.83839 8.0448 4.7263 8.12973 4.64129L10.716 2.05278C10.8009 1.96777 10.9129 1.92106 11.0324 1.92106C11.1519 1.92106 11.2639 1.96777 11.3488 2.05278L13.9351 4.64129C14.02 4.72536 14.0667 4.83746 14.0667 4.95703C14.0667 5.0766 14.02 5.1887 13.9351 5.2737Z",
+ )
+}
///|
/// Hand-drawn (not a codicon): a git branch — two commits on a trunk with
/// one forking off — for the workspace worktree affordance.
-let icon_worktree : String =
- #|
+fn icon_worktree() -> @html.Html {
+ @svg.svg(
+ width=16,
+ height=16,
+ view_box="0 0 16 16",
+ attrs=@svg.Attrs::build()
+ .fill("none")
+ .stroke("currentColor")
+ .stroke_width("1.3"),
+ [
+ svg_circle("4.5", "3.5", "1.6"),
+ svg_circle("4.5", "12.5", "1.6"),
+ svg_circle("11.5", "5.5", "1.6"),
+ @svg.path(d="M4.5 5.1v5.8M11.5 7.1c0 2.4-3.2 2.6-5.4 3.4"),
+ ],
+ )
+}
///|
/// Hand-drawn in the same style as `icon_worktree`: a pull request — a branch
/// line on the left, and on the right a line arriving through an arrow.
-let icon_pull_request : String =
- #|
+fn icon_pull_request() -> @html.Html {
+ @svg.svg(
+ width=16,
+ height=16,
+ view_box="0 0 16 16",
+ attrs=@svg.Attrs::build()
+ .fill("none")
+ .stroke("currentColor")
+ .stroke_width("1.3")
+ .stroke_linecap("round")
+ .stroke_linejoin("round"),
+ [
+ svg_circle("4.5", "3.5", "1.6"),
+ svg_circle("4.5", "12.5", "1.6"),
+ svg_circle("11.5", "12.5", "1.6"),
+ @svg.path(
+ d="M4.5 5.1v5.8M11.5 10.9V6.6a2 2 0 0 0-2-2H7.4M9.2 2.6 7.2 4.6l2 2",
+ ),
+ ],
+ )
+}
///|
-let icon_fold : String =
- #|
+fn icon_fold() -> @html.Html {
+ codicon(
+ "M11.8536 3.35355L8.35355 6.85355C8.15829 7.04882 7.84171 7.04882 7.64645 6.85355L4.14645 3.35355C3.95118 3.15829 3.95118 2.84171 4.14645 2.64645C4.34171 2.45118 4.65829 2.45118 4.85355 2.64645L8 5.79289L11.1464 2.64645C11.3417 2.45118 11.6583 2.45118 11.8536 2.64645C12.0488 2.84171 12.0488 3.15829 11.8536 3.35355ZM11.8536 12.6464L8.35355 9.14645C8.15829 8.95118 7.84171 8.95118 7.64645 9.14645L4.14645 12.6464C3.95118 12.8417 3.95118 13.1583 4.14645 13.3536C4.34171 13.5488 4.65829 13.5488 4.85355 13.3536L8 10.2071L11.1464 13.3536C11.3417 13.5488 11.6583 13.5488 11.8536 13.3536C12.0488 13.1583 12.0488 12.8417 11.8536 12.6464Z",
+ )
+}
///|
-let icon_folder : String =
- #|
+fn icon_folder() -> @html.Html {
+ codicon(
+ "M2 4.5V6H5.58579C5.71839 6 5.84557 5.94732 5.93934 5.85355L7.29289 4.5L5.93934 3.14645C5.84557 3.05268 5.71839 3 5.58579 3H3.5C2.67157 3 2 3.67157 2 4.5ZM1 4.5C1 3.11929 2.11929 2 3.5 2H5.58579C5.98361 2 6.36514 2.15804 6.64645 2.43934L8.20711 4H12.5C13.8807 4 15 5.11929 15 6.5V11.5C15 12.8807 13.8807 14 12.5 14H3.5C2.11929 14 1 12.8807 1 11.5V4.5ZM2 7V11.5C2 12.3284 2.67157 13 3.5 13H12.5C13.3284 13 14 12.3284 14 11.5V6.5C14 5.67157 13.3284 5 12.5 5H8.20711L6.64645 6.56066C6.36514 6.84197 5.98361 7 5.58579 7H2Z",
+ )
+}
///|
-let icon_key : String =
- #|
+fn icon_key() -> @html.Html {
+ codicon(
+ "M12 5C12 5.552 11.552 6 11 6C10.448 6 10 5.552 10 5C10 4.448 10.448 4 11 4C11.552 4 12 4.448 12 5ZM15 5.5C15 7.981 12.981 10 10.5 10H9.186L8.389 10.775C8.349 10.814 8.302 10.846 8.251 10.87L8.072 10.953C8.006 10.983 7.934 10.999 7.861 10.999H7V12.292C7 12.425 6.947 12.552 6.854 12.646L6.647 12.853C6.553 12.947 6.426 12.999 6.293 12.999H5V14.292C5 14.424 4.947 14.551 4.854 14.645L4.648 14.852C4.554 14.946 4.426 14.999 4.294 14.999H2C1.448 14.997 1 14.548 1 13.998V12.405C1 12.142 1.106 11.884 1.293 11.698L6.19 6.801C6.064 6.381 6 5.945 6 5.5C6 3.019 8.019 1 10.5 1C12.981 1 15 3.019 15 5.5ZM14 5.5C14 3.57 12.43 2 10.5 2C8.57 2 7 3.57 7 5.5C7 5.933 7.078 6.355 7.231 6.755C7.301 6.94 7.257 7.148 7.118 7.288L2 12.406V13.999H4V12.5C4 12.224 4.224 12 4.5 12H6V10.5C6 10.224 6.224 10 6.5 10H7.752L8.634 9.142C8.728 9.051 8.853 9 8.983 9H10.501C12.431 9 14.001 7.43 14.001 5.5H14Z",
+ )
+}
///|
-let icon_layout : String =
- #|
+fn icon_layout() -> @html.Html {
+ let evenodd = () => {
+ @svg.Attrs::build().fill_rule("evenodd").clip_rule("evenodd")
+ }
+ @svg.svg(
+ width=16,
+ height=16,
+ view_box="0 0 16 16",
+ attrs=@svg.Attrs::build().fill("currentColor"),
+ [
+ @svg.path(
+ attrs=evenodd(),
+ d="M5.5 1C6.327 1 7 1.673 7 2.5V13.5C7 14.327 6.327 15 5.5 15H2.5C1.673 15 1 14.327 1 13.5V2.5C1 1.673 1.673 1 2.5 1H5.5ZM2.5 2C2.225 2 2 2.225 2 2.5V13.5C2 13.775 2.225 14 2.5 14H5.5C5.775 14 6 13.775 6 13.5V2.5C6 2.225 5.775 2 5.5 2H2.5Z",
+ ),
+ @svg.path(
+ attrs=evenodd(),
+ d="M13.5 9C14.327 9 15 9.673 15 10.5V13.5C15 14.327 14.327 15 13.5 15H10.5C9.673 15 9 14.327 9 13.5V10.5C9 9.673 9.673 9 10.5 9H13.5ZM10.5 10C10.225 10 10 10.225 10 10.5V13.5C10 13.775 10.225 14 10.5 14H13.5C13.775 14 14 13.775 14 13.5V10.5C14 10.225 13.775 10 13.5 10H10.5Z",
+ ),
+ @svg.path(
+ attrs=evenodd(),
+ d="M13.5 1C14.327 1 15 1.673 15 2.5V5.5C15 6.327 14.327 7 13.5 7H10.5C9.673 7 9 6.327 9 5.5V2.5C9 1.673 9.673 1 10.5 1H13.5ZM10.5 2C10.225 2 10 2.225 10 2.5V5.5C10 5.775 10.225 6 10.5 6H13.5C13.775 6 14 5.775 14 5.5V2.5C14 2.225 13.775 2 13.5 2H10.5Z",
+ ),
+ ],
+ )
+}
///|
-let icon_layout_sidebar_left : String =
- #|
+fn icon_layout_sidebar_left() -> @html.Html {
+ codicon(
+ "M12.5 1C13.881 1 15 2.119 15 3.5V12.5C15 13.881 13.881 15 12.5 15H3.5C2.119 15 1 13.881 1 12.5V3.5C1 2.119 2.119 1 3.5 1H12.5ZM12.5 14C13.328 14 14 13.328 14 12.5V3.5C14 2.672 13.328 2 12.5 2H7V14H12.5Z",
+ )
+}
///|
-let icon_layout_sidebar_right : String =
- #|
+fn icon_layout_sidebar_right() -> @html.Html {
+ codicon(
+ "M12.5 1C13.881 1 15 2.119 15 3.5V12.5C15 13.881 13.881 15 12.5 15H3.5C2.119 15 1 13.881 1 12.5V3.5C1 2.119 2.119 1 3.5 1H12.5ZM9 14V2H3.5C2.672 2 2 2.672 2 3.5V12.5C2 13.328 2.672 14 3.5 14H9Z",
+ )
+}
///|
-let icon_link_external : String =
- #|
+fn icon_link_external() -> @html.Html {
+ codicon(
+ "M15 9.5V12.5C15 13.879 13.879 15 12.5 15H3.5C2.121 15 1 13.879 1 12.5V3.5C1 2.121 2.121 1 3.5 1H6.5C6.776 1 7 1.224 7 1.5C7 1.776 6.776 2 6.5 2H3.5C2.673 2 2 2.673 2 3.5V12.5C2 13.327 2.673 14 3.5 14H12.5C13.327 14 14 13.327 14 12.5V9.5C14 9.224 14.224 9 14.5 9C14.776 9 15 9.224 15 9.5ZM14.5 1H9.5C9.224 1 9 1.224 9 1.5C9 1.776 9.224 2 9.5 2H13.293L9.147 6.146C8.952 6.341 8.952 6.658 9.147 6.853C9.245 6.951 9.373 6.999 9.501 6.999C9.629 6.999 9.757 6.95 9.855 6.853L14.001 2.707V6.5C14.001 6.776 14.225 7 14.501 7C14.777 7 15.001 6.776 15.001 6.5V1.5C15.001 1.224 14.777 1 14.501 1H14.5Z",
+ )
+}
///|
-let icon_package : String =
- #|
+fn icon_package() -> @html.Html {
+ @svg.svg(
+ width=16,
+ height=16,
+ view_box="0 0 16 16",
+ attrs=@svg.Attrs::build().fill("currentColor"),
+ [
+ @svg.path(
+ d="M6 8.63998C5.934 8.63998 5.867 8.62698 5.803 8.59898L3.303 7.52898C3.049 7.42098 2.931 7.12698 3.04 6.87298C3.149 6.61998 3.444 6.50098 3.696 6.60998L6.196 7.67998C6.45 7.78798 6.568 8.08198 6.459 8.33598C6.378 8.52498 6.193 8.63898 6 8.63898V8.63998Z",
+ ),
+ @svg.path(
+ d="M4.80397 10.17C4.86797 10.197 4.93397 10.21 4.99997 10.21C5.19397 10.21 5.37897 10.096 5.45997 9.90599C5.56797 9.65199 5.44997 9.35799 5.19597 9.24999L3.69597 8.60999C3.44397 8.50199 3.14797 8.61999 3.03997 8.87399C2.93197 9.12799 3.04997 9.42199 3.30397 9.52999L4.80397 10.17Z",
+ ),
+ @svg.path(
+ attrs=@svg.Attrs::build().fill_rule("evenodd").clip_rule("evenodd"),
+ d="M14.039 3.28598L9.077 1.37798C8.384 1.11098 7.616 1.11098 6.923 1.37798L1.962 3.28598C1.383 3.50898 1 4.06498 1 4.68598V11.312C1 11.933 1.382 12.489 1.962 12.712L6.923 14.62C7.616 14.887 8.384 14.887 9.077 14.62L14.039 12.712C14.618 12.489 15 11.933 15 11.312V4.68598C15 4.06498 14.618 3.50898 14.039 3.28598ZM7.5 13.757C7.426 13.738 7.354 13.715 7.282 13.687L2.32 11.779C2.127 11.705 2 11.519 2 11.312V4.97098L7.5 7.32798V13.757ZM2.564 4.12598L7.282 2.31098C7.744 2.13298 8.256 2.13298 8.718 2.31098L13.436 4.12598L8 6.45598L2.564 4.12598ZM14 11.313C14 11.52 13.873 11.705 13.68 11.78L8.718 13.688C8.646 13.716 8.574 13.739 8.5 13.758V7.32998L14 4.97298V11.313Z",
+ ),
+ ],
+ )
+}
///|
-let icon_search : String =
- #|
+fn icon_search() -> @html.Html {
+ codicon(
+ "M10.0195 10.7266C9.06578 11.5217 7.83875 12 6.5 12C3.46243 12 1 9.53757 1 6.5C1 3.46243 3.46243 1 6.5 1C9.53757 1 12 3.46243 12 6.5C12 7.83875 11.5217 9.06578 10.7266 10.0195L13.8535 13.1464C14.0488 13.3417 14.0488 13.6583 13.8535 13.8536C13.6583 14.0488 13.3417 14.0488 13.1464 13.8536L10.0195 10.7266ZM11 6.5C11 4.01472 8.98528 2 6.5 2C4.01472 2 2 4.01472 2 6.5C2 8.98528 4.01472 11 6.5 11C8.98528 11 11 8.98528 11 6.5Z",
+ )
+}
///|
/// Not a codicon: the MoonBit brand mark from the design system (same artwork
/// as the packaged app icon), tinted via `currentColor` like the rest. Its
/// viewBox is taller than wide, so a square `.icon` span letterboxes it.
-let icon_logo : String =
- #|
+fn icon_logo() -> @html.Html {
+ @svg.svg(view_box="88.6 -6.0 375.3 568.4", [
+ @svg.g(fill="currentColor", [
+ @svg.path(
+ d="M306.79,350v-7.84l22.58-17.58v8.93l-10.29,7.29-6.9,5.1v.37l6.9,5,10.29,7.38v8.84Z",
+ ),
+ @svg.path(
+ d="M335.82,367.48v-8.84l10.29-7.38,6.9-5v-.37l-6.9-5.1-10.29-7.29v-8.93l22.58,17.58V350Z",
+ ),
+ @svg.path(
+ d="M194,344.67c0-13.81,5-20.52,12.89-20.52s12.89,6.77,12.89,20.52-5.09,20.8-12.89,20.8S194,358.54,194,344.67Zm19.82,0c0-11.42-2.87-15.59-6.93-15.59S200,333.25,200,344.67s2.87,15.87,6.93,15.87S213.86,356.21,213.86,344.67Z",
+ ),
+ @svg.path(
+ d="M227,359.62h8.72V331.89h-7.15v-3.95a24.61,24.61,0,0,0,8.67-3H242v34.71h7.8v5.14H227Z",
+ ),
+ @svg.path(
+ d="M321.4,462.05c16.17,13.55,25.71,30.28,30.71,49.63,1.06,4.06.69,8.66.16,12.91-.64,5.23-4.32,7.91-9.63,8-1.48,0-3,0-5.05,0,.5,7.92-1.75,15-8.13,19.19-4,2.67-9.57,4.86-14.19,4.46-10.45-.91-19.44-5.34-20.68-23.32H260.8c-.82,8.05-3.12,15.9-10.7,20.25-3.54,2-8.25,3.61-12.15,3.13-11-1.35-18.73-5.46-21.45-22.82-3.43-1-7.25-1.05-9.79-2.91-5.05-3.71-5.45-9.54-4.32-15.3,3.81-19.43,12.58-36.17,27.34-49.58.72-.65,1.33-1.42,2.22-2.38a9.61,9.61,0,0,0-2-1.55c-11-4.07-22.49-7.2-33-12.32-21.34-10.42-38.56-25.84-49.56-47.19-8.67-16.82-11-34.82-8.48-53.7,2-15.29,6.71-29.4,15.34-42.05,11.56-16.94,27.45-28.64,46.36-36.27,6-2.43,12.21-4.48,19.15-7L163.54,151.4c-4.67,6.14-8.06,11.12-12,15.64-8.15,9.39-16.79,18.18-29.73,20.93a17.74,17.74,0,0,1-19.06-8.71c-4.93-8.46-6.71-17.8-7.56-27.26-2.36-26.25,2.24-51.67,11-76.27,8.33-23.34,20.1-44.77,39.23-61.28,20.65-17.82,51.66-19.76,72.2-1.58,14.89,13.19,22.91,29.91,25.91,49.33,2.58,16.65,5.34,33.28,7.83,50,1.87,12.48,3.34,25,5.17,37.5s4,24.88,5.82,37.35c2.15,14.59,4.11,29.21,6.15,43.81,1,7.31,1.91,14.63,3.08,21.91.87,5.47,5.26,2.94,9,3.58,1.13-6.82,2.29-13.52,3.32-20.24q2.94-19.19,5.73-38.37c1.82-12.49,3.46-25,5.3-37.48,2.13-14.41,4.42-28.79,6.59-43.2,1.51-10,2.86-20.11,4.39-30.15,2.8-18.4,3.94-37.07,13.54-53.95S342.09,3.74,361.68.7a54.21,54.21,0,0,1,42.8,11.22C421.69,25.3,432.91,43,441.71,62.7A190.33,190.33,0,0,1,457,116.28c1.6,12.51.63,25.37.37,38.07a49.69,49.69,0,0,1-7.69,26.07c-5.1,8-14.28,10.22-23.3,6.29-12.56-5.46-21.4-15.18-29.49-25.74-2.19-2.85-4.23-5.8-7.07-9.72L333.51,262.53c5.19,1.78,9.47,3.22,13.72,4.72,22,7.75,40.42,20.65,53.62,40,8.11,11.91,12.74,25.38,14.6,39.79,1.93,14.92,1.69,29.57-4,43.75a98,98,0,0,1-18.48,30c-16.4,18-36.81,29.5-59.36,37.67C329.79,459.91,325.79,460.8,321.4,462.05ZM277.63,302.76c-24.8,0-49.6.11-74.39,0s-44.8,20.94-45.52,45.22c-.56,18.58,7.36,33.38,23.42,42.62,9.08,5.22,18.85,8.55,30.27,4,21.13-8.34,43.27-13.25,66.11-13.32a126.72,126.72,0,0,1,26.37,3c13.07,2.76,26,6.32,38.85,10,5.36,1.54,10.22,3,16.15,2,27.29-4.58,44.11-31.91,37.66-58.15-5.12-20.85-25.05-36.1-46-35.55C326.24,303.26,301.92,302.76,277.63,302.76Zm46.19,212.52,1.17-1.09-13.57-30.81-1.69.65a7.12,7.12,0,0,0,.19,1.75c3.51,8.92,7,17.86,10.67,26.7C321.08,513.66,322.71,514.36,323.82,515.28Zm-94-.44c5.1-2,15.31-24.9,13.52-31.18Z",
+ ),
+ ]),
+ ])
+}
///|
-let icon_settings_gear : String =
- #|
+fn icon_settings_gear() -> @html.Html {
+ @svg.svg(
+ width=24,
+ height=24,
+ view_box="0 0 24 24",
+ attrs=@svg.Attrs::build().fill("currentColor"),
+ [
+ @svg.path(
+ d="M12 9C10.3425 9 9.00002 10.3425 9.00002 12C9.00002 13.6575 10.3425 15 12 15C13.6575 15 15 13.6575 15 12C15 10.3425 13.6575 9 12 9ZM12 13.5C11.172 13.5 10.5 12.828 10.5 12C10.5 11.172 11.172 10.5 12 10.5C12.828 10.5 13.5 11.172 13.5 12C13.5 12.828 12.828 13.5 12 13.5ZM21.8475 14.5725L19.9185 12.942C19.8675 12.8985 19.8195 12.8505 19.776 12.7995C19.332 12.279 19.3965 11.5005 19.9185 11.058L21.8475 9.4275C22.0395 9.2655 22.113 9.0045 22.0365 8.766C21.579 7.3545 20.823 6.06 19.8285 4.962C19.7085 4.83 19.5405 4.758 19.368 4.758C19.2975 4.758 19.227 4.77 19.1595 4.794L16.779 5.6415C16.716 5.664 16.65 5.682 16.584 5.694C16.509 5.7075 16.434 5.715 16.3605 5.715C15.7725 5.715 15.2505 5.298 15.141 4.701L14.6865 2.223C14.6415 1.977 14.451 1.782 14.205 1.7295C13.485 1.5765 12.7485 1.5 12.0015 1.5C11.2545 1.5 10.5165 1.578 9.79652 1.7295C9.55052 1.782 9.36002 1.977 9.31502 2.223L8.86202 4.701C8.85002 4.767 8.83202 4.8315 8.80952 4.8945C8.62802 5.4 8.15102 5.715 7.64102 5.715C7.50302 5.715 7.36202 5.691 7.22402 5.643L4.84352 4.7955C4.77602 4.7715 4.70402 4.7595 4.63502 4.7595C4.46252 4.7595 4.29452 4.8315 4.17452 4.9635C3.17852 6.0615 2.42402 7.356 1.96502 8.7675C1.88702 9.006 1.96202 9.267 2.15402 9.429L4.08302 11.0595C4.13402 11.103 4.18202 11.151 4.22552 11.202C4.66952 11.7225 4.60502 12.501 4.08302 12.9435L2.15402 14.574C1.96202 14.736 1.88852 14.997 1.96502 15.2355C2.42252 16.647 3.17852 17.9415 4.17452 19.0395C4.29452 19.1715 4.46252 19.2435 4.63502 19.2435C4.70552 19.2435 4.77602 19.2315 4.84352 19.2075L7.22402 18.36C7.28702 18.3375 7.35302 18.3195 7.41902 18.3075C7.49402 18.294 7.56902 18.288 7.64252 18.288C8.23052 18.288 8.75252 18.705 8.86202 19.302L9.31502 21.78C9.36002 22.026 9.55052 22.221 9.79652 22.2735C10.5165 22.4265 11.2545 22.503 12.0015 22.503C12.7485 22.503 13.4865 22.425 14.205 22.2735C14.451 22.221 14.6415 22.026 14.6865 21.78L15.141 19.302C15.153 19.236 15.171 19.1715 15.1935 19.1085C15.375 18.603 15.852 18.288 16.362 18.288C16.5 18.288 16.641 18.312 16.779 18.36L19.158 19.2075C19.227 19.2315 19.2975 19.2435 19.3665 19.2435C19.539 19.2435 19.707 19.1715 19.827 19.0395C20.823 17.9415 21.5775 16.647 22.035 15.2355C22.113 14.997 22.038 14.736 21.846 14.574L21.8475 14.5725ZM19.092 17.589L17.2815 16.944C16.9845 16.839 16.6755 16.785 16.362 16.785C15.2085 16.785 14.1705 17.514 13.782 18.5985C13.731 18.738 13.6935 18.882 13.6665 19.029L13.3215 20.9055C12.8865 20.9685 12.444 21 12.0015 21C11.559 21 11.1165 20.9685 10.68 20.904L10.3365 19.0275C10.098 17.727 8.96552 16.7835 7.64252 16.7835C7.48052 16.7835 7.31552 16.7985 7.14902 16.8285C7.00352 16.8555 6.86102 16.893 6.72002 16.9425L4.90952 17.5875C4.35752 16.896 3.91652 16.1385 3.59102 15.321L5.05202 14.0865C5.61152 13.614 5.95202 12.951 6.01202 12.222C6.07202 11.493 5.84252 10.785 5.36702 10.227C5.27102 10.1145 5.16452 10.008 5.05202 9.912L3.59102 8.6775C3.91652 7.86 4.35752 7.101 4.90952 6.411L6.72002 7.056C7.01702 7.161 7.32602 7.215 7.64102 7.215C8.79452 7.215 9.83252 6.486 10.221 5.4015C10.272 5.2605 10.3095 5.1165 10.3365 4.971L10.68 3.0945C11.1165 3.0315 11.559 2.9985 12.0015 2.9985C12.444 2.9985 12.8865 3.03 13.3215 3.093L13.665 4.9695C13.9035 6.27 15.036 7.2135 16.359 7.2135C16.521 7.2135 16.686 7.1985 16.851 7.1685C16.9965 7.1415 17.1405 7.104 17.2815 7.0545L19.092 6.4095C19.644 7.0995 20.085 7.8585 20.4105 8.676L18.951 9.9105C18.3915 10.383 18.0495 11.046 17.991 11.775C17.931 12.504 18.1605 13.2135 18.636 13.77C18.7335 13.884 18.8385 13.989 18.9525 14.085L20.4135 15.3195C20.088 16.137 19.647 16.896 19.095 17.586L19.092 17.589Z",
+ ),
+ ],
+ )
+}
///|
-let icon_sparkle : String =
- #|
+fn icon_sparkle() -> @html.Html {
+ codicon(
+ "M5.46524 9.82962C5.62134 9.94037 5.80806 9.99974 5.99946 9.99948C6.19151 10.0003 6.37897 9.94082 6.53546 9.82948C6.69223 9.71378 6.81095 9.55398 6.87646 9.37048L7.22346 8.30348C7.3077 8.05191 7.44906 7.82327 7.63646 7.63548C7.82305 7.44851 8.05078 7.30776 8.30146 7.22448L9.38746 6.87148C9.56665 6.80759 9.72173 6.68989 9.83146 6.53448C9.94145 6.37908 10.0005 6.19337 10.0005 6.00298C10.0005 5.81259 9.94145 5.62689 9.83146 5.47148C9.71293 5.30613 9.54426 5.18339 9.35046 5.12148L8.28146 4.77548C8.02989 4.69238 7.80123 4.55163 7.61371 4.36447C7.4262 4.1773 7.28503 3.9489 7.20146 3.69748L6.84846 2.61348C6.78519 2.43423 6.66777 2.27908 6.51246 2.16948C6.35557 2.06133 6.16951 2.00342 5.97896 2.00342C5.78841 2.00342 5.60235 2.06133 5.44546 2.16948C5.28572 2.28196 5.16594 2.44237 5.10346 2.62748L4.74846 3.71748C4.66476 3.96155 4.52691 4.18351 4.34524 4.36673C4.16358 4.54996 3.9428 4.6897 3.69946 4.77548L2.61546 5.12648C2.43437 5.19048 2.27775 5.30937 2.16743 5.4666C2.05712 5.62383 1.99859 5.81155 2.00003 6.00361C2.00146 6.19568 2.06277 6.38251 2.17541 6.53808C2.28806 6.69364 2.44643 6.81019 2.62846 6.87148L3.69546 7.21848C3.94767 7.30297 4.17673 7.44506 4.36446 7.63348C4.41519 7.6837 4.46262 7.73715 4.50646 7.79348C4.62481 7.94615 4.71614 8.11797 4.77646 8.30148L5.12846 9.38148C5.19143 9.56222 5.30914 9.71886 5.46524 9.82962ZM4.00746 6.26448L3.15246 5.99948L4.01646 5.71848C4.41071 5.58184 4.76826 5.35637 5.06146 5.05948C5.35281 4.76039 5.57294 4.39943 5.70546 4.00348L5.97046 3.14448L6.25046 4.00648C6.38349 4.40638 6.60809 4.76969 6.90636 5.06744C7.20463 5.36519 7.56833 5.58915 7.96846 5.72148L8.84846 5.99048L7.98746 6.27048C7.58707 6.40272 7.22321 6.62691 6.92505 6.92507C6.62689 7.22324 6.4027 7.58709 6.27046 7.98748L6.00546 8.84448L5.72646 7.98548C5.63026 7.69329 5.48483 7.41968 5.29646 7.17648C5.22699 7.08766 5.15254 7.00286 5.07346 6.92248C4.7738 6.62366 4.4089 6.39842 4.00746 6.26448ZM10.5344 13.8515C10.6703 13.9477 10.8328 13.9994 10.9994 13.9995C11.1642 13.998 11.3245 13.9456 11.4584 13.8495C11.5979 13.751 11.7029 13.611 11.7584 13.4495L12.0064 12.6875C12.0595 12.529 12.1485 12.385 12.2664 12.2665C12.3837 12.148 12.5277 12.0592 12.6864 12.0075L13.4584 11.7555C13.6161 11.701 13.7528 11.5985 13.8494 11.4625C13.9227 11.3595 13.9706 11.2405 13.9891 11.1154C14.0076 10.9903 13.9962 10.8626 13.9558 10.7428C13.9154 10.623 13.8472 10.5144 13.7567 10.4261C13.6662 10.3377 13.5561 10.272 13.4354 10.2345L12.6714 9.98548C12.5132 9.93291 12.3695 9.8443 12.2514 9.72663C12.1334 9.60896 12.0444 9.46547 11.9914 9.30748L11.7394 8.53348C11.685 8.37623 11.5825 8.24011 11.4464 8.14448C11.3443 8.07153 11.2266 8.02359 11.1026 8.00453C10.9787 7.98547 10.8519 7.99582 10.7327 8.03475C10.6135 8.07369 10.5051 8.1401 10.4163 8.22865C10.3274 8.31719 10.2607 8.42538 10.2214 8.54448L9.97435 9.30648C9.92207 9.46413 9.83452 9.60777 9.71835 9.72648C9.60382 9.84272 9.46428 9.9313 9.31035 9.98548L8.53435 10.2385C8.41689 10.2793 8.31057 10.347 8.22382 10.4361C8.13708 10.5252 8.0723 10.6333 8.03464 10.7518C7.99698 10.8704 7.98746 10.996 8.00686 11.1189C8.02625 11.2417 8.07401 11.3583 8.14635 11.4595C8.24456 11.5993 8.38462 11.7044 8.54635 11.7595L9.30935 12.0065C9.46821 12.0599 9.61262 12.1492 9.73135 12.2675C9.84958 12.3857 9.93801 12.5304 9.98935 12.6895L10.2424 13.4635C10.2971 13.6199 10.3992 13.7555 10.5344 13.8515ZM9.62035 11.0585L9.44235 10.9995L9.62635 10.9355C9.92811 10.8305 10.2018 10.6578 10.4264 10.4305C10.6528 10.2015 10.8238 9.92374 10.9264 9.61848L10.9844 9.44048L11.0434 9.62148C11.1453 9.92819 11.3175 10.2069 11.5461 10.4353C11.7748 10.6638 12.0536 10.8357 12.3604 10.9375L12.5554 11.0005L12.3754 11.0595C12.068 11.1617 11.7888 11.3344 11.5601 11.5637C11.3314 11.7931 11.1596 12.0728 11.0584 12.3805L10.9994 12.5615L10.9414 12.3805C10.84 12.0721 10.6676 11.7919 10.4382 11.5623C10.2088 11.3326 9.92863 11.1601 9.62035 11.0585Z",
+ )
+}
///|
-let icon_sync : String =
- #|
+fn icon_sync() -> @html.Html {
+ codicon(
+ "M14 3.5V6.5C14 6.78 13.78 7 13.5 7H10.5C10.22 7 9.99999 6.78 9.99999 6.5C9.99999 6.22 10.22 6 10.5 6H12.58C11.78 4.17 10.01 3 7.99999 3C5.77999 3 3.79999 4.5 3.18999 6.64C3.12999 6.86 2.92999 7 2.70999 7C2.65999 7 2.61999 7 2.56999 6.98C2.29999 6.9 2.14999 6.63 2.22999 6.36C2.95999 3.79 5.32999 2 7.99999 2C10.05 2 11.91 3.02 13 4.69V3.5C13 3.22 13.22 3 13.5 3C13.78 3 14 3.22 14 3.5ZM13.42 9.02C13.16 8.95 12.88 9.1 12.8 9.37C12.19 11.51 10.22 13.01 7.98999 13.01C5.97999 13.01 4.20999 11.84 3.40999 10.01H5.48999C5.76999 10.01 5.98999 9.79 5.98999 9.51C5.98999 9.23 5.76999 9.01 5.48999 9.01H2.48999C2.20999 9.01 1.98999 9.23 1.98999 9.51V12.51C1.98999 12.79 2.20999 13.01 2.48999 13.01C2.76999 13.01 2.98999 12.79 2.98999 12.51V11.32C4.07999 12.98 5.93999 14.01 7.98999 14.01C10.66 14.01 13.03 12.22 13.76 9.65C13.84 9.38 13.68 9.11 13.41 9.03L13.42 9.02Z",
+ )
+}
///|
-let icon_terminal : String =
- #|
+fn icon_terminal() -> @html.Html {
+ @svg.svg(
+ width=24,
+ height=24,
+ view_box="0 0 24 24",
+ attrs=@svg.Attrs::build().fill("currentColor"),
+ [
+ @svg.path(
+ d="M18.75 1.5H5.25C3.1815 1.5 1.5 3.183 1.5 5.25V18.75C1.5 20.8185 3.1815 22.5 5.25 22.5H18.75C20.8185 22.5 22.5 20.8185 22.5 18.75V5.25C22.5 3.183 20.8185 1.5 18.75 1.5ZM21 18.75C21 19.9905 19.9905 21 18.75 21H5.25C4.0095 21 3 19.9905 3 18.75V5.25C3 4.0095 4.0095 3 5.25 3H18.75C19.9905 3 21 4.0095 21 5.25V18.75ZM10.281 13.281L5.781 17.781C5.634 17.928 5.442 18 5.25 18C5.058 18 4.866 17.9265 4.719 17.781C4.4265 17.4885 4.4265 17.013 4.719 16.7205L8.688 12.7515L4.719 8.7825C4.4265 8.49 4.4265 8.0145 4.719 7.722C5.0115 7.4295 5.487 7.4295 5.7795 7.722L10.2795 12.222C10.572 12.5145 10.572 12.99 10.2795 13.2825L10.281 13.281ZM19.5 17.25C19.5 17.664 19.164 18 18.75 18H11.25C10.836 18 10.5 17.664 10.5 17.25C10.5 16.836 10.836 16.5 11.25 16.5H18.75C19.164 16.5 19.5 16.836 19.5 17.25Z",
+ ),
+ ],
+ )
+}
diff --git a/desktop/frontend/moon.pkg b/desktop/frontend/moon.pkg
index 5a167a946..16e5fd345 100644
--- a/desktop/frontend/moon.pkg
+++ b/desktop/frontend/moon.pkg
@@ -10,6 +10,7 @@ import {
"moonbit-community/rabbita/html",
"moonbit-community/rabbita/js",
"moonbit-community/rabbita/sub",
+ "moonbit-community/rabbita/svg",
"moonbitlang/editor/base/common",
"openseek_desktop/internal/jsonx",
"openseek_desktop/frontend/pathx",
diff --git a/desktop/frontend/preview/moon.pkg b/desktop/frontend/preview/moon.pkg
index 4eb8ae923..0779852c0 100644
--- a/desktop/frontend/preview/moon.pkg
+++ b/desktop/frontend/preview/moon.pkg
@@ -1,6 +1,7 @@
import {
"moonbit-community/rabbita/cmd",
"moonbit-community/rabbita/html",
+ "moonbit-community/rabbita/svg",
}
supported_targets = "js"
diff --git a/desktop/frontend/preview/view.mbt b/desktop/frontend/preview/view.mbt
index 6a95045d9..65f4ba8b3 100644
--- a/desktop/frontend/preview/view.mbt
+++ b/desktop/frontend/preview/view.mbt
@@ -8,37 +8,55 @@
pub const AddressInputId = "browser-address-input"
///|
-/// The up arrow codicon from Microsoft's vscode-codicons (CC BY 4.0),
-/// embedded unmodified; the toolbar's back/forward rotate it by CSS so the
-/// history buttons keep the exact codicon geometry.
-let icon_arrow_up : String =
- #|
+/// The up arrow codicon from Microsoft's vscode-codicons (CC BY 4.0); the
+/// path data is unmodified, transcribed into `@svg` builders (an
+/// innerHTML-bearing element cannot be safely morphed by the positional
+/// child diff). The toolbar's back/forward rotate it by CSS so the history
+/// buttons keep the exact codicon geometry.
+fn icon_arrow_up() -> @html.Html {
+ codicon(
+ "M13.854 7.14576L8.85401 2.14576C8.65901 1.95076 8.34201 1.95076 8.14701 2.14576L3.14601 7.14576C2.95101 7.34076 2.95101 7.65776 3.14601 7.85276C3.34101 8.04776 3.65801 8.04776 3.85301 7.85276L7.99901 3.70676V13.4998C7.99901 13.7758 8.22301 13.9998 8.49901 13.9998C8.77501 13.9998 8.99901 13.7758 8.99901 13.4998V3.70676L13.145 7.85276C13.243 7.95076 13.371 7.99876 13.499 7.99876C13.627 7.99876 13.755 7.94976 13.853 7.85276C14.048 7.65776 14.048 7.34076 13.853 7.14576H13.854Z",
+ )
+}
///|
/// The sync codicon (CC BY 4.0), the reload button.
-let icon_sync : String =
- #|
+fn icon_sync() -> @html.Html {
+ codicon(
+ "M14 3.5V6.5C14 6.78 13.78 7 13.5 7H10.5C10.22 7 9.99999 6.78 9.99999 6.5C9.99999 6.22 10.22 6 10.5 6H12.58C11.78 4.17 10.01 3 7.99999 3C5.77999 3 3.79999 4.5 3.18999 6.64C3.12999 6.86 2.92999 7 2.70999 7C2.65999 7 2.61999 7 2.56999 6.98C2.29999 6.9 2.14999 6.63 2.22999 6.36C2.95999 3.79 5.32999 2 7.99999 2C10.05 2 11.91 3.02 13 4.69V3.5C13 3.22 13.22 3 13.5 3C13.78 3 14 3.22 14 3.5ZM13.42 9.02C13.16 8.95 12.88 9.1 12.8 9.37C12.19 11.51 10.22 13.01 7.98999 13.01C5.97999 13.01 4.20999 11.84 3.40999 10.01H5.48999C5.76999 10.01 5.98999 9.79 5.98999 9.51C5.98999 9.23 5.76999 9.01 5.48999 9.01H2.48999C2.20999 9.01 1.98999 9.23 1.98999 9.51V12.51C1.98999 12.79 2.20999 13.01 2.48999 13.01C2.76999 13.01 2.98999 12.79 2.98999 12.51V11.32C4.07999 12.98 5.93999 14.01 7.98999 14.01C10.66 14.01 13.03 12.22 13.76 9.65C13.84 9.38 13.68 9.11 13.41 9.03L13.42 9.02Z",
+ )
+}
///|
/// The link-external codicon (CC BY 4.0), the open-in-system-browser button.
-let icon_link_external : String =
- #|
+fn icon_link_external() -> @html.Html {
+ codicon(
+ "M15 9.5V12.5C15 13.879 13.879 15 12.5 15H3.5C2.121 15 1 13.879 1 12.5V3.5C1 2.121 2.121 1 3.5 1H6.5C6.776 1 7 1.224 7 1.5C7 1.776 6.776 2 6.5 2H3.5C2.673 2 2 2.673 2 3.5V12.5C2 13.327 2.673 14 3.5 14H12.5C13.327 14 14 13.327 14 12.5V9.5C14 9.224 14.224 9 14.5 9C14.776 9 15 9.224 15 9.5ZM14.5 1H9.5C9.224 1 9 1.224 9 1.5C9 1.776 9.224 2 9.5 2H13.293L9.147 6.146C8.952 6.341 8.952 6.658 9.147 6.853C9.245 6.951 9.373 6.999 9.501 6.999C9.629 6.999 9.757 6.95 9.855 6.853L14.001 2.707V6.5C14.001 6.776 14.225 7 14.501 7C14.777 7 15.001 6.776 15.001 6.5V1.5C15.001 1.224 14.777 1 14.501 1H14.5Z",
+ )
+}
///|
-/// An icon's SVG markup injected into a fixed-size span. The markup is a
-/// trusted compile-time constant, never user input, so the XSS alert on
-/// `inner_html` does not apply.
-#warnings("-alert_xss_vulnerable")
-fn icon(markup : String, rotate? : String) -> @html.Html {
+/// A 16×16 codicon: one filled path tinted by `currentColor`.
+fn codicon(d : String) -> @html.Html {
+ @svg.svg(
+ width=16,
+ height=16,
+ view_box="0 0 16 16",
+ attrs=@svg.Attrs::build().fill("currentColor"),
+ [@svg.path(d~)],
+ )
+}
+
+///|
+/// An icon in a fixed-size span. Icons are functions, not shared constants:
+/// each render site gets its own virtual-DOM value, so no Props instance is
+/// ever aliased between two mounted elements.
+fn icon(image : () -> @html.Html, rotate? : String) -> @html.Html {
let class = match rotate {
Some(direction) => "icon rotate-\{direction}"
None => "icon"
}
- @html.span(
- class~,
- attrs=@html.Attrs::build().inner_html(markup).aria_hidden("true"),
- ([] : Array[@html.Html]),
- )
+ @html.span(class~, attrs=@html.Attrs::build().aria_hidden("true"), [image()])
}
///|
diff --git a/desktop/frontend/update.mbt b/desktop/frontend/update.mbt
index 09d9c21b4..d451290f0 100644
--- a/desktop/frontend/update.mbt
+++ b/desktop/frontend/update.mbt
@@ -3682,8 +3682,28 @@ fn update_device_msg(
},
)
}
- ArchiveFailed(message) =>
- model.notify_error(dispatch, "Archive failed: \{message}")
+ ArchiveFailed(message) => {
+ // A definitive refusal usually means this page no longer matches the
+ // host's stores — the record moved, or its workspace detached while
+ // this sidebar still showed it. Re-adopt the registry snapshot (which
+ // prunes detached conversations and refreshes the live list) and the
+ // archived list, so the state that made the click possible heals
+ // instead of failing identically forever.
+ let workspaces_request_generation = dev.workspaces_request_generation + 1
+ let refresh = fetch_workspaces(
+ dispatch,
+ channel,
+ dev.connection_generation,
+ workspaces_request_generation,
+ )
+ let model = model.replace_device({ ..dev, workspaces_request_generation, })
+ let (archived, model) = request_archived_refresh(dispatch, channel, model)
+ let (toast, model) = model.notify_error(
+ dispatch,
+ "Archive failed: \{message}",
+ )
+ (@cmd.batch([refresh, archived, toast]), model)
+ }
WorktreeFailed(message) =>
model.notify_error(dispatch, "Worktree error: \{message}")
SessionsFailed(message) =>
diff --git a/desktop/frontend/view.mbt b/desktop/frontend/view.mbt
index db411769a..911034c1a 100644
--- a/desktop/frontend/view.mbt
+++ b/desktop/frontend/view.mbt
@@ -58,12 +58,12 @@ fn setting_row(
/// surface, the design system's settings section. Shared with the Skills
/// page so the two pages read as siblings.
fn settings_group(
- icon_markup : String,
+ icon_image : () -> @html.Html,
title : String,
rows : Array[@html.Html],
) -> @html.Html {
@html.div(class="settings-group", [
- @html.div(class="settings-group-head", [icon(icon_markup), @html.h2(title)]),
+ @html.div(class="settings-group-head", [icon(icon_image), @html.h2(title)]),
@html.div(class="settings-group-card", rows),
])
}
diff --git a/desktop/internal/engine/api.mbt b/desktop/internal/engine/api.mbt
index 38658c3bf..4882db543 100644
--- a/desktop/internal/engine/api.mbt
+++ b/desktop/internal/engine/api.mbt
@@ -1,7 +1,10 @@
///|
+/// Debug is what the host boundary logs: proton renders a failed handler's
+/// error through `Repr`, which prints only an opaque `<...: ...>` placeholder
+/// for types without it — swallowing the one string that says what failed.
pub(all) suberror EngineError {
EngineError(String)
-}
+} derive(Debug)
///|
impl Show for EngineError with fn output(self, logger) {
diff --git a/desktop/internal/engine/archive.mbt b/desktop/internal/engine/archive.mbt
index 724a61350..fe50bb617 100644
--- a/desktop/internal/engine/archive.mbt
+++ b/desktop/internal/engine/archive.mbt
@@ -21,17 +21,19 @@ fn archived_session_root(root : @pathx.Absolute) -> @pathx.Absolute {
}
///|
-/// Every store that can own a session id. Correlation and stale-client
-/// defenses must search all of them: request hints are optional and cannot be
-/// trusted to identify where an existing record lives.
+/// Every store that can own a session id, ordered so a workspace-based record
+/// is found before a global fallback — matching the old `session_store_root`
+/// priority. Correlation and stale-client defenses must search all of them:
+/// request hints are optional and cannot be trusted to identify where an
+/// existing record lives.
async fn known_session_roots() -> Array[@pathx.Absolute] {
let roots : Array[@pathx.Absolute] = []
- if resolved_session_root() is Some(root) {
- roots.push(root)
- }
for path in registered_workspaces() {
roots.push(workspace_session_root(path))
}
+ if resolved_session_root() is Some(root) {
+ roots.push(root)
+ }
roots
}
@@ -48,6 +50,22 @@ async fn archived_record_root(session : String) -> @pathx.Absolute? {
None
}
+///|
+/// The store root whose live `sessions/` holds this conversation's
+/// durable record, searched like `archived_record_root`. Archive must find
+/// the record where it actually lives: a stale client can name a
+/// conversation long after its workspace detached, and the session id alone
+/// says nothing about which store owns it.
+async fn live_record_root(session : String) -> @pathx.Absolute? {
+ guard !session.is_empty() else { return None }
+ for root in known_session_roots() {
+ if @fsx.is_dir(root.join("sessions").join(session).to_path()) {
+ return Some(root)
+ }
+ }
+ None
+}
+
///|
async fn ensure_session_not_archived(session : String) -> Unit {
if archived_record_root(session) is Some(_) {
@@ -199,9 +217,21 @@ async fn archive_session_commit(
if refusal is Some(refusal) {
return Some(refusal)
}
- guard session_store_root(session) is Some(root) else {
+ guard live_record_root(session) is Some(root) else {
+ // Name the state the caller can act on. A record already sitting in an
+ // archived twin is not "missing" — the stale client just resyncs; and a
+ // conversation whose workspace detached needs that workspace re-attached,
+ // not a hunt for a lost record.
+ if archived_record_root(session) is Some(_) {
+ raise EngineError("this conversation is already archived")
+ }
+ guard resolved_session_root() is Some(_) else {
+ raise EngineError(
+ "cannot determine a session root; set OPENSEEK_SESSION_ROOT",
+ )
+ }
raise EngineError(
- "cannot determine a session root; set OPENSEEK_SESSION_ROOT",
+ "no durable record for this conversation in the global store or any attached workspace",
)
}
// The rename and notification are the commit. Cancellation is delayed only
diff --git a/desktop/internal/engine/archive_lookup_wbtest.mbt b/desktop/internal/engine/archive_lookup_wbtest.mbt
new file mode 100644
index 000000000..8bf792d1c
--- /dev/null
+++ b/desktop/internal/engine/archive_lookup_wbtest.mbt
@@ -0,0 +1,182 @@
+// Archive's record lookup against stores a stale client can still name: a
+// conversation row can outlive its workspace's registration (detach) or its
+// directory (external deletion), and a second archive click can race the
+// first one's commit. Each state must answer with its own actionable
+// refusal, and a failed attempt must leave no wedged host state behind.
+
+///|
+#cfg(not(platform="windows"))
+async test "archive names the missing store after a workspace detach" {
+ archive_env_test_lock.acquire()
+ defer archive_env_test_lock.release()
+ let previous = @sys.get_env_var("OPENSEEK_SESSION_ROOT")
+ defer restore_session_root_env(previous)
+ let dir = @fs.tmpdir(prefix="openseek-archive-detach-")
+ let workspace = dir + "/ws"
+ let global_root = dir + "/global"
+ @sys.set_env_var("OPENSEEK_SESSION_ROOT", global_root)
+ @fsx.ensure_dir(Path(global_root))
+ @fsx.ensure_dir(Path(workspace + "/.openseek/sessions/s-ws"))
+ @fs.write_file(
+ global_root + "/workspaces.json",
+ ({ "workspaces": [workspace] } : Json).stringify(),
+ create_mode=CreateOrTruncate,
+ )
+ let manager = new_engine_manager("unused")
+ manager.pump = Serving(sessions=Map([]))
+ // The sidebar's remove button: detach commits and the registry forgets
+ // the workspace, while the record stays on disk in the project store.
+ let detached = detach_workspace(manager, workspace, fn(_) { })
+ assert_eq(detached.workspaces.length(), 0)
+ // A stale sidebar can still offer the conversation; archiving it now
+ // reports the record as unreachable instead of pretending it never
+ // existed.
+ let detail = try {
+ ignore(archive_session(manager, "s-ws", fn() { }))
+ "no error"
+ } catch {
+ EngineError(detail) => detail
+ error if @async.is_being_cancelled() => raise error
+ error => "\{error}"
+ }
+ debug_inspect(
+ detail,
+ content=(
+ #|"no durable record for this conversation in the global store or any attached workspace"
+ ),
+ )
+ assert_true(@fsx.is_dir(Path(workspace + "/.openseek/sessions/s-ws")))
+ // The failed attempt must leave no wedged host state behind: no record
+ // move claim, no pending slot, and the next attempt reports the same
+ // refusal instead of "already moving".
+ assert_true(manager.record_guards.get("s-ws") is None)
+ guard manager.pump is Serving(sessions~) else { fail("pump stopped") }
+ assert_true(sessions.get("s-ws") is None)
+ let second = try {
+ ignore(archive_session(manager, "s-ws", fn() { }))
+ "no error"
+ } catch {
+ EngineError(detail) => detail
+ error if @async.is_being_cancelled() => raise error
+ error => "\{error}"
+ }
+ debug_inspect(
+ second,
+ content=(
+ #|"no durable record for this conversation in the global store or any attached workspace"
+ ),
+ )
+ @fs.rmdir(dir, recursive=true)
+}
+
+///|
+#cfg(not(platform="windows"))
+async test "archive reports a workspace record whose directory vanished" {
+ archive_env_test_lock.acquire()
+ defer archive_env_test_lock.release()
+ let previous = @sys.get_env_var("OPENSEEK_SESSION_ROOT")
+ defer restore_session_root_env(previous)
+ let dir = @fs.tmpdir(prefix="openseek-archive-gone-")
+ let workspace = dir + "/ws"
+ let global_root = dir + "/global"
+ @sys.set_env_var("OPENSEEK_SESSION_ROOT", global_root)
+ @fsx.ensure_dir(Path(global_root))
+ @fsx.ensure_dir(Path(workspace + "/.openseek/sessions/s-ws"))
+ @fs.write_file(
+ global_root + "/workspaces.json",
+ ({ "workspaces": [workspace] } : Json).stringify(),
+ create_mode=CreateOrTruncate,
+ )
+ let manager = new_engine_manager("unused")
+ manager.pump = Serving(sessions=Map([]))
+ // The workspace directory vanishes outside the app; the registry keeps
+ // its dead entry addressable, so the sidebar still shows the workspace.
+ @fs.rmdir(workspace, recursive=true)
+ let detail = try {
+ ignore(archive_session(manager, "s-ws", fn() { }))
+ "no error"
+ } catch {
+ EngineError(detail) => detail
+ error if @async.is_being_cancelled() => raise error
+ error => "\{error}"
+ }
+ debug_inspect(
+ detail,
+ content=(
+ #|"no durable record for this conversation in the global store or any attached workspace"
+ ),
+ )
+ @fs.rmdir(dir, recursive=true)
+}
+
+///|
+#cfg(not(platform="windows"))
+async test "loading a conversation from a detached workspace names that state" {
+ archive_env_test_lock.acquire()
+ defer archive_env_test_lock.release()
+ let previous = @sys.get_env_var("OPENSEEK_SESSION_ROOT")
+ defer restore_session_root_env(previous)
+ let dir = @fs.tmpdir(prefix="openseek-load-detached-")
+ let workspace = dir + "/ws"
+ let global_root = dir + "/global"
+ @sys.set_env_var("OPENSEEK_SESSION_ROOT", global_root)
+ @fsx.ensure_dir(Path(global_root))
+ @fsx.ensure_dir(Path(workspace + "/.openseek/sessions/s-ws"))
+ @fs.write_file(
+ global_root + "/workspaces.json",
+ ({ "workspaces": [workspace] } : Json).stringify(),
+ create_mode=CreateOrTruncate,
+ )
+ let manager = new_engine_manager("unused")
+ manager.pump = Serving(sessions=Map([]))
+ let detached = detach_workspace(manager, workspace, fn(_) { })
+ assert_eq(detached.workspaces.length(), 0)
+ // A stale client's transcript reload names the conversation without a
+ // workspace hint; the reply must say the record is unreachable rather
+ // than surface the engine's raw file error.
+ let detail = try {
+ ignore(load_session(manager, { session: "s-ws", workspace: None }))
+ "no error"
+ } catch {
+ EngineError(detail) => detail
+ error if @async.is_being_cancelled() => raise error
+ error => "\{error}"
+ }
+ debug_inspect(
+ detail,
+ content=(
+ #|"no durable record for this conversation in the global store or any attached workspace"
+ ),
+ )
+ @fs.rmdir(dir, recursive=true)
+}
+
+///|
+#cfg(not(platform="windows"))
+async test "archiving an already-archived conversation names that state" {
+ archive_env_test_lock.acquire()
+ defer archive_env_test_lock.release()
+ let previous = @sys.get_env_var("OPENSEEK_SESSION_ROOT")
+ defer restore_session_root_env(previous)
+ let dir = @fs.tmpdir(prefix="openseek-archive-again-")
+ @sys.set_env_var("OPENSEEK_SESSION_ROOT", dir)
+ @fsx.ensure_dir(Path(dir + "/archived/sessions/s-done"))
+ let manager = new_engine_manager("unused")
+ manager.pump = Serving(sessions=Map([]))
+ let detail = try {
+ ignore(archive_session(manager, "s-done", fn() { }))
+ "no error"
+ } catch {
+ EngineError(detail) => detail
+ error if @async.is_being_cancelled() => raise error
+ error => "\{error}"
+ }
+ debug_inspect(
+ detail,
+ content=(
+ #|"this conversation is already archived"
+ ),
+ )
+ assert_true(@fsx.is_dir(Path(dir + "/archived/sessions/s-done")))
+ @fs.rmdir(dir, recursive=true)
+}
diff --git a/desktop/internal/engine/ops.mbt b/desktop/internal/engine/ops.mbt
index e78f260ab..366fab631 100644
--- a/desktop/internal/engine/ops.mbt
+++ b/desktop/internal/engine/ops.mbt
@@ -626,6 +626,18 @@ pub async fn load_session(
watermark: Some(session_max_sequence(document)),
}
}
+ // Name the missing-record states before spending an engine read that can
+ // only fail with a raw file error: a stale client asks for conversations
+ // whose store just detached (or whose record moved to an archived twin),
+ // and the answer should say that, not ENOENT.
+ if !@fsx.is_dir(root.join("sessions").join(session).to_path()) {
+ if archived_record_root(session) is Some(_) {
+ raise EngineError("this conversation is archived; unarchive it first")
+ }
+ raise EngineError(
+ "no durable record for this conversation in the global store or any attached workspace",
+ )
+ }
let output = session_show_stdout(manager, session, root) catch {
EngineError(detail) => raise EngineError("session load failed: " + detail)
error => raise error
diff --git a/desktop/internal/engine/pkg.generated.mbti b/desktop/internal/engine/pkg.generated.mbti
index 9c137a724..23764fbce 100644
--- a/desktop/internal/engine/pkg.generated.mbti
+++ b/desktop/internal/engine/pkg.generated.mbti
@@ -2,6 +2,7 @@
package "openseek_desktop/internal/engine"
import {
+ "moonbitlang/core/debug",
"openseek_desktop/internal/pathx",
"openseek_desktop/internal/protocol",
}
@@ -68,7 +69,7 @@ pub fn workspace_session_root(@pathx.Absolute) -> @pathx.Absolute
// Errors
pub(all) suberror EngineError {
EngineError(String)
-}
+} derive(@debug.Debug)
// Types and methods
type EngineActor