diff --git a/cmd/wsh/cmd/wshcmd-done.go b/cmd/wsh/cmd/wshcmd-done.go new file mode 100644 index 0000000000..ef94470fe1 --- /dev/null +++ b/cmd/wsh/cmd/wshcmd-done.go @@ -0,0 +1,62 @@ +// Copyright 2026, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/wavetermdev/waveterm/pkg/wps" + "github.com/wavetermdev/waveterm/pkg/wshrpc" + "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" +) + +var doneExitCode int +var doneTitle string +var doneMessage string + +var doneCmd = &cobra.Command{ + Use: "done [-t title] [-m message] [-e exitcode]", + Short: "Signal that a command has finished (triggers notification sound, highlight, and OS notification for background blocks)", + Args: cobra.MaximumNArgs(1), + RunE: doneRun, + PreRunE: preRunSetupRpcClient, +} + +func init() { + doneCmd.Flags().IntVarP(&doneExitCode, "exitcode", "e", 0, "exit code of the completed command") + doneCmd.Flags().StringVarP(&doneTitle, "title", "t", "", "notification title (default: Command Finished)") + doneCmd.Flags().StringVarP(&doneMessage, "message", "m", "", "notification message") + rootCmd.AddCommand(doneCmd) +} + +func doneRun(cmd *cobra.Command, args []string) (rtnErr error) { + defer func() { + sendActivity("done", rtnErr == nil) + }() + blockId := os.Getenv("WAVETERM_BLOCKID") + if blockId == "" { + return fmt.Errorf("WAVETERM_BLOCKID not set, must be run inside a Wave terminal block") + } + + if doneMessage == "" && len(args) > 0 { + doneMessage = args[0] + } + + err := wshclient.EventPublishCommand(RpcClient, wps.WaveEvent{ + Event: wps.Event_BlockDone, + Scopes: []string{fmt.Sprintf("block:%s", blockId)}, + Data: wshrpc.BlockDoneEventData{ + BlockId: blockId, + ExitCode: doneExitCode, + Title: doneTitle, + Message: doneMessage, + }, + }, &wshrpc.RpcOpts{NoResponse: true}) + if err != nil { + return fmt.Errorf("done command: %w", err) + } + return nil +} diff --git a/emain/emain-ipc.ts b/emain/emain-ipc.ts index 5e5f15b302..a46d195b86 100644 --- a/emain/emain-ipc.ts +++ b/emain/emain-ipc.ts @@ -504,6 +504,32 @@ export function initIpcHandlers() { bw.destroy(); }); + electron.ipcMain.on( + "show-completion-notification", + (event, tabId: string, blockId: string, title: string, body: string) => { + const senderWcId = event.sender.id; + const { Notification: ElectronNotification } = electron; + if (ElectronNotification == null || !ElectronNotification.isSupported()) { + return; + } + const notification = new ElectronNotification({ title, body, silent: false }); + notification.on("click", () => { + const ww = getWaveWindowByWebContentsId(senderWcId); + if (ww == null) return; + if (ww.isMinimized()) { + ww.restore(); + } + ww.focus(); + ww.setActiveTab(tabId, false); + const tabView = ww.allLoadedTabViews.get(tabId); + if (tabView) { + tabView.webContents.send("focus-block", blockId); + } + }); + notification.show(); + } + ); + electron.ipcMain.on("do-refresh", (event) => { event.sender.reloadIgnoringCache(); }); diff --git a/emain/preload.ts b/emain/preload.ts index 8d2b18a308..e6527d535d 100644 --- a/emain/preload.ts +++ b/emain/preload.ts @@ -73,6 +73,10 @@ contextBridge.exposeInMainWorld("api", { getPathForFile: (file: File): string => webUtils.getPathForFile(file), saveTextFile: (fileName: string, content: string) => ipcRenderer.invoke("save-text-file", fileName, content), setIsActive: () => ipcRenderer.invoke("set-is-active"), + showCompletionNotification: (tabId, blockId, title, body) => + ipcRenderer.send("show-completion-notification", tabId, blockId, title, body), + onFocusBlock: (callback) => + ipcRenderer.on("focus-block", (_event, blockId) => callback(blockId)), }); // Custom event for "new-window" diff --git a/frontend/app/block/block-model.ts b/frontend/app/block/block-model.ts index e2ce23e374..9158781dcc 100644 --- a/frontend/app/block/block-model.ts +++ b/frontend/app/block/block-model.ts @@ -12,12 +12,12 @@ export interface BlockHighlightType { export class BlockModel { private static instance: BlockModel | null = null; private blockHighlightAtomCache = new Map>(); + private completionHighlightAtomCache = new Map>(); blockHighlightAtom: jotai.PrimitiveAtom = jotai.atom(null) as jotai.PrimitiveAtom; + completionHighlightAtom: jotai.PrimitiveAtom> = jotai.atom(new Map()); - private constructor() { - // Empty for now - } + private constructor() {} getBlockHighlightAtom(blockId: string): jotai.Atom { let atom = this.blockHighlightAtomCache.get(blockId); @@ -38,6 +38,31 @@ export class BlockModel { globalStore.set(this.blockHighlightAtom, highlight); } + getCompletionHighlightAtom(blockId: string): jotai.Atom { + let atom = this.completionHighlightAtomCache.get(blockId); + if (!atom) { + atom = jotai.atom((get) => { + const map = get(this.completionHighlightAtom); + return map.get(blockId) ?? null; + }); + this.completionHighlightAtomCache.set(blockId, atom); + } + return atom; + } + + setCompletionHighlight(blockId: string, exitCode: number) { + const currentMap = new Map(globalStore.get(this.completionHighlightAtom)); + currentMap.set(blockId, exitCode); + globalStore.set(this.completionHighlightAtom, currentMap); + } + + clearCompletionHighlight(blockId: string) { + const currentMap = new Map(globalStore.get(this.completionHighlightAtom)); + if (!currentMap.has(blockId)) return; + currentMap.delete(blockId); + globalStore.set(this.completionHighlightAtom, currentMap); + } + static getInstance(): BlockModel { if (!BlockModel.instance) { BlockModel.instance = new BlockModel(); @@ -48,4 +73,4 @@ export class BlockModel { static resetInstance(): void { BlockModel.instance = null; } -} \ No newline at end of file +} diff --git a/frontend/app/block/block.scss b/frontend/app/block/block.scss index 6b0fda769c..1f50992d72 100644 --- a/frontend/app/block/block.scss +++ b/frontend/app/block/block.scss @@ -444,5 +444,9 @@ } } } + + .block-mask.completion-highlight { + transition: border-color 0.3s ease-out, box-shadow 0.3s ease-out; + } } } diff --git a/frontend/app/block/blockframe-header.tsx b/frontend/app/block/blockframe-header.tsx index a70f323e71..c1b1ad5530 100644 --- a/frontend/app/block/blockframe-header.tsx +++ b/frontend/app/block/blockframe-header.tsx @@ -23,7 +23,7 @@ import { uxCloseBlock } from "@/app/store/keymodel"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { useWaveEnv } from "@/app/waveenv/waveenv"; import { IconButton } from "@/element/iconbutton"; -import { NodeModel } from "@/layout/index"; +import { NodeModel, getLayoutModelForStaticTab } from "@/layout/index"; import * as util from "@/util/util"; import { cn, makeIconClass } from "@/util/util"; import * as jotai from "jotai"; @@ -281,7 +281,17 @@ const BlockFrame_Header = ({ /> )} {useTermHeader && badge && ( -
+
{ + const layoutModel = getLayoutModelForStaticTab(); + const node = layoutModel?.getNodeByBlockId(nodeModel.blockId); + if (node?.id) { + layoutModel.focusNode(node.id); + } + }} + >
)} diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index 8ff2e2d0a7..ac3109aa6e 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -6,6 +6,7 @@ import { BlockFrame_Header } from "@/app/block/blockframe-header"; import { blockViewToIcon, getViewIconElem, useTabBackground } from "@/app/block/blockutil"; import { ConnStatusOverlay } from "@/app/block/connstatusoverlay"; import { ChangeConnectionBlockModal } from "@/app/modals/conntypeahead"; +import { clearBadgesForBlockOnFocus } from "@/app/store/badge"; import { getBlockComponentModel, globalStore, useBlockAtom } from "@/app/store/global"; import { useTabModel } from "@/app/store/tab-model"; import { TabRpcClient } from "@/app/store/wshrpcutil"; @@ -32,6 +33,9 @@ const BlockMask = React.memo(({ nodeModel }: { nodeModel: NodeModel }) => { const isLayoutMode = jotai.useAtomValue(waveEnv.atoms.controlShiftDelayAtom); const showOverlayBlockNums = jotai.useAtomValue(waveEnv.getSettingsKeyAtom("app:showoverlayblocknums")) ?? true; const blockHighlight = jotai.useAtomValue(BlockModel.getInstance().getBlockHighlightAtom(nodeModel.blockId)); + const completionHighlight = jotai.useAtomValue( + BlockModel.getInstance().getCompletionHighlightAtom(nodeModel.blockId) + ); const frameActiveBorderColor = jotai.useAtomValue( waveEnv.getBlockMetaKeyAtom(nodeModel.blockId, "frame:activebordercolor") ); @@ -63,6 +67,19 @@ const BlockMask = React.memo(({ nodeModel }: { nodeModel: NodeModel }) => { style.borderColor = "rgb(59, 130, 246)"; } + if (completionHighlight != null && !isFocused) { + const highlightColor = completionHighlight === 0 ? "rgb(59, 130, 246)" : "rgb(239, 68, 68)"; + style.borderColor = highlightColor; + style.boxShadow = `0 0 8px 2px ${highlightColor}`; + } + + React.useEffect(() => { + if (isFocused && completionHighlight != null) { + BlockModel.getInstance().clearCompletionHighlight(nodeModel.blockId); + clearBadgesForBlockOnFocus(nodeModel.blockId); + } + }, [isFocused, completionHighlight]); + let innerElem = null; if (isLayoutMode && showOverlayBlockNums) { showBlockMask = true; @@ -83,7 +100,11 @@ const BlockMask = React.memo(({ nodeModel }: { nodeModel: NodeModel }) => { return (
{innerElem} diff --git a/frontend/app/view/term/term-model.ts b/frontend/app/view/term/term-model.ts index a256929e7d..560bb30b20 100644 --- a/frontend/app/view/term/term-model.ts +++ b/frontend/app/view/term/term-model.ts @@ -2,9 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import { WaveAIModel } from "@/app/aipanel/waveai-model"; +import { BlockModel } from "@/app/block/block-model"; import { BlockNodeModel } from "@/app/block/blocktypes"; import { appHandleKeyDown } from "@/app/store/keymodel"; +import { FocusManager } from "@/app/store/focusManager"; import { modalsModel } from "@/app/store/modalmodel"; +import { setBadge } from "@/app/store/badge"; import type { TabModel } from "@/app/store/tab-model"; import { waveEventSubscribeSingle } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; @@ -14,6 +17,7 @@ import { TermClaudeIcon, TerminalView } from "@/app/view/term/term"; import { TermWshClient } from "@/app/view/term/term-wsh"; import { VDomModel } from "@/app/view/vdom/vdom-model"; import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model"; +import { getLayoutModelForStaticTab } from "@/layout/lib/layoutModelHooks"; import { atoms, createBlock, @@ -73,6 +77,7 @@ export class TermViewModel implements ViewModel { shellProcFullStatus: jotai.PrimitiveAtom; shellProcStatus: jotai.Atom; shellProcStatusUnsubFn: () => void; + blockDoneUnsubFn: () => void; blockJobStatusAtom: jotai.PrimitiveAtom; blockJobStatusVersionTs: number; blockJobStatusUnsubFn: () => void; @@ -346,6 +351,13 @@ export class TermViewModel implements ViewModel { this.updateShellProcStatus(event.data); }, }); + this.blockDoneUnsubFn = waveEventSubscribeSingle({ + eventType: "block:done", + scope: WOS.makeORef("block", blockId), + handler: (event) => { + this.handleBlockDoneEvent(event.data); + }, + }); this.shellProcStatus = jotai.atom((get) => { const fullStatus = get(this.shellProcFullStatus); return fullStatus?.shellprocstatus ?? "init"; @@ -565,6 +577,75 @@ export class TermViewModel implements ViewModel { } } + getLastTerminalLine(): string { + const term = this.termRef.current?.terminal; + if (term == null) return ""; + const buf = term.buffer.active; + const start = Math.max(0, buf.length - 200); + for (let i = buf.length - 1; i >= start; i--) { + const line = buf.getLine(i); + if (line == null) continue; + const text = line.translateToString(true).trim(); + if (text.length > 0) return text; + } + return ""; + } + + handleBlockDoneEvent(data: BlockDoneEventData) { + if (data == null || data.blockid !== this.blockId) { + return; + } + const exitCode = data.exitcode ?? 0; + const title = data.title || (exitCode === 0 ? "Command Finished" : "Command Failed"); + let body = data.message; + if (!body) { + body = this.getLastTerminalLine() || `exit code ${exitCode}`; + } + this.triggerCompletionNotifications(exitCode, title, body); + } + + triggerCompletionNotifications(exitCode: number, title: string, notifyBody?: string) { + const focusManager = FocusManager.getInstance(); + const focusedBlockId = globalStore.get(focusManager.blockFocusAtom); + if (focusedBlockId === this.blockId) { + return; + } + + const doneSoundEnabled = globalStore.get(getOverrideConfigAtom(this.blockId, "term:donesound")) ?? true; + if (doneSoundEnabled) { + fireAndForget(() => + RpcApi.ElectronSystemBellCommand(TabRpcClient, { route: "electron" }) + ); + } + + const doneNotifyEnabled = globalStore.get(getOverrideConfigAtom(this.blockId, "term:donenotify")) ?? true; + if (doneNotifyEnabled) { + const body = notifyBody || `exit code ${exitCode}`; + getApi().showCompletionNotification(this.tabModel.tabId, this.blockId, title, body); + } + + const doneAutoFocusEnabled = globalStore.get(getOverrideConfigAtom(this.blockId, "term:doneautofocus")) ?? false; + if (doneAutoFocusEnabled) { + getApi().setActiveTab(this.tabModel.tabId); + setTimeout(() => { + const layoutModel = getLayoutModelForStaticTab(); + const node = layoutModel?.getNodeByBlockId(this.blockId); + if (node?.id) { + layoutModel.focusNode(node.id); + } + }, 150); + } + + BlockModel.getInstance().setCompletionHighlight(this.blockId, exitCode); + + setBadge(this.blockId, { + badgeid: `done-${this.blockId}`, + icon: "bell", + color: exitCode === 0 ? "#3b82f6" : "#ef4444", + priority: 5, + }); + } + getVDomModel(): VDomModel { const vdomBlockId = globalStore.get(this.vdomBlockId); if (!vdomBlockId) { @@ -592,6 +673,7 @@ export class TermViewModel implements ViewModel { dispose() { DefaultRouter.unregisterRoute(makeFeBlockRouteId(this.blockId)); this.shellProcStatusUnsubFn?.(); + this.blockDoneUnsubFn?.(); this.blockJobStatusUnsubFn?.(); this.termBPMUnsubFn?.(); this.termCursorUnsubFn?.(); diff --git a/frontend/preview/mock/preview-electron-api.ts b/frontend/preview/mock/preview-electron-api.ts index 36c82f26da..9ac0239c4a 100644 --- a/frontend/preview/mock/preview-electron-api.ts +++ b/frontend/preview/mock/preview-electron-api.ts @@ -59,6 +59,8 @@ const previewElectronApi: ElectronApi = { doRefresh: () => {}, saveTextFile: (_fileName: string, _content: string) => Promise.resolve(false), setIsActive: async () => {}, + showCompletionNotification: (_tabId: string, _blockId: string, _title: string, _body: string) => {}, + onFocusBlock: (_callback: (blockId: string) => void) => {}, }; function installPreviewElectronApi() { diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 06157e2566..860339363f 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -136,6 +136,8 @@ declare global { getPathForFile: (file: File) => string; // webUtils.getPathForFile saveTextFile: (fileName: string, content: string) => Promise; // save-text-file setIsActive: () => Promise; // set-is-active + showCompletionNotification: (tabId: string, blockId: string, title: string, body: string) => void; // show-completion-notification + onFocusBlock: (callback: (blockId: string) => void) => void; // focus-block }; type ElectronContextMenuItem = { diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index c5b870d7ed..89172e4729 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -162,6 +162,14 @@ declare global { meta?: MetaType; }; + // wshrpc.BlockDoneEventData + type BlockDoneEventData = { + blockid: string; + exitcode: number; + title?: string; + message?: string; + }; + // wshrpc.BlockInfoData type BlockInfoData = { blockid: string; @@ -1188,6 +1196,9 @@ declare global { "term:conndebug"?: string; "term:bellsound"?: boolean; "term:bellindicator"?: boolean; + "term:donenotify"?: boolean; + "term:donesound"?: boolean; + "term:doneautofocus"?: boolean; "term:osc52"?: string; "term:durable"?: boolean; "web:zoom"?: number; @@ -1422,6 +1433,9 @@ declare global { "term:cursorblink"?: boolean; "term:bellsound"?: boolean; "term:bellindicator"?: boolean; + "term:donenotify"?: boolean; + "term:donesound"?: boolean; + "term:doneautofocus"?: boolean; "term:osc52"?: string; "term:durable"?: boolean; "term:showsplitbuttons"?: boolean; @@ -1589,6 +1603,7 @@ declare global { "debug:panictype"?: string; "block:view"?: string; "block:controller"?: string; + "block:subblock"?: boolean; "ai:backendtype"?: string; "ai:local"?: boolean; "wsh:cmd"?: string; diff --git a/frontend/types/waveevent.d.ts b/frontend/types/waveevent.d.ts index c3e5bd7822..b6ee02e3c8 100644 --- a/frontend/types/waveevent.d.ts +++ b/frontend/types/waveevent.d.ts @@ -26,6 +26,7 @@ declare global { | "waveai:modeconfig" | "block:jobstatus" | "badge" + | "block:done" ; type WaveEvent = { @@ -53,7 +54,8 @@ declare global { { event: "tsunami:updatemeta"; data?: AppMeta; } | { event: "waveai:modeconfig"; data?: AIModeConfigUpdate; } | { event: "block:jobstatus"; data?: BlockJobStatusData; } | - { event: "badge"; data?: BadgeEvent; } + { event: "badge"; data?: BadgeEvent; } | + { event: "block:done"; data?: BlockDoneEventData; } ); } diff --git a/frontend/wave.ts b/frontend/wave.ts index 20ee2ba97a..53568f55bf 100644 --- a/frontend/wave.ts +++ b/frontend/wave.ts @@ -69,6 +69,13 @@ async function initBare() { getApi().onZoomFactorChange((zoomFactor) => { updateZoomFactor(zoomFactor); }); + getApi().onFocusBlock((blockId) => { + const layoutModel = getLayoutModelForStaticTab(); + const node = layoutModel?.getNodeByBlockId(blockId); + if (node?.id) { + layoutModel.focusNode(node.id); + } + }); document.fonts.ready.then(() => { console.log("Init Bare Done"); getApi().setWindowInitStatus("ready"); diff --git a/pkg/tsgen/tsgenevent.go b/pkg/tsgen/tsgenevent.go index 6e1c08e981..1bdf0ab6ec 100644 --- a/pkg/tsgen/tsgenevent.go +++ b/pkg/tsgen/tsgenevent.go @@ -41,6 +41,7 @@ var WaveEventDataTypes = map[string]reflect.Type{ wps.Event_AIModeConfig: reflect.TypeOf(wconfig.AIModeConfigUpdate{}), wps.Event_BlockJobStatus: reflect.TypeOf(wshrpc.BlockJobStatusData{}), wps.Event_Badge: reflect.TypeOf(baseds.BadgeEvent{}), + wps.Event_BlockDone: reflect.TypeOf(wshrpc.BlockDoneEventData{}), } func getWaveEventDataTSType(eventName string, tsTypesMap map[reflect.Type]string) string { diff --git a/pkg/waveobj/metaconsts.go b/pkg/waveobj/metaconsts.go index 0ce08099d8..3e94f63afc 100644 --- a/pkg/waveobj/metaconsts.go +++ b/pkg/waveobj/metaconsts.go @@ -127,6 +127,9 @@ const ( MetaKey_TermConnDebug = "term:conndebug" MetaKey_TermBellSound = "term:bellsound" MetaKey_TermBellIndicator = "term:bellindicator" + MetaKey_TermDoneNotify = "term:donenotify" + MetaKey_TermDoneSound = "term:donesound" + MetaKey_TermDoneAutoFocus = "term:doneautofocus" MetaKey_TermOsc52 = "term:osc52" MetaKey_TermDurable = "term:durable" diff --git a/pkg/waveobj/wtypemeta.go b/pkg/waveobj/wtypemeta.go index 2280b55d2d..3029456eef 100644 --- a/pkg/waveobj/wtypemeta.go +++ b/pkg/waveobj/wtypemeta.go @@ -131,6 +131,9 @@ type MetaTSType struct { TermConnDebug string `json:"term:conndebug,omitempty"` // null, info, debug TermBellSound *bool `json:"term:bellsound,omitempty"` TermBellIndicator *bool `json:"term:bellindicator,omitempty"` + TermDoneNotify *bool `json:"term:donenotify,omitempty"` + TermDoneSound *bool `json:"term:donesound,omitempty"` + TermDoneAutoFocus *bool `json:"term:doneautofocus,omitempty"` TermOsc52 string `json:"term:osc52,omitempty"` TermDurable *bool `json:"term:durable,omitempty"` diff --git a/pkg/wconfig/defaultconfig/settings.json b/pkg/wconfig/defaultconfig/settings.json index d8847cabf2..59a54378d9 100644 --- a/pkg/wconfig/defaultconfig/settings.json +++ b/pkg/wconfig/defaultconfig/settings.json @@ -32,6 +32,9 @@ "telemetry:enabled": true, "term:bellsound": false, "term:bellindicator": true, + "term:donenotify": true, + "term:donesound": true, + "term:doneautofocus": false, "term:osc52": "always", "term:cursor": "block", "term:cursorblink": false, diff --git a/pkg/wconfig/metaconsts.go b/pkg/wconfig/metaconsts.go index 7d5bba5d9d..f28cebdfb5 100644 --- a/pkg/wconfig/metaconsts.go +++ b/pkg/wconfig/metaconsts.go @@ -57,6 +57,9 @@ const ( ConfigKey_TermCursorBlink = "term:cursorblink" ConfigKey_TermBellSound = "term:bellsound" ConfigKey_TermBellIndicator = "term:bellindicator" + ConfigKey_TermDoneNotify = "term:donenotify" + ConfigKey_TermDoneSound = "term:donesound" + ConfigKey_TermDoneAutoFocus = "term:doneautofocus" ConfigKey_TermOsc52 = "term:osc52" ConfigKey_TermDurable = "term:durable" ConfigKey_TermShowSplitButtons = "term:showsplitbuttons" diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index 67118b1670..56f77575d1 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -108,6 +108,9 @@ type SettingsType struct { TermCursorBlink *bool `json:"term:cursorblink,omitempty"` TermBellSound *bool `json:"term:bellsound,omitempty"` TermBellIndicator *bool `json:"term:bellindicator,omitempty"` + TermDoneNotify *bool `json:"term:donenotify,omitempty"` + TermDoneSound *bool `json:"term:donesound,omitempty"` + TermDoneAutoFocus *bool `json:"term:doneautofocus,omitempty"` TermOsc52 string `json:"term:osc52,omitempty" jsonschema:"enum=focus,enum=always"` TermDurable *bool `json:"term:durable,omitempty"` TermShowSplitButtons bool `json:"term:showsplitbuttons,omitempty"` diff --git a/pkg/wps/wpstypes.go b/pkg/wps/wpstypes.go index 0077ec9d5e..8d79d61f93 100644 --- a/pkg/wps/wpstypes.go +++ b/pkg/wps/wpstypes.go @@ -34,6 +34,7 @@ const ( Event_AIModeConfig = "waveai:modeconfig" // type: wconfig.AIModeConfigUpdate Event_BlockJobStatus = "block:jobstatus" // type: wshrpc.BlockJobStatusData Event_Badge = "badge" // type: baseds.BadgeEvent + Event_BlockDone = "block:done" // type: wshrpc.BlockDoneEventData ) var AllEvents []string = []string{ @@ -56,6 +57,7 @@ var AllEvents []string = []string{ Event_AIModeConfig, Event_BlockJobStatus, Event_Badge, + Event_BlockDone, } type WaveEvent struct { diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index 51e2338ba8..3f3b109bde 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -845,6 +845,13 @@ type CommandBadgeWatchPidData struct { BadgeId string `json:"badgeid"` } +type BlockDoneEventData struct { + BlockId string `json:"blockid"` + ExitCode int `json:"exitcode"` + Title string `json:"title,omitempty"` + Message string `json:"message,omitempty"` +} + type BlockJobStatusData struct { BlockId string `json:"blockid"` JobId string `json:"jobid"` diff --git a/schema/settings.json b/schema/settings.json index f341a0f365..496810ae5a 100644 --- a/schema/settings.json +++ b/schema/settings.json @@ -158,6 +158,15 @@ "term:bellindicator": { "type": "boolean" }, + "term:donenotify": { + "type": "boolean" + }, + "term:donesound": { + "type": "boolean" + }, + "term:doneautofocus": { + "type": "boolean" + }, "term:osc52": { "type": "string", "enum": [