-
-
Notifications
You must be signed in to change notification settings - Fork 931
New Context Menu Item + Wsh Command to Save Scrollback of a Terminal Widget #2892
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
03ec486
62e2849
597754a
3613050
4a8a18b
42a9353
25227c8
328724b
bc49698
4a5dee6
98b6444
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| // Copyright 2025, Command Line Inc. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package cmd | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
| "strings" | ||
|
|
||
| "github.com/spf13/cobra" | ||
| "github.com/wavetermdev/waveterm/pkg/waveobj" | ||
| "github.com/wavetermdev/waveterm/pkg/wshrpc" | ||
| "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" | ||
| "github.com/wavetermdev/waveterm/pkg/wshutil" | ||
| ) | ||
|
|
||
| var termScrollbackCmd = &cobra.Command{ | ||
| Use: "termscrollback", | ||
| Short: "Get terminal scrollback from a terminal block", | ||
| Long: `Get the terminal scrollback from a terminal block. | ||
|
|
||
| By default, retrieves all lines. You can specify line ranges or get the | ||
| output of the last command using the --lastcommand flag.`, | ||
| RunE: termScrollbackRun, | ||
| PreRunE: preRunSetupRpcClient, | ||
| DisableFlagsInUseLine: true, | ||
| } | ||
|
|
||
| var ( | ||
| termScrollbackLineStart int | ||
| termScrollbackLineEnd int | ||
| termScrollbackLastCmd bool | ||
| termScrollbackOutputFile string | ||
| ) | ||
|
|
||
| func init() { | ||
| rootCmd.AddCommand(termScrollbackCmd) | ||
|
|
||
| termScrollbackCmd.Flags().IntVar(&termScrollbackLineStart, "start", 0, "starting line number (0 = beginning)") | ||
| termScrollbackCmd.Flags().IntVar(&termScrollbackLineEnd, "end", 0, "ending line number (0 = all lines)") | ||
| termScrollbackCmd.Flags().BoolVar(&termScrollbackLastCmd, "lastcommand", false, "get output of last command (requires shell integration)") | ||
| termScrollbackCmd.Flags().StringVarP(&termScrollbackOutputFile, "output", "o", "", "write output to file instead of stdout") | ||
| } | ||
|
|
||
| func termScrollbackRun(cmd *cobra.Command, args []string) (rtnErr error) { | ||
| defer func() { | ||
| sendActivity("termscrollback", rtnErr == nil) | ||
| }() | ||
|
|
||
| // Resolve the block argument | ||
| fullORef, err := resolveBlockArg() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // Get block metadata to verify it's a terminal block | ||
| metaData, err := wshclient.GetMetaCommand(RpcClient, wshrpc.CommandGetMetaData{ | ||
| ORef: *fullORef, | ||
| }, &wshrpc.RpcOpts{Timeout: 2000}) | ||
| if err != nil { | ||
| return fmt.Errorf("error getting block metadata: %w", err) | ||
| } | ||
|
|
||
| // Check if the block is a terminal block | ||
| viewType, ok := metaData[waveobj.MetaKey_View].(string) | ||
| if !ok || viewType != "term" { | ||
| return fmt.Errorf("block %s is not a terminal block (view type: %s)", fullORef.OID, viewType) | ||
| } | ||
|
|
||
| // Make the RPC call to get scrollback | ||
| scrollbackData := wshrpc.CommandTermGetScrollbackLinesData{ | ||
| LineStart: termScrollbackLineStart, | ||
| LineEnd: termScrollbackLineEnd, | ||
| LastCommand: termScrollbackLastCmd, | ||
| } | ||
|
|
||
| result, err := wshclient.TermGetScrollbackLinesCommand(RpcClient, scrollbackData, &wshrpc.RpcOpts{ | ||
| Route: wshutil.MakeFeBlockRouteId(fullORef.OID), | ||
| Timeout: 5000, | ||
| }) | ||
| if err != nil { | ||
| return fmt.Errorf("error getting terminal scrollback: %w", err) | ||
| } | ||
|
|
||
| // Format the output | ||
| output := strings.Join(result.Lines, "\n") | ||
| if len(result.Lines) > 0 { | ||
| output += "\n" // Add final newline | ||
| } | ||
|
|
||
| // Write to file or stdout | ||
| if termScrollbackOutputFile != "" { | ||
| err = os.WriteFile(termScrollbackOutputFile, []byte(output), 0644) | ||
| if err != nil { | ||
| return fmt.Errorf("error writing to file %s: %w", termScrollbackOutputFile, err) | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| fmt.Printf("terminal scrollback written to %s (%d lines)\n", termScrollbackOutputFile, len(result.Lines)) | ||
| } else { | ||
| fmt.Print(output) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -792,6 +792,57 @@ wsh setvar -b client MYVAR=value | |
|
|
||
| Variables set with these commands persist across sessions and can be used to store configuration values, secrets, or any other string data that needs to be accessible across blocks or tabs. | ||
|
|
||
| --- | ||
|
|
||
| ## termscrollback | ||
|
|
||
| Get the terminal scrollback from a terminal block. This is useful for capturing terminal output for processing or archiving. | ||
|
|
||
| ```sh | ||
| wsh termscrollback [-b blockid] [flags] | ||
| ``` | ||
|
|
||
| By default, retrieves all lines from the current terminal block. You can specify line ranges or get only the output of the last command. | ||
|
|
||
| Flags: | ||
|
|
||
| - `-b, --block <blockid>` - specify target terminal block (default: current block) | ||
| - `--start <line>` - starting line number (0 = beginning, default: 0) | ||
| - `--end <line>` - ending line number (0 = all lines, default: 0) | ||
| - `--lastcommand` - get output of last command (requires shell integration) | ||
|
Comment on lines
+807
to
+812
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The docs say const startBufferIndex = totalLines - endLine;
const endBufferIndex = totalLines - startLine;With the defaults ( The actual semantics appear to be:
The docs should be corrected, and the frontend handler needs to handle the 📝 Suggested doc correction + code fixUpdate docs: - - `--start <line>` - starting line number (0 = beginning, default: 0)
- - `--end <line>` - ending line number (0 = all lines, default: 0)
+ - `--start <line>` - lines to skip from the most recent (0 = include most recent, default: 0)
+ - `--end <line>` - total lines to return counting from the most recent (0 = all lines, default: 0)Fix in - const endLine = Math.min(totalLines, data.lineend);
+ const endLine = data.lineend === 0 ? totalLines : Math.min(totalLines, data.lineend);🤖 Prompt for AI Agents |
||
| - `-o, --output <file>` - write output to file instead of stdout | ||
|
|
||
| Examples: | ||
|
|
||
| ```sh | ||
| # Get all scrollback from current terminal | ||
| wsh termscrollback | ||
|
|
||
| # Get scrollback from a specific terminal block | ||
| wsh termscrollback -b 2 | ||
|
|
||
| # Get only the last command's output | ||
| wsh termscrollback --lastcommand | ||
|
|
||
| # Get a specific line range (lines 100-200) | ||
| wsh termscrollback --start 100 --end 200 | ||
|
|
||
| # Save scrollback to a file | ||
| wsh termscrollback -o terminal-log.txt | ||
|
|
||
| # Save last command output to a file | ||
| wsh termscrollback --lastcommand -o last-output.txt | ||
|
|
||
| # Process last command output with grep | ||
| wsh termscrollback --lastcommand | grep "ERROR" | ||
| ``` | ||
|
|
||
| :::note | ||
| The `--lastcommand` flag requires shell integration to be enabled. This feature allows you to capture just the output from the most recent command, which is particularly useful for scripting and automation. | ||
| ::: | ||
|
|
||
| --- | ||
|
|
||
| ## wavepath | ||
|
|
||
| The `wavepath` command lets you get the paths to various Wave Terminal directories and files, including configuration, data storage, and logs. | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { WaveAIModel } from "@/app/aipanel/waveai-model"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { BlockNodeModel } from "@/app/block/blocktypes"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { appHandleKeyDown } from "@/app/store/keymodel"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { modalsModel } from "@/app/store/modalmodel"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { TabModel } from "@/app/store/tab-model"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { waveEventSubscribe } from "@/app/store/wps"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { RpcApi } from "@/app/store/wshclientapi"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -912,6 +913,36 @@ export class TermViewModel implements ViewModel { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fullMenu.push({ type: "separator" }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fullMenu.push({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| label: "Save Session As...", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| click: () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (this.termRef.current) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const content = this.termRef.current.getScrollbackContent(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (content) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fireAndForget(async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const success = await getApi().saveTextFile("session.log", content); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!success) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log("Save scrollback cancelled by user"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("Failed to save scrollback:", error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const errorMessage = error?.message || "An unknown error occurred"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| modalsModel.pushModal("MessageModal", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| children: `Failed to save session scrollback: ${errorMessage}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| modalsModel.pushModal("MessageModal", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| children: "No scrollback content to save.", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+918
to
+941
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Silent no-op when If 🛡️ Proposed fix- if (this.termRef.current) {
- const content = this.termRef.current.getScrollbackContent();
- if (content) {
- // ...
- } else {
- modalsModel.pushModal("MessageModal", {
- children: "No scrollback content to save.",
- });
- }
- }
+ const content = this.termRef.current?.getScrollbackContent();
+ if (content) {
+ // ...
+ } else {
+ modalsModel.pushModal("MessageModal", {
+ children: "No scrollback content to save.",
+ });
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fullMenu.push({ type: "separator" }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const submenu: ContextMenuItem[] = termThemeKeys.map((themeName) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| label: termThemes[themeName]["display:name"] ?? themeName, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ import { RpcResponseHelper, WshClient } from "@/app/store/wshclient"; | |
| import { RpcApi } from "@/app/store/wshclientapi"; | ||
| import { makeFeBlockRouteId } from "@/app/store/wshrouter"; | ||
| import { TermViewModel } from "@/app/view/term/term-model"; | ||
| import { bufferLinesToText } from "@/app/view/term/termutil"; | ||
| import { isBlank } from "@/util/util"; | ||
| import debug from "debug"; | ||
|
|
||
|
|
@@ -120,36 +121,37 @@ export class TermWshClient extends WshClient { | |
|
|
||
| const buffer = termWrap.terminal.buffer.active; | ||
| const totalLines = buffer.length; | ||
| const lines: string[] = []; | ||
|
|
||
| if (data.lastcommand) { | ||
| if (globalStore.get(termWrap.shellIntegrationStatusAtom) == null) { | ||
| throw new Error("Cannot get last command data without shell integration"); | ||
| } | ||
|
|
||
| let startLine = 0; | ||
| let startBufferIndex = 0; | ||
| let endBufferIndex = totalLines; | ||
| 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)); | ||
| // The last marker is the current prompt, so we want the second-to-last for the previous command | ||
| // If there's only one marker, use it (edge case for first command) | ||
| const markerIndex = termWrap.promptMarkers.length > 1 | ||
| ? termWrap.promptMarkers.length - 2 | ||
| : termWrap.promptMarkers.length - 1; | ||
| const commandStartMarker = termWrap.promptMarkers[markerIndex]; | ||
| startBufferIndex = commandStartMarker.line; | ||
|
|
||
| // End at the last marker (current prompt) if there are multiple markers | ||
| if (termWrap.promptMarkers.length > 1) { | ||
| const currentPromptMarker = termWrap.promptMarkers[termWrap.promptMarkers.length - 1]; | ||
| endBufferIndex = currentPromptMarker.line; | ||
| } | ||
| } | ||
|
|
||
| lines.reverse(); | ||
| const lines = bufferLinesToText(buffer, startBufferIndex, endBufferIndex); | ||
|
|
||
| let returnLines = lines; | ||
| let returnStartLine = startLine; | ||
| let returnStartLine = startBufferIndex; | ||
| if (lines.length > 1000) { | ||
| returnLines = lines.slice(lines.length - 1000); | ||
| returnStartLine = startLine + (lines.length - 1000); | ||
| returnStartLine = startBufferIndex + (lines.length - 1000); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: rg -n --type=go -C4 'linestart|LineStart|GetScrollbackLines'Repository: wavetermdev/waveterm Length of output: 11438 🏁 Script executed: # Let's also look at the full context of the function in question
fd -n "term-wsh.tsx" -type fRepository: wavetermdev/waveterm Length of output: 294 🏁 Script executed: # Search for lineStart usage in the TypeScript/JavaScript files
rg -n 'lineStart|linestart' --type=ts --type=tsxRepository: wavetermdev/waveterm Length of output: 91 🏁 Script executed: fd 'term-wsh.tsx' --type fRepository: wavetermdev/waveterm Length of output: 99 🏁 Script executed: # Look at the full context around lines 150-155 in term-wsh.tsx
fd 'term-wsh.tsx' --type f -x cat -n {} | head -200Repository: wavetermdev/waveterm Length of output: 8293 🏁 Script executed: # Also search for bufferLinesToText to understand the line conversion
rg -n 'bufferLinesToText' --type tsRepository: wavetermdev/waveterm Length of output: 805 🏁 Script executed: sed -n '334,380p' frontend/app/view/term/termutil.tsRepository: wavetermdev/waveterm Length of output: 2047 🏁 Script executed: # Let's also check how the Go backend uses the linestart value in both paths
rg -n -B5 -A10 'effectiveLineEnd = result.LineStart' pkg/aiusechat/tools_term.goRepository: wavetermdev/waveterm Length of output: 570 🏁 Script executed: # Search for places where linestart is interpreted or used after being returned
rg -n 'LineStart|linestart' pkg/aiusechat/tools_term.go -A2 -B2Repository: wavetermdev/waveterm Length of output: 1819 🏁 Script executed: # Check if there are other consumers of this RPC response
rg -n 'TermGetScrollbackLines' frontend/ -A3 -B3Repository: wavetermdev/waveterm Length of output: 2129 🏁 Script executed: # Search for other places that call TermGetScrollbackLinesCommand and use the result
rg -n 'TermGetScrollbackLinesCommand' frontend/ -A5 -B2Repository: wavetermdev/waveterm Length of output: 772 🏁 Script executed: # Also check the CLI to see how it uses linestart
rg -n 'result.LineStart\|result\.linestart' cmd/wsh/ -B3 -A3Repository: wavetermdev/waveterm Length of output: 46 🏁 Script executed: # Search for all places that use result.linestart after the RPC call
rg -n 'result\.linestart\|result\.LineStart' --type ts --type tsx -B3 -A3Repository: wavetermdev/waveterm Length of output: 91 🏁 Script executed: # Let's trace through the AI use case which actually uses LineStart
rg -n 'getTermScrollbackOutput\|TermGetScrollbackLinesCommand' pkg/aiusechat/tools_term.go -B5 -A15Repository: wavetermdev/waveterm Length of output: 46 🏁 Script executed: # Search for usages of linestart in TypeScript files (with a simpler approach)
rg -n 'result\.linestart' frontend/ -B2 -A2Repository: wavetermdev/waveterm Length of output: 46 🏁 Script executed: # Check the full context of the AI tool that uses TermGetScrollbackOutput
sed -n '84,150p' pkg/aiusechat/tools_term.goRepository: wavetermdev/waveterm Length of output: 2068 🏁 Script executed: # Let me check the full context to understand the trim calculation better
sed -n '148,165p' frontend/app/view/term/term-wsh.tsxRepository: wavetermdev/waveterm Length of output: 653 🏁 Script executed: # And verify the exact coordinates being used - check both buffer indices used
sed -n '125,180p' frontend/app/view/term/term-wsh.tsxRepository: wavetermdev/waveterm Length of output: 2427 🏁 Script executed: # Let me verify one more thing - check if linestart from lastcommand path is actually used anywhere
# that would interpret it as scroll-from-bottom
rg -n 'lastcommand.*true\|LastCommand.*true' frontend/ -B5 -A10Repository: wavetermdev/waveterm Length of output: 46 🏁 Script executed: # Also check to confirm that nextStart is used for the NEXT request
sed -n '118,160p' pkg/aiusechat/tools_term.goRepository: wavetermdev/waveterm Length of output: 1532
The trim logic at line 155 computes Additionally, the lastcommand path returns 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| return { | ||
|
|
@@ -163,15 +165,9 @@ export class TermWshClient extends WshClient { | |
| const startLine = Math.max(0, data.linestart); | ||
| const endLine = Math.min(totalLines, data.lineend); | ||
|
|
||
| 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(); | ||
| const startBufferIndex = totalLines - endLine; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CRITICAL: Incorrect buffer index calculation The conversion from line numbers to buffer indices is backwards. According to the documentation, Current calculation:
The correct conversion should be: const startBufferIndex = startLine;
const endBufferIndex = endLine;This will cause the |
||
| const endBufferIndex = totalLines - startLine; | ||
| const lines = bufferLinesToText(buffer, startBufferIndex, endBufferIndex); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| return { | ||
| totallines: totalLines, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| export const DefaultTermTheme = "default-dark"; | ||
| import { RpcApi } from "@/app/store/wshclientapi"; | ||
| import { TabRpcClient } from "@/app/store/wshrpcutil"; | ||
| import * as TermTypes from "@xterm/xterm"; | ||
| import base64 from "base64-js"; | ||
| import { colord } from "colord"; | ||
|
|
||
|
|
@@ -319,3 +320,65 @@ export async function extractAllClipboardData(e?: ClipboardEvent): Promise<Array | |
| return results; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Converts terminal buffer lines to text, properly handling wrapped lines. | ||
| * Wrapped lines (long lines split across multiple buffer rows) are concatenated | ||
| * without adding newlines between them, while preserving actual line breaks. | ||
| * | ||
| * @param buffer - The xterm.js buffer to extract lines from | ||
| * @param startIndex - Starting buffer index (inclusive, 0-based) | ||
| * @param endIndex - Ending buffer index (exclusive, 0-based) | ||
| * @returns Array of logical lines (with wrapped lines concatenated) | ||
| */ | ||
| export function bufferLinesToText(buffer: TermTypes.IBuffer, startIndex: number, endIndex: number): string[] { | ||
| const lines: string[] = []; | ||
| let currentLine = ""; | ||
| let isFirstLine = true; | ||
|
|
||
| // Clamp indices to valid buffer range to avoid out-of-bounds access on the | ||
| // underlying circular buffer, which could return stale/wrong data. | ||
| const clampedStart = Math.max(0, Math.min(startIndex, buffer.length)); | ||
| const clampedEnd = Math.max(0, Math.min(endIndex, buffer.length)); | ||
|
|
||
| for (let i = clampedStart; i < clampedEnd; i++) { | ||
| const line = buffer.getLine(i); | ||
| if (line) { | ||
| const lineText = line.translateToString(true); | ||
| // If this line is wrapped (continuation of previous line), concatenate without newline | ||
| if (line.isWrapped && !isFirstLine) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CRITICAL: Logic bug with wrapped lines at range start When Example scenario:
Fix: Change the condition to handle the first wrapped line correctly: if (line.isWrapped) {
currentLine += lineText;
} else {
if (!isFirstLine) {
lines.push(currentLine);
}
currentLine = lineText;
}
isFirstLine = false;This ensures wrapped lines are always concatenated, regardless of whether they're the first line in the range. |
||
| currentLine += lineText; | ||
| } else { | ||
| // This is a new logical line | ||
| if (!isFirstLine) { | ||
| lines.push(currentLine); | ||
| } | ||
| currentLine = lineText; | ||
| isFirstLine = false; | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+344
to
+360
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The fix is to use 🐛 Proposed fix for (let i = clampedStart; i < clampedEnd; i++) {
const line = buffer.getLine(i);
if (line) {
- const lineText = line.translateToString(true);
+ // Trim trailing whitespace only on the last physical row of a logical
+ // line; intermediate wrapped rows may carry a significant trailing
+ // space at the wrap boundary.
+ const nextLine = i + 1 < clampedEnd ? buffer.getLine(i + 1) : undefined;
+ const trimRight = !nextLine?.isWrapped;
+ const lineText = line.translateToString(trimRight);
// If this line is wrapped (continuation of previous line), concatenate without newline
if (line.isWrapped && !isFirstLine) {
currentLine += lineText;
} else {🤖 Prompt for AI Agents |
||
|
|
||
| // Don't forget the last line | ||
| if (!isFirstLine) { | ||
| lines.push(currentLine); | ||
| } | ||
|
|
||
| // Trim trailing blank lines only when the requested range extends to the | ||
| // actual end of the buffer. A terminal allocates a fixed number of rows | ||
| // (e.g. 80) but only the first few may contain real content; the rest are | ||
| // empty placeholder rows. We strip those so callers don't receive a wall | ||
| // of empty strings. | ||
| // | ||
| // Crucially, if the caller requested a specific sub-range (e.g. lines | ||
| // 100-150) and lines 140-150 happen to be blank, those blanks are | ||
| // intentional and must NOT be removed. We only trim when the range | ||
| // reaches the very end of the buffer. | ||
| if (clampedEnd >= buffer.length) { | ||
| while (lines.length > 0 && lines[lines.length - 1] === "") { | ||
| lines.pop(); | ||
| } | ||
| } | ||
|
|
||
| return lines; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ambiguous error when the view-type assertion fails.
When
!ok(key absent or non-string value),viewTypeis"", producing"block ... is not a terminal block (view type: )". It's worth distinguishing the two cases:📝 Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents