From 5dce5b064e9bead8dd4eb88b04a3d2580a91dab2 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 21 Oct 2025 18:31:28 -0700 Subject: [PATCH 1/5] add markers to terminal for osc 16162;A events (prompt start) --- frontend/app/view/term/termwrap.ts | 50 ++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index 5d9e64b564..d6886a63de 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -171,6 +171,32 @@ function handleOsc7Command(data: string, blockId: string, loaded: boolean): bool return true; } +// some POC concept code for adding a decoration to a marker +function addTestMarkerDecoration(terminal: Terminal, marker: TermTypes.IMarker, termWrap: TermWrap): void { + const decoration = terminal.registerDecoration({ + marker: marker, + layer: "top", + }); + if (!decoration) { + return; + } + decoration.onRender((el) => { + el.classList.add("wave-decoration"); + el.classList.add("bg-ansi-white"); + el.dataset.markerline = String(marker.line); + if (!el.querySelector(".wave-deco-line")) { + const line = document.createElement("div"); + line.classList.add("wave-deco-line", "bg-accent/20"); + line.style.position = "absolute"; + line.style.top = "0"; + line.style.left = "0"; + line.style.width = "500px"; + line.style.height = "1px"; + el.appendChild(line); + } + }); +} + // OSC 16162 - Shell Integration Commands // See aiprompts/wave-osc-16162.md for full documentation type Osc16162Command = @@ -181,7 +207,8 @@ type Osc16162Command = | { command: "I"; data: { inputempty?: boolean } } | { command: "R"; data: {} }; -function handleOsc16162Command(data: string, blockId: string, loaded: boolean, terminal: Terminal): boolean { +function handleOsc16162Command(data: string, blockId: string, loaded: boolean, termWrap: TermWrap): boolean { + const terminal = termWrap.terminal; if (!loaded) { return true; } @@ -206,6 +233,17 @@ function handleOsc16162Command(data: string, blockId: string, loaded: boolean, t switch (cmd.command) { case "A": rtInfo["shell:state"] = "ready"; + const marker = terminal.registerMarker(0); + if (marker) { + termWrap.promptMarkers.push(marker); + // addTestMarkerDecoration(terminal, marker, termWrap); + marker.onDispose(() => { + const idx = termWrap.promptMarkers.indexOf(marker); + if (idx !== -1) { + termWrap.promptMarkers.splice(idx, 1); + } + }); + } break; case "C": rtInfo["shell:state"] = "running-command"; @@ -298,6 +336,7 @@ export class TermWrap { private toDispose: TermTypes.IDisposable[] = []; pasteActive: boolean = false; lastUpdated: number; + promptMarkers: TermTypes.IMarker[] = []; constructor( blockId: string, @@ -312,6 +351,7 @@ export class TermWrap { this.dataBytesProcessed = 0; this.hasResized = false; this.lastUpdated = Date.now(); + this.promptMarkers = []; this.terminal = new Terminal(options); this.fitAddon = new FitAddon(); this.fitAddon.noScrollbar = PLATFORM === PlatformMacOS; @@ -358,7 +398,7 @@ export class TermWrap { return handleOsc7Command(data, this.blockId, this.loaded); }); this.terminal.parser.registerOscHandler(16162, (data: string) => { - return handleOsc16162Command(data, this.blockId, this.loaded, this.terminal); + return handleOsc16162Command(data, this.blockId, this.loaded, this); }); this.terminal.attachCustomKeyEventHandler(waveOptions.keydownHandler); this.connectElem = connectElem; @@ -413,6 +453,12 @@ export class TermWrap { } dispose() { + this.promptMarkers.forEach((marker) => { + try { + marker.dispose(); + } catch (_) {} + }); + this.promptMarkers = []; this.terminal.dispose(); this.toDispose.forEach((d) => { try { From 93a508e31d81273ce701d9ee5bd603fc04afe1f3 Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 21 Oct 2025 19:53:08 -0700 Subject: [PATCH 2/5] working on deeper shell integration, track integration status on FE. implement a get last command output tool + rpc command. --- frontend/app/view/term/term-wsh.tsx | 38 +++++ frontend/app/view/term/term.tsx | 51 ++++-- frontend/app/view/term/termwrap.ts | 10 ++ frontend/types/gotypes.d.ts | 1 + package-lock.json | 4 +- pkg/aiusechat/tools.go | 1 + pkg/aiusechat/tools_term.go | 208 ++++++++++++++++++------- pkg/blockcontroller/shellcontroller.go | 10 +- pkg/wshrpc/wshrpctypes.go | 5 +- 9 files changed, 256 insertions(+), 72 deletions(-) diff --git a/frontend/app/view/term/term-wsh.tsx b/frontend/app/view/term/term-wsh.tsx index 099e4bb7e4..a4febcb631 100644 --- a/frontend/app/view/term/term-wsh.tsx +++ b/frontend/app/view/term/term-wsh.tsx @@ -122,6 +122,44 @@ export class TermWshClient extends WshClient { const totalLines = buffer.length; const lines: string[] = []; + if (data.lastcommand) { + if (termWrap.shellIntegrationStatus == null) { + throw new Error("Cannot get last command data without shell integration"); + } + + let startLine = 0; + if (termWrap.promptMarkers.length > 0) { + const lastMarker = termWrap.promptMarkers[termWrap.promptMarkers.length - 1]; + const markerLine = lastMarker.line; + startLine = totalLines - markerLine; + } + + const endLine = totalLines; + for (let i = startLine; i < endLine; i++) { + const bufferIndex = totalLines - 1 - i; + const line = buffer.getLine(bufferIndex); + if (line) { + lines.push(line.translateToString(true)); + } + } + + lines.reverse(); + + let returnLines = lines; + let returnStartLine = startLine; + if (lines.length > 1000) { + returnLines = lines.slice(lines.length - 1000); + returnStartLine = startLine + (lines.length - 1000); + } + + return { + totallines: totalLines, + linestart: returnStartLine, + lines: returnLines, + lastupdated: termWrap.lastUpdated, + }; + } + const startLine = Math.max(0, data.linestart); const endLine = Math.min(totalLines, data.lineend); diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index 37f146838d..ae9a8060e7 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -70,6 +70,7 @@ class TermViewModel implements ViewModel { termTransparencyAtom: jotai.Atom; noPadding: jotai.PrimitiveAtom; endIconButtons: jotai.Atom; + shellIntegrationStatusAtom: jotai.PrimitiveAtom<"ready" | "running-command" | null>; shellProcFullStatus: jotai.PrimitiveAtom; shellProcStatus: jotai.Atom; shellProcStatusUnsubFn: () => void; @@ -260,16 +261,42 @@ class TermViewModel implements ViewModel { }); }); this.noPadding = jotai.atom(true); + this.shellIntegrationStatusAtom = jotai.atom(null) as jotai.PrimitiveAtom<"ready" | "running-command" | null>; this.endIconButtons = jotai.atom((get) => { const blockData = get(this.blockAtom); const shellProcStatus = get(this.shellProcStatus); const connStatus = get(this.connStatus); const isCmd = get(this.isCmdController); + const shellIntegrationStatus = get(this.shellIntegrationStatusAtom); + const rtn: IconButtonDecl[] = []; + + if (shellIntegrationStatus != null) { + let iconColor: string; + let iconSpin: boolean = false; + let title: string; + if (shellIntegrationStatus === "ready") { + iconColor = "var(--success-color)"; + title = "Ready to Run Command (Wave AI Enabled)"; + } else { + iconColor = "var(--warning-color)"; + iconSpin = true; + title = "Running Command. Cannot run Wave AI command until completed"; + } + rtn.push({ + elemtype: "iconbutton", + icon: "sparkles", + iconColor: iconColor, + iconSpin: iconSpin, + title: title, + noAction: true, + }); + } + if (blockData?.meta?.["controller"] != "cmd" && shellProcStatus != "done") { - return []; + return rtn; } if (connStatus?.status != "connected") { - return []; + return rtn; } let iconName: string = null; let title: string = null; @@ -284,16 +311,15 @@ class TermViewModel implements ViewModel { iconName = "refresh"; title = noun + " Exited. Click to Restart"; } - if (iconName == null) { - return []; + if (iconName != null) { + const buttonDecl: IconButtonDecl = { + elemtype: "iconbutton", + icon: iconName, + click: this.forceRestartController.bind(this), + title: title, + }; + rtn.push(buttonDecl); } - const buttonDecl: IconButtonDecl = { - elemtype: "iconbutton", - icon: iconName, - click: this.forceRestartController.bind(this), - title: title, - }; - const rtn = [buttonDecl]; return rtn; }); this.isCmdController = jotai.atom((get) => { @@ -1023,6 +1049,9 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => ); (window as any).term = termWrap; model.termRef.current = termWrap; + termWrap.shellIntegrationStatusCallback = (status) => { + globalStore.set(model.shellIntegrationStatusAtom, status); + }; const rszObs = new ResizeObserver(() => { termWrap.handleResize_debounced(); }); diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index d6886a63de..ae86ca4b8d 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -199,6 +199,8 @@ function addTestMarkerDecoration(terminal: Terminal, marker: TermTypes.IMarker, // OSC 16162 - Shell Integration Commands // See aiprompts/wave-osc-16162.md for full documentation +type ShellIntegrationStatus = "ready" | "running-command"; + type Osc16162Command = | { command: "A"; data: {} } | { command: "C"; data: { cmd64?: string } } @@ -233,6 +235,8 @@ function handleOsc16162Command(data: string, blockId: string, loaded: boolean, t switch (cmd.command) { case "A": rtInfo["shell:state"] = "ready"; + termWrap.shellIntegrationStatus = "ready"; + termWrap.shellIntegrationStatusCallback?.(termWrap.shellIntegrationStatus); const marker = terminal.registerMarker(0); if (marker) { termWrap.promptMarkers.push(marker); @@ -247,6 +251,8 @@ function handleOsc16162Command(data: string, blockId: string, loaded: boolean, t break; case "C": rtInfo["shell:state"] = "running-command"; + termWrap.shellIntegrationStatus = "running-command"; + termWrap.shellIntegrationStatusCallback?.(termWrap.shellIntegrationStatus); if (cmd.data.cmd64) { const decodedLen = Math.ceil(cmd.data.cmd64.length * 0.75); if (decodedLen > 8192) { @@ -293,6 +299,8 @@ function handleOsc16162Command(data: string, blockId: string, loaded: boolean, t } break; case "R": + termWrap.shellIntegrationStatus = null; + termWrap.shellIntegrationStatusCallback?.(termWrap.shellIntegrationStatus); if (terminal.buffer.active.type === "alternate") { terminal.write("\x1b[?1049l"); } @@ -337,6 +345,8 @@ export class TermWrap { pasteActive: boolean = false; lastUpdated: number; promptMarkers: TermTypes.IMarker[] = []; + shellIntegrationStatus: ShellIntegrationStatus | null = null; + shellIntegrationStatusCallback?: (status: ShellIntegrationStatus | null) => void; constructor( blockId: string, diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 5a44c802a2..ce2c8f3455 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -296,6 +296,7 @@ declare global { type CommandTermGetScrollbackLinesData = { linestart: number; lineend: number; + lastcommand: boolean; }; // wshrpc.CommandTermGetScrollbackLinesRtnData diff --git a/package-lock.json b/package-lock.json index cd1c128bad..24a7815f79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "waveterm", - "version": "0.12.1-beta.0", + "version": "0.12.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "waveterm", - "version": "0.12.1-beta.0", + "version": "0.12.1", "hasInstallScript": true, "license": "Apache-2.0", "workspaces": [ diff --git a/pkg/aiusechat/tools.go b/pkg/aiusechat/tools.go index b038848ce9..a270f0c24e 100644 --- a/pkg/aiusechat/tools.go +++ b/pkg/aiusechat/tools.go @@ -173,6 +173,7 @@ func GenerateTabStateAndTools(ctx context.Context, tabid string, widgetAccess bo } if viewTypes["term"] { tools = append(tools, GetTermGetScrollbackToolDefinition(tabid)) + tools = append(tools, GetTermCommandOutputToolDefinition(tabid)) } if viewTypes["web"] { tools = append(tools, GetWebNavigateToolDefinition(tabid)) diff --git a/pkg/aiusechat/tools_term.go b/pkg/aiusechat/tools_term.go index dfbc764855..57c9164bbe 100644 --- a/pkg/aiusechat/tools_term.go +++ b/pkg/aiusechat/tools_term.go @@ -81,6 +81,76 @@ func parseTermGetScrollbackInput(input any) (*TermGetScrollbackToolInput, error) return result, nil } +func getTermScrollbackOutput(tabId string, widgetId string, rpcData wshrpc.CommandTermGetScrollbackLinesData) (*TermGetScrollbackToolOutput, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + fullBlockId, err := wcore.ResolveBlockIdFromPrefix(ctx, tabId, widgetId) + if err != nil { + return nil, err + } + + rpcClient := wshclient.GetBareRpcClient() + result, err := wshclient.TermGetScrollbackLinesCommand( + rpcClient, + rpcData, + &wshrpc.RpcOpts{Route: wshutil.MakeFeBlockRouteId(fullBlockId)}, + ) + if err != nil { + return nil, err + } + + content := strings.Join(result.Lines, "\n") + var effectiveLineEnd int + if rpcData.LastCommand { + effectiveLineEnd = result.LineStart + len(result.Lines) + } else { + effectiveLineEnd = min(rpcData.LineEnd, result.TotalLines) + } + hasMore := effectiveLineEnd < result.TotalLines + + var sinceLastOutputSec *int + if result.LastUpdated > 0 { + sec := max(0, int((time.Now().UnixMilli()-result.LastUpdated)/1000)) + sinceLastOutputSec = &sec + } + + var nextStart *int + if hasMore { + nextStart = &effectiveLineEnd + } + + blockORef := waveobj.MakeORef(waveobj.OType_Block, fullBlockId) + rtInfo := wstore.GetRTInfo(blockORef) + + var lastCommand *CommandInfo + if rtInfo != nil && rtInfo.ShellIntegration && rtInfo.ShellLastCmd != "" { + cmdInfo := &CommandInfo{ + Command: rtInfo.ShellLastCmd, + } + if rtInfo.ShellState == "running-command" { + cmdInfo.Status = "running" + } else if rtInfo.ShellState == "ready" { + cmdInfo.Status = "completed" + exitCode := rtInfo.ShellLastCmdExitCode + cmdInfo.ExitCode = &exitCode + } + lastCommand = cmdInfo + } + + return &TermGetScrollbackToolOutput{ + TotalLines: result.TotalLines, + LineStart: result.LineStart, + LineEnd: effectiveLineEnd, + ReturnedLines: len(result.Lines), + Content: content, + SinceLastOutputSec: sinceLastOutputSec, + HasMore: hasMore, + NextStart: nextStart, + LastCommand: lastCommand, + }, nil +} + func GetTermGetScrollbackToolDefinition(tabId string) uctypes.ToolDefinition { return uctypes.ToolDefinition{ Name: "term_get_scrollback", @@ -97,12 +167,12 @@ func GetTermGetScrollbackToolDefinition(tabId string) uctypes.ToolDefinition { "line_start": map[string]any{ "type": "integer", "minimum": 0, - "description": "Logical start index where 0 = most recent line (default: 0)", + "description": "Logical start index where 0 = most recent line (default: 0).", }, "count": map[string]any{ "type": "integer", "minimum": 1, - "description": "Number of lines to return from line_start (default: 200)", + "description": "Number of lines to return from line_start (default: 200).", }, }, "required": []string{"widget_id"}, @@ -126,73 +196,107 @@ func GetTermGetScrollbackToolDefinition(tabId string) uctypes.ToolDefinition { return nil, err } - ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) - defer cancelFn() - - fullBlockId, err := wcore.ResolveBlockIdFromPrefix(ctx, tabId, parsed.WidgetId) - if err != nil { - return nil, err - } - lineEnd := parsed.LineStart + parsed.Count - - rpcClient := wshclient.GetBareRpcClient() - result, err := wshclient.TermGetScrollbackLinesCommand( - rpcClient, + output, err := getTermScrollbackOutput( + tabId, + parsed.WidgetId, wshrpc.CommandTermGetScrollbackLinesData{ - LineStart: parsed.LineStart, - LineEnd: lineEnd, + LineStart: parsed.LineStart, + LineEnd: lineEnd, + LastCommand: false, }, - &wshrpc.RpcOpts{Route: wshutil.MakeFeBlockRouteId(fullBlockId)}, ) if err != nil { return nil, fmt.Errorf("failed to get terminal scrollback: %w", err) } + return output, nil + }, + } +} + - content := strings.Join(result.Lines, "\n") - effectiveLineEnd := min(lineEnd, result.TotalLines) - hasMore := effectiveLineEnd < result.TotalLines +type TermCommandOutputToolInput struct { + WidgetId string `json:"widget_id"` +} + +func parseTermCommandOutputInput(input any) (*TermCommandOutputToolInput, error) { + result := &TermCommandOutputToolInput{} + + if input == nil { + return nil, fmt.Errorf("widget_id is required") + } + + inputBytes, err := json.Marshal(input) + if err != nil { + return nil, fmt.Errorf("failed to marshal input: %w", err) + } - var sinceLastOutputSec *int - if result.LastUpdated > 0 { - sec := max(0, int((time.Now().UnixMilli()-result.LastUpdated)/1000)) - sinceLastOutputSec = &sec + if err := json.Unmarshal(inputBytes, result); err != nil { + return nil, fmt.Errorf("failed to unmarshal input: %w", err) + } + + if result.WidgetId == "" { + return nil, fmt.Errorf("widget_id is required") + } + + return result, nil +} + +func GetTermCommandOutputToolDefinition(tabId string) uctypes.ToolDefinition { + return uctypes.ToolDefinition{ + Name: "term_command_output", + DisplayName: "Get Last Command Output", + Description: "Retrieve output from the most recent command in a terminal widget. Requires shell integration to be enabled. Returns the command text, exit code, and up to 1000 lines of output.", + ToolLogName: "term:commandoutput", + InputSchema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "widget_id": map[string]any{ + "type": "string", + "description": "8-character widget ID of the terminal widget", + }, + }, + "required": []string{"widget_id"}, + "additionalProperties": false, + }, + ToolInputDesc: func(input any) string { + parsed, err := parseTermCommandOutputInput(input) + if err != nil { + return fmt.Sprintf("error parsing input: %v", err) + } + return fmt.Sprintf("reading last command output from %s", parsed.WidgetId) + }, + ToolAnyCallback: func(input any) (any, error) { + parsed, err := parseTermCommandOutputInput(input) + if err != nil { + return nil, err } - var nextStart *int - if hasMore { - nextStart = &effectiveLineEnd + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + fullBlockId, err := wcore.ResolveBlockIdFromPrefix(ctx, tabId, parsed.WidgetId) + if err != nil { + return nil, err } blockORef := waveobj.MakeORef(waveobj.OType_Block, fullBlockId) rtInfo := wstore.GetRTInfo(blockORef) - - var lastCommand *CommandInfo - if rtInfo != nil && rtInfo.ShellIntegration && rtInfo.ShellLastCmd != "" { - cmdInfo := &CommandInfo{ - Command: rtInfo.ShellLastCmd, - } - if rtInfo.ShellState == "running-command" { - cmdInfo.Status = "running" - } else if rtInfo.ShellState == "ready" { - cmdInfo.Status = "completed" - exitCode := rtInfo.ShellLastCmdExitCode - cmdInfo.ExitCode = &exitCode - } - lastCommand = cmdInfo + if rtInfo == nil || !rtInfo.ShellIntegration { + return nil, fmt.Errorf("shell integration is not enabled for this terminal") } - return &TermGetScrollbackToolOutput{ - TotalLines: result.TotalLines, - LineStart: result.LineStart, - LineEnd: effectiveLineEnd, - ReturnedLines: len(result.Lines), - Content: content, - SinceLastOutputSec: sinceLastOutputSec, - HasMore: hasMore, - NextStart: nextStart, - LastCommand: lastCommand, - }, nil + output, err := getTermScrollbackOutput( + tabId, + parsed.WidgetId, + wshrpc.CommandTermGetScrollbackLinesData{ + LastCommand: true, + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to get command output: %w", err) + } + return output, nil }, } } diff --git a/pkg/blockcontroller/shellcontroller.go b/pkg/blockcontroller/shellcontroller.go index 04124153f1..aca0864600 100644 --- a/pkg/blockcontroller/shellcontroller.go +++ b/pkg/blockcontroller/shellcontroller.go @@ -200,11 +200,11 @@ func (sc *ShellController) resetTerminalState(logCtx context.Context) { blocklogger.Debugf(logCtx, "[conndebug] resetTerminalState: resetting terminal state\n") // controller type = "shell" var buf bytes.Buffer - buf.WriteString("\x1b[0m") // reset attributes - buf.WriteString("\x1b[?25h") // show cursor - buf.WriteString("\x1b[?1000l") // disable mouse tracking - buf.WriteString("\x1b[?1007l") // disable alternate scroll mode - buf.WriteString(shellutil.FormatOSC(16162, "R")) // OSC 16162 "R" - disable alternate screen mode (only if active) + buf.WriteString("\x1b[0m") // reset attributes + buf.WriteString("\x1b[?25h") // show cursor + buf.WriteString("\x1b[?1000l") // disable mouse tracking + buf.WriteString("\x1b[?1007l") // disable alternate scroll mode + buf.WriteString(shellutil.FormatOSC(16162, "R")) // OSC 16162 "R" - disable alternate screen mode (only if active), reset "shell integration" status. buf.WriteString("\r\n\r\n") err := HandleAppendBlockFile(sc.BlockId, wavebase.BlockFile_Term, buf.Bytes()) if err != nil { diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index f1abf21e04..b4cd905633 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -865,8 +865,9 @@ type CommandSetRTInfoData struct { } type CommandTermGetScrollbackLinesData struct { - LineStart int `json:"linestart"` - LineEnd int `json:"lineend"` + LineStart int `json:"linestart"` + LineEnd int `json:"lineend"` + LastCommand bool `json:"lastcommand"` } type CommandTermGetScrollbackLinesRtnData struct { From f3a32149648d45be61c7584705ce3f71145c7b6d Mon Sep 17 00:00:00 2001 From: sawka Date: Tue, 21 Oct 2025 22:24:34 -0700 Subject: [PATCH 3/5] break out term-model from term.tsx. update makeIconClass to deal with suffixes create atoms for shell integration state, show in term header. restore from server on reload. --- frontend/app/block/block.tsx | 2 +- frontend/app/element/iconbutton.tsx | 8 +- frontend/app/view/term/term-model.ts | 773 ++++++++++++++++++++++++++ frontend/app/view/term/term-wsh.tsx | 4 +- frontend/app/view/term/term.tsx | 774 +-------------------------- frontend/app/view/term/termtheme.ts | 2 +- frontend/app/view/term/termwrap.ts | 41 +- frontend/util/util.ts | 54 +- 8 files changed, 858 insertions(+), 800 deletions(-) create mode 100644 frontend/app/view/term/term-model.ts diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index 350f984563..81896572f7 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -27,7 +27,7 @@ import { getWaveObjectAtom, makeORef, useWaveObjectValue } from "@/store/wos"; import { focusedBlockId, getElemAsStr } from "@/util/focusutil"; import { isBlank, useAtomValueSafe } from "@/util/util"; import { HelpViewModel } from "@/view/helpview/helpview"; -import { TermViewModel } from "@/view/term/term"; +import { TermViewModel } from "@/view/term/term-model"; import { WaveAiModel } from "@/view/waveai/waveai"; import { WebViewModel } from "@/view/webview/webview"; import clsx from "clsx"; diff --git a/frontend/app/element/iconbutton.tsx b/frontend/app/element/iconbutton.tsx index 45e5771099..318e2cbbee 100644 --- a/frontend/app/element/iconbutton.tsx +++ b/frontend/app/element/iconbutton.tsx @@ -5,7 +5,7 @@ import { useLongClick } from "@/app/hook/useLongClick"; import { makeIconClass } from "@/util/util"; import clsx from "clsx"; import { atom, useAtom } from "jotai"; -import { forwardRef, memo, useMemo, useRef } from "react"; +import { CSSProperties, forwardRef, memo, useMemo, useRef } from "react"; import "./iconbutton.scss"; type IconButtonProps = { decl: IconButtonDecl; className?: string }; @@ -15,6 +15,10 @@ export const IconButton = memo( const spin = decl.iconSpin ?? false; useLongClick(ref, decl.click, decl.longClick, decl.disabled); const disabled = decl.disabled ?? false; + const styleVal: CSSProperties = {}; + if (decl.iconColor) { + styleVal.color = decl.iconColor; + } return (