Skip to content

Commit 9990893

Browse files
committed
first cut at reading terminal scrollback
1 parent 7b56a14 commit 9990893

8 files changed

Lines changed: 182 additions & 2 deletions

File tree

frontend/app/aipanel/aipanel.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { atoms, getSettingsKeyAtom } from "@/app/store/global";
99
import { globalStore } from "@/app/store/jotaiStore";
1010
import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model";
1111
import { getWebServerEndpoint } from "@/util/endpoints";
12-
import { getElemAsStr } from "@/util/focusutil";
1312
import { checkKeyPressed, keydownWrapper } from "@/util/keyutil";
1413
import { cn } from "@/util/util";
1514
import { useChat } from "@ai-sdk/react";
@@ -251,7 +250,7 @@ const AIPanelComponentInner = memo(({ className, onClose }: AIPanelProps) => {
251250
};
252251

253252
const handleFocusCapture = useCallback((event: React.FocusEvent) => {
254-
console.log("Wave AI focus capture", getElemAsStr(event.target));
253+
// console.log("Wave AI focus capture", getElemAsStr(event.target));
255254
focusManager.requestWaveAIFocus();
256255
}, []);
257256

frontend/app/store/wshclientapi.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,11 @@ class RpcApiType {
447447
return client.wshRpcStream("streamwaveai", data, opts);
448448
}
449449

450+
// command "termgetscrollbacklines" [call]
451+
TermGetScrollbackLinesCommand(client: WshClient, data: CommandTermGetScrollbackLinesData, opts?: RpcOpts): Promise<CommandTermGetScrollbackLinesRtnData> {
452+
return client.wshRpcCall("termgetscrollbacklines", data, opts);
453+
}
454+
450455
// command "test" [call]
451456
TestCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> {
452457
return client.wshRpcCall("test", data, opts);

frontend/app/view/term/term-wsh.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,43 @@ export class TermWshClient extends WshClient {
103103
return oref;
104104
}
105105
}
106+
107+
async handle_termgetscrollbacklines(
108+
rh: RpcResponseHelper,
109+
data: CommandTermGetScrollbackLinesData
110+
): Promise<CommandTermGetScrollbackLinesRtnData> {
111+
const termWrap = this.model.termRef.current;
112+
if (!termWrap || !termWrap.terminal) {
113+
return {
114+
totallines: 0,
115+
linestart: data.linestart,
116+
lines: [],
117+
lastupdated: 0,
118+
};
119+
}
120+
121+
const buffer = termWrap.terminal.buffer.active;
122+
const totalLines = buffer.length;
123+
const lines: string[] = [];
124+
125+
const startLine = Math.max(0, data.linestart);
126+
const endLine = Math.min(totalLines, data.lineend);
127+
128+
for (let i = startLine; i < endLine; i++) {
129+
const bufferIndex = totalLines - 1 - i;
130+
const line = buffer.getLine(bufferIndex);
131+
if (line) {
132+
lines.push(line.translateToString(true));
133+
}
134+
}
135+
136+
lines.reverse();
137+
138+
return {
139+
totallines: totalLines,
140+
linestart: startLine,
141+
lines: lines,
142+
lastupdated: termWrap.lastUpdated,
143+
};
144+
}
106145
}

