Skip to content

Commit f2f7613

Browse files
committed
feat: enhance code block functionality
- Implement sending code to terminal
1 parent faa957d commit f2f7613

File tree

4 files changed

+92
-106
lines changed

4 files changed

+92
-106
lines changed

frontend/app/element/markdown.scss

+20-11
Original file line numberDiff line numberDiff line change
@@ -125,22 +125,31 @@
125125
}
126126

127127
.codeblock-actions {
128-
visibility: hidden;
129-
display: flex;
130128
position: absolute;
131-
top: 0;
132-
right: 0;
129+
top: 5px;
130+
right: 5px;
131+
display: flex;
132+
gap: 4px;
133+
opacity: 0;
134+
transition: opacity 0.1s ease-in-out;
135+
background-color: rgba(0, 0, 0, 0.2);
136+
backdrop-filter: blur(2px);
133137
border-radius: 4px;
134-
backdrop-filter: blur(8px);
135-
margin: 0.143em;
136-
padding: 0.286em;
137-
align-items: center;
138-
justify-content: flex-end;
139-
gap: 0.286em;
138+
padding: 2px 4px;
139+
140+
.iconbutton {
141+
font-size: 14px;
142+
padding: 3px;
143+
border-radius: 3px;
144+
145+
&:hover {
146+
background-color: rgba(255, 255, 255, 0.1);
147+
}
148+
}
140149
}
141150

142151
&:hover .codeblock-actions {
143-
visibility: visible;
152+
opacity: 1;
144153
}
145154
}
146155

frontend/app/element/markdown.tsx

+71-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import {
99
resolveSrcSet,
1010
transformBlocks,
1111
} from "@/app/element/markdown-util";
12-
import { boundNumber, useAtomValueSafe } from "@/util/util";
12+
import { ContextMenuModel as contextMenuModel } from "@/app/store/contextmenu";
13+
import { RpcApi } from "@/app/store/wshclientapi";
14+
import { TabRpcClient } from "@/app/store/wshrpcutil";
15+
import { boundNumber, stringToBase64, useAtomValueSafe } from "@/util/util";
1316
import { clsx } from "clsx";
1417
import { Atom } from "jotai";
1518
import { OverlayScrollbarsComponent, OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
@@ -21,7 +24,7 @@ import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
2124
import rehypeSlug from "rehype-slug";
2225
import RemarkFlexibleToc, { TocItem } from "remark-flexible-toc";
2326
import remarkGfm from "remark-gfm";
24-
import { openLink } from "../store/global";
27+
import { atoms, getAllBlockComponentModels, globalStore, openLink } from "../store/global";
2528
import { IconButton } from "./iconbutton";
2629
import "./markdown.scss";
2730

@@ -91,6 +94,64 @@ const CodeBlock = ({ children, onClickExecute }: CodeBlockProps) => {
9194
}
9295
};
9396

97+
const handleSendToTerminal = async (e: React.MouseEvent) => {
98+
let textToSend = getTextContent(children);
99+
textToSend = textToSend.replace(/\n$/, "");
100+
101+
const allBCMs = getAllBlockComponentModels();
102+
const terminalBlocks = [];
103+
104+
for (const bcm of allBCMs) {
105+
if (bcm?.viewModel?.viewType === "term") {
106+
terminalBlocks.push({
107+
id: (bcm.viewModel as any).blockId || "",
108+
title: `Terminal ${terminalBlocks.length + 1}`,
109+
});
110+
}
111+
}
112+
113+
const menuItems: ContextMenuItem[] = terminalBlocks.map((terminal) => ({
114+
label: terminal.title,
115+
click: () => sendTextToTerminal(terminal.id, textToSend),
116+
}));
117+
118+
menuItems.push({ type: "separator" });
119+
menuItems.push({
120+
label: "Create New Terminal",
121+
click: async () => {
122+
const termBlockDef = {
123+
meta: {
124+
controller: "shell",
125+
view: "term",
126+
},
127+
};
128+
try {
129+
const tabId = globalStore.get(atoms.staticTabId);
130+
const oref = await RpcApi.CreateBlockCommand(TabRpcClient, {
131+
tabid: tabId,
132+
blockdef: termBlockDef,
133+
});
134+
135+
const blockId = oref.split(":")[1];
136+
setTimeout(() => sendTextToTerminal(blockId, textToSend), 500);
137+
} catch (error) {
138+
console.error("Failed to create new terminal block:", error);
139+
}
140+
},
141+
});
142+
143+
contextMenuModel.showContextMenu(menuItems, e);
144+
};
145+
146+
const sendTextToTerminal = (blockId: string, text: string) => {
147+
const textWithReturn = text + "\n";
148+
const b64data = stringToBase64(textWithReturn);
149+
RpcApi.ControllerInputCommand(TabRpcClient, {
150+
blockid: blockId,
151+
inputdata64: b64data,
152+
});
153+
};
154+
94155
return (
95156
<pre className="codeblock">
96157
{children}
@@ -105,6 +166,14 @@ const CodeBlock = ({ children, onClickExecute }: CodeBlockProps) => {
105166
}}
106167
/>
107168
)}
169+
<IconButton
170+
decl={{
171+
elemtype: "iconbutton",
172+
icon: "regular@terminal",
173+
click: handleSendToTerminal,
174+
title: "Send to Terminal",
175+
}}
176+
/>
108177
</div>
109178
</pre>
110179
);

