Skip to content
Merged
921 changes: 921 additions & 0 deletions .kilocode/skills/add-wshcmd/SKILL.md

Large diffs are not rendered by default.

104 changes: 104 additions & 0 deletions cmd/wsh/cmd/wshcmd-termscrollback.go
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)
Comment on lines +66 to +68
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Ambiguous error when the view-type assertion fails.

When !ok (key absent or non-string value), viewType is "", producing "block ... is not a terminal block (view type: )". It's worth distinguishing the two cases:

📝 Proposed fix
- 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)
- }
+ viewType, ok := metaData[waveobj.MetaKey_View].(string)
+ if !ok {
+     return fmt.Errorf("block %s is not a terminal block (no view type in metadata)", fullORef.OID)
+ }
+ if viewType != "term" {
+     return fmt.Errorf("block %s is not a terminal block (view type: %s)", fullORef.OID, viewType)
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)
viewType, ok := metaData[waveobj.MetaKey_View].(string)
if !ok {
return fmt.Errorf("block %s is not a terminal block (no view type in metadata)", fullORef.OID)
}
if viewType != "term" {
return fmt.Errorf("block %s is not a terminal block (view type: %s)", fullORef.OID, viewType)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/wsh/cmd/wshcmd-termscrollback.go` around lines 66 - 68, The error message
from the view-type check in the TermScRollback handler is ambiguous when the map
assertion fails; update the logic around metaData[waveobj.MetaKey_View] /
viewType / ok so you distinguish "key missing or not a string" from "present but
not 'term'": if !ok return an error that the view key is missing or not a string
(include the raw value via a safe %#v of metaData[waveobj.MetaKey_View] or note
it's absent), otherwise when viewType != "term" return an error stating the
unexpected view type and include viewType and fullORef.OID to aid debugging.

}

// 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)
}
fmt.Printf("terminal scrollback written to %s (%d lines)\n", termScrollbackOutputFile, len(result.Lines))
} else {
fmt.Print(output)
}

return nil
}
51 changes: 51 additions & 0 deletions docs/docs/wsh-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

--start/--end descriptions are inverted relative to the implementation.

The docs say --start 0 = "beginning" (implying oldest/top) and --end 0 = "all lines". The frontend handler in term-wsh.tsx implements a bottom-up coordinate system:

const startBufferIndex = totalLines - endLine;
const endBufferIndex   = totalLines - startLine;

With the defaults (linestart=0, lineend=0), endLine = Math.min(totalLines, 0) = 0, so startBufferIndex = endBufferIndex = totalLines — an empty range. Running wsh termscrollback with no flags produces zero output, directly contradicting the claim that it "retrieves all lines from the current terminal block."

The actual semantics appear to be:

  • --start N: skip the N most-recent lines (0 = include all recent lines)
  • --end N: include up to N lines counting from the most recent (0 currently produces nothing, but docs imply it means "all lines")

The docs should be corrected, and the frontend handler needs to handle the lineend=0 sentinel:

📝 Suggested doc correction + code fix

Update 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 frontend/app/view/term/term-wsh.tsx (non-lastcommand branch):

- const endLine = Math.min(totalLines, data.lineend);
+ const endLine = data.lineend === 0 ? totalLines : Math.min(totalLines, data.lineend);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/docs/wsh-reference.mdx` around lines 807 - 812, Docs and frontend are
inconsistent: the bottom-up indexing in term-wsh.tsx (using totalLines - endLine
and totalLines - startLine) makes the current defaults produce an empty range;
update docs in docs/wsh-reference.mdx to describe that --start N skips the N
most-recent lines and --end N includes up to N lines from the most-recent (with
0 meaning "all" in docs), and change the frontend handler in
frontend/app/view/term/term-wsh.tsx to treat endLine===0 as a sentinel for "all
lines" (e.g. set endLine = totalLines when endLine === 0 before computing
startBufferIndex/endBufferIndex) and ensure startLine/endLine clamping uses
Math.min/Math.max consistently so startBufferIndex <= endBufferIndex and the
non-lastcommand branch returns all lines by default.

- `-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.
Expand Down
23 changes: 23 additions & 0 deletions emain/emain-ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,4 +470,27 @@ export function initIpcHandlers() {
electron.ipcMain.on("do-refresh", (event) => {
event.sender.reloadIgnoringCache();
});

electron.ipcMain.handle("save-text-file", async (event, fileName: string, content: string) => {
const ww = focusedWaveWindow;
if (ww == null) {
return false;
}
const result = await electron.dialog.showSaveDialog(ww, {
title: "Save Scrollback",
defaultPath: fileName || "session.log",
filters: [{ name: "Text Files", extensions: ["txt", "log"] }],
});
if (result.canceled || !result.filePath) {
return false;
}
try {
await fs.promises.writeFile(result.filePath, content, "utf-8");
console.log("saved scrollback to", result.filePath);
return true;
} catch (err) {
console.error("error saving scrollback file", err);
return false;
}
});
}
1 change: 1 addition & 0 deletions emain/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ contextBridge.exposeInMainWorld("api", {
openBuilder: (appId?: string) => ipcRenderer.send("open-builder", appId),
setBuilderWindowAppId: (appId: string) => ipcRenderer.send("set-builder-window-appid", appId),
doRefresh: () => ipcRenderer.send("do-refresh"),
saveTextFile: (fileName: string, content: string) => ipcRenderer.invoke("save-text-file", fileName, content),
});

// Custom event for "new-window"
Expand Down
31 changes: 31 additions & 0 deletions frontend/app/view/term/term-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Silent no-op when termRef.current is null lacks user feedback

If this.termRef.current is null, the click handler exits silently — no modal, no toast. This is asymmetric with the !content branch which surfaces a MessageModal. While the terminal ref should always be set when a context menu is triggered, the inconsistency is surprising.

🛡️ 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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.",
});
}
}
click: () => {
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.",
});
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/view/term/term-model.ts` around lines 918 - 941, The click
handler currently silently no-ops when this.termRef.current is null; update the
click callback in the same handler (the function that calls
this.termRef.current.getScrollbackContent()) to surface a user-facing message
via modalsModel.pushModal (similar to the !content branch) when termRef.current
is null (e.g., push a MessageModal stating the terminal is unavailable or could
not be accessed) so users receive consistent feedback; keep the existing flow
for the content and error branches unchanged.

},
});
fullMenu.push({ type: "separator" });

const submenu: ContextMenuItem[] = termThemeKeys.map((themeName) => {
return {
label: termThemes[themeName]["display:name"] ?? themeName,
Expand Down
49 changes: 23 additions & 26 deletions frontend/app/view/term/term-wsh.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -120,36 +121,38 @@ 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 f

Repository: wavetermdev/waveterm

Length of output: 294


🏁 Script executed:

# Search for lineStart usage in the TypeScript/JavaScript files
rg -n 'lineStart|linestart' --type=ts --type=tsx

Repository: wavetermdev/waveterm

Length of output: 91


🏁 Script executed:

fd 'term-wsh.tsx' --type f

Repository: 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 -200

Repository: wavetermdev/waveterm

Length of output: 8293


🏁 Script executed:

# Also search for bufferLinesToText to understand the line conversion
rg -n 'bufferLinesToText' --type ts

Repository: wavetermdev/waveterm

Length of output: 805


🏁 Script executed:

sed -n '334,380p' frontend/app/view/term/termutil.ts

Repository: 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.go

Repository: 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 -B2

Repository: wavetermdev/waveterm

Length of output: 1819


🏁 Script executed:

# Check if there are other consumers of this RPC response
rg -n 'TermGetScrollbackLines' frontend/ -A3 -B3

Repository: wavetermdev/waveterm

Length of output: 2129


🏁 Script executed:

# Search for other places that call TermGetScrollbackLinesCommand and use the result
rg -n 'TermGetScrollbackLinesCommand' frontend/ -A5 -B2

Repository: 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 -A3

Repository: 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 -A3

Repository: 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 -A15

Repository: 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 -A2

Repository: 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.go

Repository: 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.tsx

Repository: 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.tsx

Repository: 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 -A10

Repository: 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.go

Repository: wavetermdev/waveterm

Length of output: 1532


returnStartLine trim calculation mixes logical and physical line counts; coordinate semantics are inconsistent between code paths.

The trim logic at line 155 computes returnStartLine = startBufferIndex + (lines.length - 1000), but this is incorrect when terminal lines wrap. The bufferLinesToText function merges wrapped physical lines into logical lines, so lines.length is a logical line count. When added to startBufferIndex (a physical buffer index), the result is inaccurate — for example, if 50 wrapped physical lines produce 25 logical lines, the offset calculation would be off by 25 lines.

Additionally, the lastcommand path returns linestart as an absolute buffer index (0 = oldest line), while the non-lastcommand path returns it as a scroll-from-bottom coordinate (0 = newest line). Although these paths are currently isolated, this semantic inconsistency could lead to bugs if the response handling is refactored or reused.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/view/term/term-wsh.tsx` around lines 150 - 155, The trim logic
mixes logical lines from bufferLinesToText with a physical index
(startBufferIndex): replace the current calculation of returnStartLine =
startBufferIndex + (lines.length - 1000) with a mapping that converts the
trimmed logical-start to the corresponding physical buffer index (i.e., count
how many physical lines were dropped or walk the original physical buffer
starting at startBufferIndex to find the physical index that corresponds to the
new logical-first line). Also normalize the coordinate semantics so both the
lastcommand path and the non-lastcommand path return the same coordinate type
(prefer returning an absolute buffer index like linestart) and update any
callers accordingly; use identifiers returnLines, returnStartLine,
startBufferIndex, bufferLinesToText, lastcommand, and linestart to locate and
change the logic.

}

return {
Expand All @@ -161,17 +164,11 @@ 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));
}
}
const endLine = data.lineend === 0 ? totalLines : Math.min(totalLines, data.lineend);

lines.reverse();
const startBufferIndex = totalLines - endLine;
Copy link
Contributor

Choose a reason for hiding this comment

The 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, startLine and endLine are line numbers from the beginning (0 = first line). In xterm.js, buffer index 0 is also the oldest/first line.

Current calculation:

  • If user requests lines 100-200 from a 1000-line buffer
  • startBufferIndex = 1000 - 200 = 800
  • endBufferIndex = 1000 - 100 = 900
  • This returns lines 800-900, NOT lines 100-200

The correct conversion should be:

const startBufferIndex = startLine;
const endBufferIndex = endLine;

This will cause the wsh termscrollback --start X --end Y command to return completely wrong line ranges.

const endBufferIndex = totalLines - startLine;
const lines = bufferLinesToText(buffer, startBufferIndex, endBufferIndex);

return {
totallines: totalLines,
Expand Down
Loading
Loading