Use non-native alert and confirm dialogs
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
import { Component, Show, createMemo, createEffect, createSignal } from "solid-js"
|
import { Component, Show, createMemo, createEffect, createSignal } from "solid-js"
|
||||||
import { Dialog } from "@kobalte/core/dialog"
|
import { Dialog } from "@kobalte/core/dialog"
|
||||||
import { Toaster } from "solid-toast"
|
import { Toaster } from "solid-toast"
|
||||||
|
import AlertDialog from "./components/alert-dialog"
|
||||||
import FolderSelectionView from "./components/folder-selection-view"
|
import FolderSelectionView from "./components/folder-selection-view"
|
||||||
|
import { showConfirmDialog } from "./stores/alerts"
|
||||||
import InstanceTabs from "./components/instance-tabs"
|
import InstanceTabs from "./components/instance-tabs"
|
||||||
import InstanceDisconnectedModal from "./components/instance-disconnected-modal"
|
import InstanceDisconnectedModal from "./components/instance-disconnected-modal"
|
||||||
import InstanceShell from "./components/instance/instance-shell"
|
import InstanceShell from "./components/instance/instance-shell"
|
||||||
@@ -135,11 +137,21 @@ const App: Component = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleCloseInstance(instanceId: string) {
|
async function handleCloseInstance(instanceId: string) {
|
||||||
if (confirm("Stop OpenCode instance? This will stop the server.")) {
|
const confirmed = await showConfirmDialog(
|
||||||
await stopInstance(instanceId)
|
"Stop OpenCode instance? This will stop the server.",
|
||||||
if (instances().size === 0) {
|
{
|
||||||
setHasInstances(false)
|
title: "Stop instance",
|
||||||
}
|
variant: "warning",
|
||||||
|
confirmLabel: "Stop",
|
||||||
|
cancelLabel: "Keep running",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!confirmed) return
|
||||||
|
|
||||||
|
await stopInstance(instanceId)
|
||||||
|
if (instances().size === 0) {
|
||||||
|
setHasInstances(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,6 +333,8 @@ const App: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<AlertDialog />
|
||||||
|
|
||||||
<Toaster
|
<Toaster
|
||||||
position="top-right"
|
position="top-right"
|
||||||
gutter={16}
|
gutter={16}
|
||||||
|
|||||||
132
packages/ui/src/components/alert-dialog.tsx
Normal file
132
packages/ui/src/components/alert-dialog.tsx
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { Dialog } from "@kobalte/core/dialog"
|
||||||
|
import { Component, Show, createEffect } from "solid-js"
|
||||||
|
import { alertDialogState, dismissAlertDialog } from "../stores/alerts"
|
||||||
|
import type { AlertVariant, AlertDialogState } from "../stores/alerts"
|
||||||
|
|
||||||
|
const variantAccent: Record<AlertVariant, { badgeBg: string; badgeBorder: string; badgeText: string; symbol: string; fallbackTitle: string }> = {
|
||||||
|
info: {
|
||||||
|
badgeBg: "var(--badge-neutral-bg)",
|
||||||
|
badgeBorder: "var(--border-base)",
|
||||||
|
badgeText: "var(--accent-primary)",
|
||||||
|
symbol: "i",
|
||||||
|
fallbackTitle: "Heads up",
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
badgeBg: "rgba(255, 152, 0, 0.14)",
|
||||||
|
badgeBorder: "var(--status-warning)",
|
||||||
|
badgeText: "var(--status-warning)",
|
||||||
|
symbol: "!",
|
||||||
|
fallbackTitle: "Please review",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
badgeBg: "var(--danger-soft-bg)",
|
||||||
|
badgeBorder: "var(--status-error)",
|
||||||
|
badgeText: "var(--status-error)",
|
||||||
|
symbol: "!",
|
||||||
|
fallbackTitle: "Something went wrong",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismiss(confirmed: boolean, payload?: AlertDialogState | null) {
|
||||||
|
const current = payload ?? alertDialogState()
|
||||||
|
if (current?.type === "confirm") {
|
||||||
|
if (confirmed) {
|
||||||
|
current.onConfirm?.()
|
||||||
|
} else {
|
||||||
|
current.onCancel?.()
|
||||||
|
}
|
||||||
|
current.resolve?.(confirmed)
|
||||||
|
} else if (confirmed) {
|
||||||
|
current?.onConfirm?.()
|
||||||
|
}
|
||||||
|
dismissAlertDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
const AlertDialog: Component = () => {
|
||||||
|
let primaryButtonRef: HTMLButtonElement | undefined
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (alertDialogState()) {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
primaryButtonRef?.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={alertDialogState()} keyed>
|
||||||
|
{(payload) => {
|
||||||
|
const variant = payload.variant ?? "info"
|
||||||
|
const accent = variantAccent[variant]
|
||||||
|
const title = payload.title || accent.fallbackTitle
|
||||||
|
const isConfirm = payload.type === "confirm"
|
||||||
|
const confirmLabel = payload.confirmLabel || (isConfirm ? "Confirm" : "OK")
|
||||||
|
const cancelLabel = payload.cancelLabel || "Cancel"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open
|
||||||
|
modal
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
dismiss(false, payload)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Overlay class="modal-overlay" />
|
||||||
|
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<Dialog.Content class="modal-surface w-full max-w-sm p-6 border border-base shadow-2xl" tabIndex={-1}>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div
|
||||||
|
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl border text-base font-semibold"
|
||||||
|
style={{
|
||||||
|
"background-color": accent.badgeBg,
|
||||||
|
"border-color": accent.badgeBorder,
|
||||||
|
color: accent.badgeText,
|
||||||
|
}}
|
||||||
|
aria-hidden
|
||||||
|
>
|
||||||
|
{accent.symbol}
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<Dialog.Title class="text-lg font-semibold text-primary">{title}</Dialog.Title>
|
||||||
|
<Dialog.Description class="text-sm text-secondary mt-1 whitespace-pre-line">
|
||||||
|
{payload.message}
|
||||||
|
{payload.detail && <p class="mt-2 text-secondary">{payload.detail}</p>}
|
||||||
|
</Dialog.Description>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 flex justify-end gap-3">
|
||||||
|
{isConfirm && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button-secondary"
|
||||||
|
onClick={() => dismiss(false, payload)}
|
||||||
|
>
|
||||||
|
{cancelLabel}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button-primary"
|
||||||
|
ref={(el) => {
|
||||||
|
primaryButtonRef = el
|
||||||
|
}}
|
||||||
|
onClick={() => dismiss(true, payload)}
|
||||||
|
>
|
||||||
|
{confirmLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</div>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AlertDialog
|
||||||
@@ -10,6 +10,7 @@ import Kbd from "./kbd"
|
|||||||
import HintRow from "./hint-row"
|
import HintRow from "./hint-row"
|
||||||
import { getActiveInstance } from "../stores/instances"
|
import { getActiveInstance } from "../stores/instances"
|
||||||
import { agents, getSessionDraftPrompt, setSessionDraftPrompt, clearSessionDraftPrompt } from "../stores/sessions"
|
import { agents, getSessionDraftPrompt, setSessionDraftPrompt, clearSessionDraftPrompt } from "../stores/sessions"
|
||||||
|
import { showAlertDialog } from "../stores/alerts"
|
||||||
|
|
||||||
interface PromptInputProps {
|
interface PromptInputProps {
|
||||||
instanceId: string
|
instanceId: string
|
||||||
@@ -526,7 +527,11 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to send message:", error)
|
console.error("Failed to send message:", error)
|
||||||
alert("Failed to send message: " + (error instanceof Error ? error.message : String(error)))
|
showAlertDialog("Failed to send message", {
|
||||||
|
title: "Send failed",
|
||||||
|
detail: error instanceof Error ? error.message : String(error),
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
} finally {
|
} finally {
|
||||||
textareaRef?.focus()
|
textareaRef?.focus()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import MessageStream from "../message-stream"
|
|||||||
import PromptInput from "../prompt-input"
|
import PromptInput from "../prompt-input"
|
||||||
import { instances } from "../../stores/instances"
|
import { instances } from "../../stores/instances"
|
||||||
import { loadMessages, sendMessage, forkSession, isSessionMessagesLoading, setActiveParentSession, setActiveSession, runShellCommand } from "../../stores/sessions"
|
import { loadMessages, sendMessage, forkSession, isSessionMessagesLoading, setActiveParentSession, setActiveSession, runShellCommand } from "../../stores/sessions"
|
||||||
|
import { showAlertDialog } from "../../stores/alerts"
|
||||||
|
|
||||||
interface SessionViewProps {
|
interface SessionViewProps {
|
||||||
sessionId: string
|
sessionId: string
|
||||||
@@ -73,7 +74,10 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to revert:", error)
|
console.error("Failed to revert:", error)
|
||||||
alert("Failed to revert to message")
|
showAlertDialog("Failed to revert to message", {
|
||||||
|
title: "Revert failed",
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +110,10 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fork session:", error)
|
console.error("Failed to fork session:", error)
|
||||||
alert("Failed to fork session")
|
showAlertDialog("Failed to fork session", {
|
||||||
|
title: "Fork failed",
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Command } from "./commands"
|
import type { Command } from "./commands"
|
||||||
import type { Command as SDKCommand } from "@opencode-ai/sdk"
|
import type { Command as SDKCommand } from "@opencode-ai/sdk"
|
||||||
|
import { showAlertDialog } from "../stores/alerts"
|
||||||
import { activeSessionId, executeCustomCommand } from "../stores/sessions"
|
import { activeSessionId, executeCustomCommand } from "../stores/sessions"
|
||||||
|
|
||||||
export function commandRequiresArguments(template?: string): boolean {
|
export function commandRequiresArguments(template?: string): boolean {
|
||||||
@@ -33,7 +34,10 @@ export function buildCustomCommandEntries(instanceId: string, commands: SDKComma
|
|||||||
action: async () => {
|
action: async () => {
|
||||||
const sessionId = activeSessionId().get(instanceId)
|
const sessionId = activeSessionId().get(instanceId)
|
||||||
if (!sessionId || sessionId === "info") {
|
if (!sessionId || sessionId === "info") {
|
||||||
alert("Select a session before running a custom command.")
|
showAlertDialog("Select a session before running a custom command.", {
|
||||||
|
title: "Session required",
|
||||||
|
variant: "warning",
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const args = promptForCommandArguments(cmd)
|
const args = promptForCommandArguments(cmd)
|
||||||
@@ -44,7 +48,10 @@ export function buildCustomCommandEntries(instanceId: string, commands: SDKComma
|
|||||||
await executeCustomCommand(instanceId, sessionId, cmd.name, args)
|
await executeCustomCommand(instanceId, sessionId, cmd.name, args)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to run custom command:", error)
|
console.error("Failed to run custom command:", error)
|
||||||
alert("Failed to run custom command. Check the console for details.")
|
showAlertDialog("Failed to run custom command. Check the console for details.", {
|
||||||
|
title: "Command failed",
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
setActiveSession,
|
setActiveSession,
|
||||||
} from "../../stores/sessions"
|
} from "../../stores/sessions"
|
||||||
import { setSessionCompactionState } from "../../stores/session-compaction"
|
import { setSessionCompactionState } from "../../stores/session-compaction"
|
||||||
|
import { showAlertDialog } from "../../stores/alerts"
|
||||||
import type { Instance } from "../../types/instance"
|
import type { Instance } from "../../types/instance"
|
||||||
|
|
||||||
export interface UseCommandsOptions {
|
export interface UseCommandsOptions {
|
||||||
@@ -207,7 +208,10 @@ export function useCommands(options: UseCommandsOptions) {
|
|||||||
setSessionCompactionState(instance.id, sessionId, false)
|
setSessionCompactionState(instance.id, sessionId, false)
|
||||||
console.error("Failed to compact session:", error)
|
console.error("Failed to compact session:", error)
|
||||||
const message = error instanceof Error ? error.message : "Failed to compact session"
|
const message = error instanceof Error ? error.message : "Failed to compact session"
|
||||||
alert(`Compact failed: ${message}`)
|
showAlertDialog(`Compact failed: ${message}`, {
|
||||||
|
title: "Compact failed",
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -256,7 +260,10 @@ export function useCommands(options: UseCommandsOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!messageID) {
|
if (!messageID) {
|
||||||
alert("Nothing to undo")
|
showAlertDialog("Nothing to undo", {
|
||||||
|
title: "No actions to undo",
|
||||||
|
variant: "info",
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +289,10 @@ export function useCommands(options: UseCommandsOptions) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to revert message:", error)
|
console.error("Failed to revert message:", error)
|
||||||
alert("Failed to revert message")
|
showAlertDialog("Failed to revert message", {
|
||||||
|
title: "Undo failed",
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
46
packages/ui/src/stores/alerts.ts
Normal file
46
packages/ui/src/stores/alerts.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { createSignal } from "solid-js"
|
||||||
|
|
||||||
|
export type AlertVariant = "info" | "warning" | "error"
|
||||||
|
|
||||||
|
export type AlertDialogState = {
|
||||||
|
type?: "alert" | "confirm"
|
||||||
|
title?: string
|
||||||
|
message: string
|
||||||
|
detail?: string
|
||||||
|
variant?: AlertVariant
|
||||||
|
confirmLabel?: string
|
||||||
|
cancelLabel?: string
|
||||||
|
onConfirm?: () => void
|
||||||
|
onCancel?: () => void
|
||||||
|
resolve?: (value: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const [alertDialogState, setAlertDialogState] = createSignal<AlertDialogState | null>(null)
|
||||||
|
|
||||||
|
export function showAlertDialog(message: string, options?: Omit<AlertDialogState, "message">) {
|
||||||
|
setAlertDialogState({
|
||||||
|
type: "alert",
|
||||||
|
message,
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showConfirmDialog(message: string, options?: Omit<AlertDialogState, "message">): Promise<boolean> {
|
||||||
|
const activeElement = typeof document !== "undefined" ? (document.activeElement as HTMLElement | null) : null
|
||||||
|
activeElement?.blur()
|
||||||
|
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
setAlertDialogState({
|
||||||
|
type: "confirm",
|
||||||
|
message,
|
||||||
|
...options,
|
||||||
|
resolve,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dismissAlertDialog() {
|
||||||
|
setAlertDialogState(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { alertDialogState }
|
||||||
@@ -16,6 +16,7 @@ import type {
|
|||||||
import { showToastNotification, ToastVariant } from "../lib/notifications"
|
import { showToastNotification, ToastVariant } from "../lib/notifications"
|
||||||
import { preferences } from "./preferences"
|
import { preferences } from "./preferences"
|
||||||
import { instances, addPermissionToQueue, removePermissionFromQueue, refreshPermissionsForSession } from "./instances"
|
import { instances, addPermissionToQueue, removePermissionFromQueue, refreshPermissionsForSession } from "./instances"
|
||||||
|
import { showAlertDialog } from "./alerts"
|
||||||
import {
|
import {
|
||||||
sessions,
|
sessions,
|
||||||
setSessions,
|
setSessions,
|
||||||
@@ -441,7 +442,10 @@ function handleSessionError(_instanceId: string, event: EventSessionError): void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
alert(`Error: ${message}`)
|
showAlertDialog(`Error: ${message}`, {
|
||||||
|
title: "Session error",
|
||||||
|
variant: "error",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMessageRemoved(instanceId: string, event: MessageRemovedEvent): void {
|
function handleMessageRemoved(instanceId: string, event: MessageRemovedEvent): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user