Skip to content

Commit 2e0b3d2

Browse files
authored
implement more OSC 16162 for fish, pwsh, and bash (#2462)
1 parent 6bfb9e6 commit 2e0b3d2

File tree

17 files changed

+664
-98
lines changed

17 files changed

+664
-98
lines changed

aiprompts/wave-osc-16162.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ Sends shell metadata information (typically only once at shell initialization).
7272
shell?: string; // Shell name (e.g., "zsh", "bash")
7373
shellversion?: string; // Version string of the shell
7474
uname?: string; // Output of "uname -smr" (e.g., "Darwin 23.0.0 arm64")
75+
integration?: boolean; // Whether shell integration is active (true) or disabled (false)
7576
}
7677
```
7778

cmd/wsh/cmd/wshcmd-ssh.go

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,18 +89,6 @@ func sshRun(cmd *cobra.Command, args []string) (rtnErr error) {
8989
if err != nil {
9090
return fmt.Errorf("setting connection in block: %w", err)
9191
}
92-
93-
// Clear the cmd:hascurcwd rtinfo field
94-
rtInfoData := wshrpc.CommandSetRTInfoData{
95-
ORef: waveobj.MakeORef(waveobj.OType_Block, blockId),
96-
Data: map[string]any{
97-
"cmd:hascurcwd": nil,
98-
},
99-
}
100-
err = wshclient.SetRTInfoCommand(RpcClient, rtInfoData, nil)
101-
if err != nil {
102-
return fmt.Errorf("setting RTInfo in block: %w", err)
103-
}
10492
WriteStderr("switched connection to %q\n", sshArg)
10593
return nil
10694
}

frontend/app/modals/conntypeahead.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -404,14 +404,6 @@ const ChangeConnectionBlockModal = React.memo(
404404
meta: { connection: connName, file: newFile, "cmd:cwd": null },
405405
});
406406

407-
const rtInfo = { "cmd:hascurcwd": null };
408-
const rtInfoData: CommandSetRTInfoData = {
409-
oref: WOS.makeORef("block", blockId),
410-
data: rtInfo
411-
};
412-
RpcApi.SetRTInfoCommand(TabRpcClient, rtInfoData).catch((e) =>
413-
console.log("error setting RT info", e)
414-
);
415407
try {
416408
await RpcApi.ConnEnsureCommand(
417409
TabRpcClient,

frontend/app/view/term/termwrap.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,12 @@ function handleOsc7Command(data: string, blockId: string, loaded: boolean): bool
141141
// Strip leading slash and normalize to forward slashes
142142
pathPart = pathPart.substring(1).replace(/\\/g, "/");
143143
}
144+
145+
// Handle UNC paths (e.g., /\\server\share)
146+
if (pathPart.startsWith("/\\\\")) {
147+
// Strip leading slash but keep backslashes for UNC
148+
pathPart = pathPart.substring(1);
149+
}
144150
} catch (e) {
145151
console.log("Invalid OSC 7 command received (parse error)", data, e);
146152
return true;
@@ -152,7 +158,7 @@ function handleOsc7Command(data: string, blockId: string, loaded: boolean): bool
152158
"cmd:cwd": pathPart,
153159
});
154160

155-
const rtInfo = { "cmd:hascurcwd": true };
161+
const rtInfo = { "shell:hascurcwd": true };
156162
const rtInfoData: CommandSetRTInfoData = {
157163
oref: WOS.makeORef("block", blockId),
158164
data: rtInfo,
@@ -170,7 +176,7 @@ function handleOsc7Command(data: string, blockId: string, loaded: boolean): bool
170176
type Osc16162Command =
171177
| { command: "A"; data: {} }
172178
| { command: "C"; data: { cmd64?: string } }
173-
| { command: "M"; data: { shell?: string; shellversion?: string; uname?: string } }
179+
| { command: "M"; data: { shell?: string; shellversion?: string; uname?: string; integration?: boolean } }
174180
| { command: "D"; data: { exitcode?: number } }
175181
| { command: "I"; data: { inputempty?: boolean } }
176182
| { command: "R"; data: {} };
@@ -219,6 +225,8 @@ function handleOsc16162Command(data: string, blockId: string, loaded: boolean, t
219225
} else {
220226
rtInfo["shell:lastcmd"] = null;
221227
}
228+
// also clear lastcmdexitcode (since we've now started a new command)
229+
rtInfo["shell:lastcmdexitcode"] = null;
222230
break;
223231
case "M":
224232
if (cmd.data.shell) {
@@ -230,6 +238,9 @@ function handleOsc16162Command(data: string, blockId: string, loaded: boolean, t
230238
if (cmd.data.uname) {
231239
rtInfo["shell:uname"] = cmd.data.uname;
232240
}
241+
if (cmd.data.integration != null) {
242+
rtInfo["shell:integration"] = cmd.data.integration;
243+
}
233244
break;
234245
case "D":
235246
if (cmd.data.exitcode != null) {

frontend/types/gotypes.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -710,11 +710,12 @@ declare global {
710710
"tsunami:title"?: string;
711711
"tsunami:shortdesc"?: string;
712712
"tsunami:schemas"?: any;
713-
"cmd:hascurcwd"?: boolean;
713+
"shell:hascurcwd"?: boolean;
714714
"shell:state"?: string;
715715
"shell:type"?: string;
716716
"shell:version"?: string;
717717
"shell:uname"?: string;
718+
"shell:integration"?: boolean;
718719
"shell:inputempty"?: boolean;
719720
"shell:lastcmd"?: string;
720721
"shell:lastcmdexitcode"?: number;

pkg/aiusechat/tools.go

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,67 @@ import (
1212
"github.com/google/uuid"
1313
"github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes"
1414
"github.com/wavetermdev/waveterm/pkg/blockcontroller"
15+
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
1516
"github.com/wavetermdev/waveterm/pkg/wavebase"
1617
"github.com/wavetermdev/waveterm/pkg/waveobj"
1718
"github.com/wavetermdev/waveterm/pkg/wstore"
1819
)
1920

21+
func makeTerminalBlockDesc(block *waveobj.Block) string {
22+
connection, hasConnection := block.Meta["connection"].(string)
23+
cwd, hasCwd := block.Meta["cmd:cwd"].(string)
24+
25+
blockORef := waveobj.MakeORef(waveobj.OType_Block, block.OID)
26+
rtInfo := wstore.GetRTInfo(blockORef)
27+
hasCurCwd := rtInfo != nil && rtInfo.ShellHasCurCwd
28+
29+
var desc string
30+
if hasConnection && connection != "" {
31+
desc = fmt.Sprintf("CLI terminal connected to %q", connection)
32+
} else {
33+
desc = "local CLI terminal"
34+
}
35+
36+
if rtInfo != nil && rtInfo.ShellType != "" {
37+
desc += fmt.Sprintf(" (%s", rtInfo.ShellType)
38+
if rtInfo.ShellVersion != "" {
39+
desc += fmt.Sprintf(" %s", rtInfo.ShellVersion)
40+
}
41+
desc += ")"
42+
}
43+
44+
if rtInfo != nil {
45+
if rtInfo.ShellIntegration {
46+
var stateStr string
47+
switch rtInfo.ShellState {
48+
case "ready":
49+
stateStr = "waiting for input"
50+
case "running-command":
51+
stateStr = "running command"
52+
if rtInfo.ShellLastCmd != "" {
53+
cmdStr := rtInfo.ShellLastCmd
54+
if len(cmdStr) > 30 {
55+
cmdStr = cmdStr[:27] + "..."
56+
}
57+
cmdJSON := utilfn.MarshalJSONString(cmdStr)
58+
stateStr = fmt.Sprintf("running command %s", cmdJSON)
59+
}
60+
default:
61+
stateStr = "state unknown"
62+
}
63+
desc += fmt.Sprintf(", %s", stateStr)
64+
} else {
65+
desc += ", no shell integration"
66+
}
67+
}
68+
69+
if hasCurCwd && hasCwd && cwd != "" {
70+
desc += fmt.Sprintf(", in directory %q", cwd)
71+
}
72+
73+
return desc
74+
}
75+
2076
func MakeBlockShortDesc(block *waveobj.Block) string {
2177
if block.Meta == nil {
2278
return ""
@@ -29,25 +85,7 @@ func MakeBlockShortDesc(block *waveobj.Block) string {
2985

3086
switch viewType {
3187
case "term":
32-
connection, hasConnection := block.Meta["connection"].(string)
33-
cwd, hasCwd := block.Meta["cmd:cwd"].(string)
34-
35-
blockORef := waveobj.MakeORef(waveobj.OType_Block, block.OID)
36-
rtInfo := wstore.GetRTInfo(blockORef)
37-
hasCurCwd := rtInfo != nil && rtInfo.CmdHasCurCwd
38-
39-
var desc string
40-
if hasConnection && connection != "" {
41-
desc = fmt.Sprintf("CLI terminal on %q", connection)
42-
} else {
43-
desc = "local CLI terminal"
44-
}
45-
46-
if hasCurCwd && hasCwd && cwd != "" {
47-
desc += fmt.Sprintf(" in directory %q", cwd)
48-
}
49-
50-
return desc
88+
return makeTerminalBlockDesc(block)
5189
case "preview":
5290
file, hasFile := block.Meta["file"].(string)
5391
connection, hasConnection := block.Meta["connection"].(string)
@@ -111,6 +149,8 @@ func GenerateTabStateAndTools(ctx context.Context, tabid string, widgetAccess bo
111149
}
112150
}
113151
tabState := GenerateCurrentTabStatePrompt(blocks, widgetAccess)
152+
// for debugging
153+
// log.Printf("TABPROMPT %s\n", tabState)
114154
var tools []uctypes.ToolDefinition
115155
if widgetAccess {
116156
tools = append(tools, GetCaptureScreenshotToolDefinition(tabid))
@@ -176,7 +216,6 @@ func GenerateCurrentTabStatePrompt(blocks []*waveobj.Block, widgetAccess bool) s
176216
}
177217
prompt.WriteString("</current_tab_state>")
178218
rtn := prompt.String()
179-
// log.Printf("%s\n", rtn)
180219
return rtn
181220
}
182221

pkg/aiusechat/tools_term.go

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import (
1111
"time"
1212

1313
"github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes"
14+
"github.com/wavetermdev/waveterm/pkg/waveobj"
1415
"github.com/wavetermdev/waveterm/pkg/wcore"
1516
"github.com/wavetermdev/waveterm/pkg/wshrpc"
1617
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
1718
"github.com/wavetermdev/waveterm/pkg/wshutil"
19+
"github.com/wavetermdev/waveterm/pkg/wstore"
1820
)
1921

2022
type TermGetScrollbackToolInput struct {
@@ -23,15 +25,22 @@ type TermGetScrollbackToolInput struct {
2325
Count int `json:"count,omitempty"`
2426
}
2527

28+
type CommandInfo struct {
29+
Command string `json:"command"`
30+
Status string `json:"status"`
31+
ExitCode *int `json:"exitcode,omitempty"`
32+
}
33+
2634
type TermGetScrollbackToolOutput struct {
27-
TotalLines int `json:"total_lines"`
28-
LineStart int `json:"line_start"`
29-
LineEnd int `json:"line_end"`
30-
ReturnedLines int `json:"returned_lines"`
31-
Content string `json:"content"`
32-
SinceLastOutputSec *int `json:"since_last_output_sec,omitempty"`
33-
HasMore bool `json:"has_more"`
34-
NextStart *int `json:"next_start"`
35+
TotalLines int `json:"totallines"`
36+
LineStart int `json:"linestart"`
37+
LineEnd int `json:"lineend"`
38+
ReturnedLines int `json:"returnedlines"`
39+
Content string `json:"content"`
40+
SinceLastOutputSec *int `json:"sincelastoutputsec,omitempty"`
41+
HasMore bool `json:"hasmore"`
42+
NextStart *int `json:"nextstart"`
43+
LastCommand *CommandInfo `json:"lastcommand,omitempty"`
3544
}
3645

3746
func parseTermGetScrollbackInput(input any) (*TermGetScrollbackToolInput, error) {
@@ -76,7 +85,7 @@ func GetTermGetScrollbackToolDefinition(tabId string) uctypes.ToolDefinition {
7685
return uctypes.ToolDefinition{
7786
Name: "term_get_scrollback",
7887
DisplayName: "Get Terminal Scrollback",
79-
Description: "Fetch terminal scrollback from a widget as plain text. Index 0 is the most recent line; indices increase going upward (older lines).",
88+
Description: "Fetch terminal scrollback from a widget as plain text. Index 0 is the most recent line; indices increase going upward (older lines). Also returns last command and exit code if shell integration is enabled.",
8089
ToolLogName: "term:getscrollback",
8190
InputSchema: map[string]any{
8291
"type": "object",
@@ -155,6 +164,24 @@ func GetTermGetScrollbackToolDefinition(tabId string) uctypes.ToolDefinition {
155164
nextStart = &effectiveLineEnd
156165
}
157166

167+
blockORef := waveobj.MakeORef(waveobj.OType_Block, fullBlockId)
168+
rtInfo := wstore.GetRTInfo(blockORef)
169+
170+
var lastCommand *CommandInfo
171+
if rtInfo != nil && rtInfo.ShellIntegration && rtInfo.ShellLastCmd != "" {
172+
cmdInfo := &CommandInfo{
173+
Command: rtInfo.ShellLastCmd,
174+
}
175+
if rtInfo.ShellState == "running-command" {
176+
cmdInfo.Status = "running"
177+
} else if rtInfo.ShellState == "ready" {
178+
cmdInfo.Status = "completed"
179+
exitCode := rtInfo.ShellLastCmdExitCode
180+
cmdInfo.ExitCode = &exitCode
181+
}
182+
lastCommand = cmdInfo
183+
}
184+
158185
return &TermGetScrollbackToolOutput{
159186
TotalLines: result.TotalLines,
160187
LineStart: result.LineStart,
@@ -164,6 +191,7 @@ func GetTermGetScrollbackToolDefinition(tabId string) uctypes.ToolDefinition {
164191
SinceLastOutputSec: sinceLastOutputSec,
165192
HasMore: hasMore,
166193
NextStart: nextStart,
194+
LastCommand: lastCommand,
167195
}, nil
168196
},
169197
}

pkg/blockcontroller/blockcontroller.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ func registerController(blockId string, controller Controller) {
9494

9595
if existingController != nil {
9696
existingController.Stop(false, Status_Done)
97+
wstore.DeleteRTInfo(waveobj.MakeORef(waveobj.OType_Block, blockId))
9798
}
9899
}
99100

@@ -243,6 +244,7 @@ func StopBlockController(blockId string) {
243244
return
244245
}
245246
controller.Stop(true, Status_Done)
247+
wstore.DeleteRTInfo(waveobj.MakeORef(waveobj.OType_Block, blockId))
246248
}
247249

248250
func StopBlockControllerAndSetStatus(blockId string, newStatus string) {
@@ -251,6 +253,7 @@ func StopBlockControllerAndSetStatus(blockId string, newStatus string) {
251253
return
252254
}
253255
controller.Stop(true, newStatus)
256+
wstore.DeleteRTInfo(waveobj.MakeORef(waveobj.OType_Block, blockId))
254257
}
255258

256259
func SendInput(blockId string, inputUnion *BlockInputUnion) error {
@@ -268,6 +271,7 @@ func StopAllBlockControllers() {
268271
if status != nil && status.ShellProcStatus == Status_Running {
269272
go func(id string, c Controller) {
270273
c.Stop(true, Status_Done)
274+
wstore.DeleteRTInfo(waveobj.MakeORef(waveobj.OType_Block, id))
271275
}(blockId, controller)
272276
}
273277
}

pkg/remote/conncontroller/conncontroller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ type SSHConn struct {
7373

7474
var ConnServerCmdTemplate = strings.TrimSpace(
7575
strings.Join([]string{
76-
"%s version 2> /dev/null || (echo -n \"not-installed \"; uname -sm);",
76+
"%s version 2> /dev/null || (echo -n \"not-installed \"; uname -sm; exit 0);",
7777
"exec %s connserver",
7878
}, "\n"))
7979

0 commit comments

Comments
 (0)