frontend/app/element/typingindicator.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
transform: translateY(0);
4949
}
5050
50% {
51-
transform: translateY(-5px);
51+
transform: translateY(-2.5px);
5252
opacity: 1;
5353
}
5454
}

frontend/app/view/waveai/waveai.tsx

-92
Original file line numberDiff line numberDiff line change
@@ -190,98 +190,6 @@ export class WaveAiModel implements ViewModel {
190190
return opts;
191191
});
192192

193-
this.viewText = atom((get) => {
194-
const viewTextChildren: HeaderElem[] = [];
195-
const aiOpts = get(this.aiOpts);
196-
const presets = get(this.presetMap);
197-
const presetKey = get(this.presetKey);
198-
const presetName = presets[presetKey]?.["display:name"] ?? "";
199-
const isCloud = isBlank(aiOpts.apitoken) && isBlank(aiOpts.baseurl);
200-
201-
// Handle known API providers
202-
switch (aiOpts?.apitype) {
203-
case "anthropic":
204-
viewTextChildren.push({
205-
elemtype: "iconbutton",
206-
icon: "globe",
207-
title: `Using Remote Anthropic API (${aiOpts.model})`,
208-
noAction: true,
209-
});
210-
break;
211-
case "perplexity":
212-
viewTextChildren.push({
213-
elemtype: "iconbutton",
214-
icon: "globe",
215-
title: `Using Remote Perplexity API (${aiOpts.model})`,
216-
noAction: true,
217-
});
218-
break;
219-
default:
220-
if (isCloud) {
221-
viewTextChildren.push({
222-
elemtype: "iconbutton",
223-
icon: "cloud",
224-
title: "Using Wave's AI Proxy (gpt-4o-mini)",
225-
noAction: true,
226-
});
227-
} else {
228-
const baseUrl = aiOpts.baseurl ?? "OpenAI Default Endpoint";
229-
const modelName = aiOpts.model;
230-
if (baseUrl.startsWith("http://localhost") || baseUrl.startsWith("http://127.0.0.1")) {
231-
viewTextChildren.push({
232-
elemtype: "iconbutton",
233-
icon: "location-dot",
234-
title: `Using Local Model @ ${baseUrl} (${modelName})`,
235-
noAction: true,
236-
});
237-
} else {
238-
viewTextChildren.push({
239-
elemtype: "iconbutton",
240-
icon: "globe",
241-
title: `Using Remote Model @ ${baseUrl} (${modelName})`,
242-
noAction: true,
243-
});
244-
}
245-
}
246-
}
247-
248-
const dropdownItems = Object.entries(presets)
249-
.sort((a, b) => ((a[1]["display:order"] ?? 0) > (b[1]["display:order"] ?? 0) ? 1 : -1))
250-
.map(
251-
(preset) =>
252-
({
253-
label: preset[1]["display:name"],
254-
onClick: () =>
255-
fireAndForget(() =>
256-
ObjectService.UpdateObjectMeta(WOS.makeORef("block", this.blockId), {
257-
"ai:preset": preset[0],
258-
})
259-
),
260-
}) as MenuItem
261-
);
262-
dropdownItems.push({
263-
label: "Add AI preset...",
264-
onClick: () => {
265-
fireAndForget(async () => {
266-
const path = `${getApi().getConfigDir()}/presets/ai.json`;
267-
const blockDef: BlockDef = {
268-
meta: {
269-
view: "preview",
270-
file: path,
271-
},
272-
};
273-
await createBlock(blockDef, false, true);
274-
});
275-
},
276-
});
277-
viewTextChildren.push({
278-
elemtype: "menubutton",
279-
text: presetName,
280-
title: "Select AI Configuration",
281-
items: dropdownItems,
282-
});
283-
return viewTextChildren;
284-
});
285193
this.endIconButtons = atom((_) => {
286194
let clearButton: IconButtonDecl = {
287195
elemtype: "iconbutton",

0 commit comments

Comments
 (0)