frontend/app/view/term/termwrap.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ export class TermWrap {
162162
onSearchResultsDidChange?: (result: { resultIndex: number; resultCount: number }) => void;
163163
private toDispose: TermTypes.IDisposable[] = [];
164164
pasteActive: boolean = false;
165+
lastUpdated: number;
165166

166167
constructor(
167168
blockId: string,
@@ -175,6 +176,7 @@ export class TermWrap {
175176
this.ptyOffset = 0;
176177
this.dataBytesProcessed = 0;
177178
this.hasResized = false;
179+
this.lastUpdated = Date.now();
178180
this.terminal = new Terminal(options);
179181
this.fitAddon = new FitAddon();
180182
this.fitAddon.noScrollbar = PLATFORM === PlatformMacOS;
@@ -334,6 +336,7 @@ export class TermWrap {
334336
this.ptyOffset += data.length;
335337
this.dataBytesProcessed += data.length;
336338
}
339+
this.lastUpdated = Date.now();
337340
resolve();
338341
});
339342
return prtn;

frontend/types/gotypes.d.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,20 @@ declare global {
269269
data: ObjRTInfo;
270270
};
271271

272+
// wshrpc.CommandTermGetScrollbackLinesData
273+
type CommandTermGetScrollbackLinesData = {
274+
linestart: number;
275+
lineend: number;
276+
};
277+
278+
// wshrpc.CommandTermGetScrollbackLinesRtnData
279+
type CommandTermGetScrollbackLinesRtnData = {
280+
totallines: number;
281+
linestart: number;
282+
lines: string[];
283+
lastupdated: number;
284+
};
285+
272286
// wshrpc.CommandVarData
273287
type CommandVarData = {
274288
key: string;

pkg/aiusechat/tools.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,8 @@ func generateToolsForBlock(block *waveobj.Block) []uctypes.ToolDefinition {
180180

181181
var tools []uctypes.ToolDefinition
182182
switch viewType {
183+
case "term":
184+
tools = append(tools, GetTermGetScrollbackToolDefinition(block))
183185
case "web":
184186
tools = append(tools, GetWebNavigateToolDefinition(block))
185187
case "tsunami":
@@ -253,6 +255,101 @@ func GetWebNavigateToolDefinition(block *waveobj.Block) uctypes.ToolDefinition {
253255
}
254256
}
255257

258+
func GetTermGetScrollbackToolDefinition(block *waveobj.Block) uctypes.ToolDefinition {
259+
blockIdPrefix := block.OID[:8]
260+
toolName := fmt.Sprintf("term_get_scrollback_%s", blockIdPrefix)
261+
262+
return uctypes.ToolDefinition{
263+
Name: toolName,
264+
DisplayName: fmt.Sprintf("Get Terminal Scrollback %s", blockIdPrefix),
265+
Description: fmt.Sprintf("Fetch terminal scrollback from widget %s as plain text. Index 0 is the most recent line; indices increase going upward (older lines).", blockIdPrefix),
266+
Strict: false,
267+
InputSchema: map[string]any{
268+
"type": "object",
269+
"properties": map[string]any{
270+
"linestart": map[string]any{
271+
"type": "integer",
272+
"minimum": 0,
273+
"description": "Logical start index where 0 = most recent line (default: 0)",
274+
},
275+
"lineend": map[string]any{
276+
"type": "integer",
277+
"minimum": 0,
278+
"description": "Exclusive end index. Returns lines [linestart, lineend)",
279+
},
280+
"count": map[string]any{
281+
"type": "integer",
282+
"minimum": 1,
283+
"description": "Alternative to lineend: number of lines to return from linestart (default: 200)",
284+
},
285+
},
286+
"required": []string{},
287+
"additionalProperties": false,
288+
},
289+
ToolAnyCallback: func(input any) (any, error) {
290+
const DEFAULT_COUNT = 200
291+
const MAX_COUNT = 1000
292+
293+
inputMap := make(map[string]any)
294+
if input != nil {
295+
var ok bool
296+
inputMap, ok = input.(map[string]any)
297+
if !ok {
298+
return nil, fmt.Errorf("invalid input format")
299+
}
300+
}
301+
302+
lineStart := 0
303+
if val, ok := inputMap["linestart"].(float64); ok {
304+
lineStart = int(val)
305+
}
306+
307+
count := DEFAULT_COUNT
308+
if val, ok := inputMap["count"].(float64); ok {
309+
count = int(val)
310+
} else if lineEndVal, ok := inputMap["lineend"].(float64); ok {
311+
lineEnd := int(lineEndVal)
312+
count = lineEnd - lineStart
313+
}
314+
315+
count = min(count, MAX_COUNT)
316+
if count < 0 {
317+
count = 0
318+
}
319+
lineEnd := lineStart + count
320+
321+
rpcClient := wshclient.GetBareRpcClient()
322+
result, err := wshclient.TermGetScrollbackLinesCommand(
323+
rpcClient,
324+
wshrpc.CommandTermGetScrollbackLinesData{
325+
LineStart: lineStart,
326+
LineEnd: lineEnd,
327+
},
328+
&wshrpc.RpcOpts{Route: wshutil.MakeFeBlockRouteId(block.OID)},
329+
)
330+
if err != nil {
331+
return nil, fmt.Errorf("failed to get terminal scrollback: %w", err)
332+
}
333+
334+
content := strings.Join(result.Lines, "\n")
335+
sinceLastOutputSec := 0
336+
if result.LastUpdated > 0 {
337+
sinceLastOutputSec = max(0, int((time.Now().UnixMilli()-result.LastUpdated)/1000))
338+
}
339+
340+
return map[string]any{
341+
"totallines": result.TotalLines,
342+
"linestart": result.LineStart,
343+
"lineend": min(lineEnd, result.TotalLines),
344+
"returned_lines": len(result.Lines),
345+
"content": content,
346+
"since_last_output_sec": sinceLastOutputSec,
347+
"has_more": lineEnd < result.TotalLines,
348+
}, nil
349+
},
350+
}
351+
}
352+
256353
func makeTsunamiGetCallback(status *blockcontroller.BlockControllerRuntimeStatus, apiPath string) func(any) (any, error) {
257354
return func(input any) (any, error) {
258355
if status.TsunamiPort == 0 {

pkg/wshrpc/wshclient/wshclient.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,12 @@ func StreamWaveAiCommand(w *wshutil.WshRpc, data wshrpc.WaveAIStreamRequest, opt
535535
return sendRpcRequestResponseStreamHelper[wshrpc.WaveAIPacketType](w, "streamwaveai", data, opts)
536536
}
537537

538+
// command "termgetscrollbacklines", wshserver.TermGetScrollbackLinesCommand
539+
func TermGetScrollbackLinesCommand(w *wshutil.WshRpc, data wshrpc.CommandTermGetScrollbackLinesData, opts *wshrpc.RpcOpts) (*wshrpc.CommandTermGetScrollbackLinesRtnData, error) {
540+
resp, err := sendRpcRequestCallHelper[*wshrpc.CommandTermGetScrollbackLinesRtnData](w, "termgetscrollbacklines", data, opts)
541+
return resp, err
542+
}
543+
538544
// command "test", wshserver.TestCommand
539545
func TestCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error {
540546
_, err := sendRpcRequestCallHelper[any](w, "test", data, opts)

pkg/wshrpc/wshrpctypes.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ const (
146146

147147
Command_GetRTInfo = "getrtinfo"
148148
Command_SetRTInfo = "setrtinfo"
149+
150+
Command_TermGetScrollbackLines = "termgetscrollbacklines"
149151
)
150152

151153
type RespOrErrorUnion[T any] struct {
@@ -275,6 +277,9 @@ type WshRpcInterface interface {
275277
GetRTInfoCommand(ctx context.Context, data CommandGetRTInfoData) (*waveobj.ObjRTInfo, error)
276278
SetRTInfoCommand(ctx context.Context, data CommandSetRTInfoData) error
277279

280+
// terminal
281+
TermGetScrollbackLinesCommand(ctx context.Context, data CommandTermGetScrollbackLinesData) (*CommandTermGetScrollbackLinesRtnData, error)
282+
278283
// proc
279284
VDomRenderCommand(ctx context.Context, data vdom.VDomFrontendUpdate) chan RespOrErrorUnion[*vdom.VDomBackendUpdate]
280285
VDomUrlRequestCommand(ctx context.Context, data VDomUrlRequestData) chan RespOrErrorUnion[VDomUrlRequestResponse]
@@ -819,3 +824,15 @@ type CommandSetRTInfoData struct {
819824
ORef waveobj.ORef `json:"oref"`
820825
Data map[string]any `json:"data" tstype:"ObjRTInfo"`
821826
}
827+
828+
type CommandTermGetScrollbackLinesData struct {
829+
LineStart int `json:"linestart"`
830+
LineEnd int `json:"lineend"`
831+
}
832+
833+
type CommandTermGetScrollbackLinesRtnData struct {
834+
TotalLines int `json:"totallines"`
835+
LineStart int `json:"linestart"`
836+
Lines []string `json:"lines"`
837+
LastUpdated int64 `json:"lastupdated"`
838+
}

0 commit comments

Comments
 (0)