Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/ISSUE_TEMPLATE/bug-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ body:
required: false

- type: input
id: opencode-version
id: mimo-version
attributes:
label: OpenCode version
description: What version of OpenCode are you using?
label: MiMo Code version
description: What version of MiMo Code are you using?
validations:
required: false

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function ErrorComponent(props: {
)
}

issueURL.searchParams.set("opencode-version", InstallationVersion)
issueURL.searchParams.set("mimo-version", InstallationVersion)

const copyIssueURL = () => {
void Clipboard.copy(issueURL.toString()).then(() => {
Expand Down
122 changes: 82 additions & 40 deletions packages/opencode/src/cli/cmd/tui/ui/toast.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createContext, useContext, type ParentProps, Show } from "solid-js"
import { createStore } from "solid-js/store"
import { createContext, useContext, type ParentProps, Show, For } from "solid-js"
import { createStore, produce } from "solid-js/store"
import { useTheme } from "@tui/context/theme"
import { useTerminalDimensions } from "@opentui/solid"
import { SplitBorder } from "../component/border"
Expand All @@ -10,61 +10,100 @@ import { useLanguage } from "@tui/context/language"

export type ToastOptions = z.infer<typeof TuiEvent.ToastShow.properties>

const MAX_TOASTS = 3

interface ToastEntry extends ToastOptions {
id: number
}

export function Toast() {
const toast = useToast()
const { theme } = useTheme()
const dimensions = useTerminalDimensions()

return (
<Show when={toast.currentToast}>
{(current) => (
<box
position="absolute"
zIndex={4000}
justifyContent="center"
alignItems="flex-start"
top={2}
right={2}
maxWidth={Math.min(60, dimensions().width - 6)}
paddingLeft={2}
paddingRight={2}
paddingTop={1}
paddingBottom={1}
backgroundColor={theme.backgroundPanel}
borderColor={theme[current().variant]}
border={["left", "right"]}
customBorderChars={SplitBorder.customBorderChars}
>
<Show when={current().title}>
<text attributes={TextAttributes.BOLD} marginBottom={1} fg={theme.text}>
{current().title}
<For each={toast.toasts()}>
{(entry, index) => {
const height = entry.title
? (entry.message.length > 40 ? 4 : 3)
: (entry.message.length > 40 ? 3 : 2)
return (
<box
position="absolute"
zIndex={4000}
justifyContent="center"
alignItems="flex-start"
top={2 + index() * (height + 1)}
right={2}
maxWidth={Math.min(60, dimensions().width - 6)}
paddingLeft={2}
paddingRight={2}
paddingTop={1}
paddingBottom={1}
backgroundColor={theme.backgroundPanel}
borderColor={theme[entry.variant]}
border={["left", "right"]}
customBorderChars={SplitBorder.customBorderChars}
>
<Show when={entry.title}>
<text attributes={TextAttributes.BOLD} marginBottom={1} fg={theme.text}>
{entry.title}
</text>
</Show>
<text fg={theme.text} wrapMode="word" width="100%">
{entry.message}
</text>
</Show>
<text fg={theme.text} wrapMode="word" width="100%">
{current().message}
</text>
</box>
)}
</Show>
</box>
)
}}
</For>
)
}

let nextToastId = 0

function init() {
const [store, setStore] = createStore({
currentToast: null as ToastOptions | null,
toasts: [] as ToastEntry[],
})
const t = useLanguage().t

let timeoutHandle: NodeJS.Timeout | null = null
const timeoutHandles = new Map<number, NodeJS.Timeout>()

function removeToast(id: number) {
const handle = timeoutHandles.get(id)
if (handle) {
clearTimeout(handle)
timeoutHandles.delete(id)
}
setStore(produce((draft: { toasts: ToastEntry[] }) => {
const idx = draft.toasts.findIndex(t => t.id === id)
if (idx !== -1) draft.toasts.splice(idx, 1)
}))
}

const toast = {
show(options: ToastOptions) {
const { duration = 5000, ...currentToast } = options
setStore("currentToast", currentToast)
if (timeoutHandle) clearTimeout(timeoutHandle)
timeoutHandle = setTimeout(() => {
setStore("currentToast", null)
}, duration).unref()
const { duration = 5000, ...toastOpts } = options
const id = nextToastId++
const entry: ToastEntry = { ...toastOpts, id }

// Evict oldest toasts if at capacity
const current = store.toasts
while (current.length >= MAX_TOASTS) {
const removed = current.shift()!
const handle = timeoutHandles.get(removed.id)
if (handle) {
clearTimeout(handle)
timeoutHandles.delete(removed.id)
}
}
current.push(entry)
setStore("toasts", [...current])

timeoutHandles.set(id, setTimeout(() => {
removeToast(id)
}, duration).unref())
},
error: (err: any) => {
if (err instanceof Error)
Expand All @@ -77,8 +116,11 @@ function init() {
message: t("tui.toast.unknown_error"),
})
},
get toasts() {
return store.toasts
},
get currentToast(): ToastOptions | null {
return store.currentToast
return store.toasts[store.toasts.length - 1] ?? null
},
}
return toast
Expand Down