Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
335f3f0
docs: add session daemon design docs (v1 + v2)
lyx-tec Jun 7, 2026
b32ae21
docs: update v2 - anonymous daemon for SSH, 1h idle timeout
lyx-tec Jun 7, 2026
f9b7f3f
feat: session daemon full implementation
lyx-tec Jun 8, 2026
de7cc8a
chore: stop tracking .kilocode/, CLAUDE.md (ai config); add to .gitig…
lyx-tec Jun 8, 2026
10788cb
chore: stop tracking .roo/ (ai config)
lyx-tec Jun 8, 2026
59871b9
chore: add .roo to .gitignore
lyx-tec Jun 8, 2026
bb184b9
Revert "chore: add .roo to .gitignore"
lyx-tec Jun 8, 2026
96b6861
Revert "chore: stop tracking .roo/ (ai config)"
lyx-tec Jun 8, 2026
2981f0e
Revert "chore: stop tracking .kilocode/, CLAUDE.md (ai config); add t…
lyx-tec Jun 8, 2026
76d4382
fix: restore block-zone output mirror + ReconnectJob in SessionDaemon…
lyx-tec Jun 9, 2026
6d082cd
fix: remove ReconnectJob for new jobs (StartJob already sets up strea…
lyx-tec Jun 9, 2026
a2284b6
chore: add sessiondaemon debug logs (backend Go)
lyx-tec Jun 9, 2026
66ad248
fix: handle stale daemon (status=done) — clear SessionDaemonId and au…
lyx-tec Jun 9, 2026
7dc8a72
feat: session list popup in header — click link icon to switch sessions
lyx-tec Jun 9, 2026
8eaaa02
Fix session menu clipping and package ordering
lyx-tec Jun 10, 2026
aa99901
Update shared session attach handling
lyx-tec Jun 10, 2026
8d315e3
fix(session-daemon): extend anonymous idle timeout to 10min
lyx-tec Jun 12, 2026
7ba5ba5
fix: session switching output duplication, stale job recovery, and se…
lyx-tec Jun 14, 2026
553a3d1
session: name display, rename, sorting, and cross-block sync
lyx-tec Jun 14, 2026
fdf351d
feat: named session creation, universal create button, and floating-u…
lyx-tec Jun 14, 2026
9f39b10
feat: session daemon state machine and full DB load on startup
lyx-tec Jun 15, 2026
dd23cdb
fix: daemon Stop returns error, block DB delete on terminate failure
lyx-tec Jun 15, 2026
04fe60f
fix: session daemon jobs use daemon: prefix for AttachedBlockId to pr…
lyx-tec Jun 15, 2026
3f1f1a6
fix: restore MakeFile call for term file in SessionDaemonController.S…
lyx-tec Jun 15, 2026
35f1dee
feat: add pencil icon hint on session name in popup list
lyx-tec Jun 15, 2026
cf11192
feat: remote jobmanager idle timeout with centralized disconnect mana…
lyx-tec Jun 15, 2026
941b41b
fix: session daemon reliability - race conditions, memory/DB consiste…
lyx-tec Jun 16, 2026
b15cbbc
feat: session list filter by connection, cross-connection attach guar…
lyx-tec Jun 16, 2026
86a4d49
fix: handle dead remote job gracefully - auto-recover, direct delete,…
lyx-tec Jun 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@.kilocode/rules/rules.md

---

## Skill Guides

This project uses a set of "skill" guides — focused how-to documents for common implementation tasks. When your task matches one of the descriptions below, **read the linked SKILL.md file before proceeding** and follow its instructions precisely.

| Skill | File | Description |
| ------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| add-config | `.kilocode/skills/add-config/SKILL.md` | Guide for adding new configuration settings to Wave Terminal. Use when adding a new setting to the configuration system, implementing a new config key, or adding user-customizable settings. |
| add-rpc | `.kilocode/skills/add-rpc/SKILL.md` | Guide for adding new RPC calls to Wave Terminal. Use when implementing new RPC commands, adding server-client communication methods, or extending the RPC interface with new functionality. |
| add-wshcmd | `.kilocode/skills/add-wshcmd/SKILL.md` | Guide for adding new wsh commands to Wave Terminal. Use when implementing new CLI commands, adding command-line functionality, or extending the wsh command interface. |
| context-menu | `.kilocode/skills/context-menu/SKILL.md` | Guide for creating and displaying context menus in Wave Terminal. Use when implementing right-click menus, adding context menu items, creating submenus, or handling menu interactions with checkboxes and separators. |
| create-view | `.kilocode/skills/create-view/SKILL.md` | Guide for implementing a new view type in Wave Terminal. Use when creating a new view component, implementing the ViewModel interface, registering a new view type in BlockRegistry, or adding a new content type to display within blocks. |
| electron-api | `.kilocode/skills/electron-api/SKILL.md` | Guide for adding new Electron APIs to Wave Terminal. Use when implementing new frontend-to-electron communications via preload/IPC. |
| waveenv | `.kilocode/skills/waveenv/SKILL.md` | Guide for creating WaveEnv narrowings in Wave Terminal. Use when writing a named subset type of WaveEnv for a component tree, documenting environmental dependencies, or enabling mock environments for preview/test server usage. |
| wps-events | `.kilocode/skills/wps-events/SKILL.md` | Guide for working with Wave Terminal's WPS (Wave PubSub) event system. Use when implementing new event types, publishing events, subscribing to events, or adding asynchronous communication between components. |
9 changes: 4 additions & 5 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,11 @@ tasks:
package:
desc: Package the application for the current platform.
cmds:
- task: clean
- task: npm:install
- task: build:backend
- task: build:tsunamiscaffold
- npm run build:prod && npm exec electron-builder -- -c electron-builder.config.cjs -p never {{.CLI_ARGS}}
deps:
- clean
- npm:install
- build:backend
- build:tsunamiscaffold

build:frontend:dev:
desc: Build the frontend in development mode.
Expand Down
12 changes: 12 additions & 0 deletions cmd/server/main-server.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/wavetermdev/waveterm/pkg/remote/conncontroller"
"github.com/wavetermdev/waveterm/pkg/remote/fileshare/wshfs"
"github.com/wavetermdev/waveterm/pkg/secretstore"
"github.com/wavetermdev/waveterm/pkg/sessiondaemon"
"github.com/wavetermdev/waveterm/pkg/service"
"github.com/wavetermdev/waveterm/pkg/telemetry"
"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata"
Expand Down Expand Up @@ -525,6 +526,10 @@ func main() {
log.Printf("error initializing wstore: %v\n", err)
return
}
err = wstore.RunSessionDaemonMigration(context.Background())
if err != nil {
log.Printf("error running session daemon migration: %v\n", err)
}
panichandler.PanicTelemetryHandler = panicTelemetryHandler
go func() {
defer func() {
Expand Down Expand Up @@ -554,6 +559,13 @@ func main() {
return
}

ctx := context.Background()
err = sessiondaemon.Manager.InitFromDB(ctx)
if err != nil {
log.Printf("error initializing session daemon manager: %v\n", err)
}
sessiondaemon.Manager.StartIdleReaper(ctx)

err = shellutil.FixupWaveZshHistory()
if err != nil {
log.Printf("error fixing up wave zsh history: %v\n", err)
Expand Down
277 changes: 277 additions & 0 deletions cmd/wsh/cmd/wshcmd-session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"fmt"
"time"

"github.com/spf13/cobra"
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
)

var sessionCmd = &cobra.Command{
Use: "session",
Short: "manage session daemons",
Long: "Commands to create, list, attach to, and manage session daemons for persistent remote shells.",
}

var sessionCreateCmd = &cobra.Command{
Use: "create",
Short: "create a new session daemon",
Long: `Create a named session daemon. Anonymous daemons are created automatically for SSH blocks.`,
Args: cobra.NoArgs,
RunE: sessionCreateRun,
PreRunE: preRunSetupRpcClient,
}

var sessionDeleteCmd = &cobra.Command{
Use: "delete DAEMONID",
Short: "delete a session daemon",
Long: `Delete a session daemon, stopping any attached job and detaching all blocks.`,
Args: cobra.ExactArgs(1),
RunE: sessionDeleteRun,
PreRunE: preRunSetupRpcClient,
}

var sessionListCmd = &cobra.Command{
Use: "list",
Short: "list session daemons",
Long: `List all named session daemons. Use --all to include anonymous daemons.`,
Args: cobra.NoArgs,
RunE: sessionListRun,
PreRunE: preRunSetupRpcClient,
}

var sessionAttachCmd = &cobra.Command{
Use: "attach DAEMONID",
Short: "attach current block to a session daemon",
Long: `Attach the current block to the specified session daemon.`,
Args: cobra.ExactArgs(1),
RunE: sessionAttachRun,
PreRunE: preRunSetupRpcClient,
}

var sessionDetachCmd = &cobra.Command{
Use: "detach",
Short: "detach current block from its session daemon",
Long: `Detach the current block from its attached session daemon.`,
Args: cobra.NoArgs,
RunE: sessionDetachRun,
PreRunE: preRunSetupRpcClient,
}

var sessionInfoCmd = &cobra.Command{
Use: "info DAEMONID",
Short: "show session daemon info",
Long: `Show detailed information about a session daemon.`,
Args: cobra.ExactArgs(1),
RunE: sessionInfoRun,
PreRunE: preRunSetupRpcClient,
}

var sessionTagCmd = &cobra.Command{
Use: "tag DAEMONID",
Short: "tag an anonymous session daemon with a name",
Long: `Convert an anonymous session daemon to a named one, preventing auto-cleanup.`,
Args: cobra.ExactArgs(1),
RunE: sessionTagRun,
PreRunE: preRunSetupRpcClient,
}

var sessionCreateFlagName string
var sessionCreateFlagConnection string
var sessionCreateFlagIdleTimeout int64
var sessionListFlagAll bool
var sessionTagFlagName string

func init() {
rootCmd.AddCommand(sessionCmd)
sessionCmd.AddCommand(sessionCreateCmd)
sessionCmd.AddCommand(sessionDeleteCmd)
sessionCmd.AddCommand(sessionListCmd)
sessionCmd.AddCommand(sessionAttachCmd)
sessionCmd.AddCommand(sessionDetachCmd)
sessionCmd.AddCommand(sessionInfoCmd)
sessionCmd.AddCommand(sessionTagCmd)

sessionCreateCmd.Flags().StringVarP(&sessionCreateFlagName, "name", "n", "", "session name (creates a named daemon)")
sessionCreateCmd.Flags().StringVarP(&sessionCreateFlagConnection, "connection", "c", "", "connection name (e.g. ssh://host)")
sessionCreateCmd.Flags().Int64Var(&sessionCreateFlagIdleTimeout, "idle-timeout", 0, "idle timeout in seconds (default: 86400 for named, 60 for anonymous)")

sessionListCmd.Flags().BoolVarP(&sessionListFlagAll, "all", "a", false, "include anonymous session daemons")

sessionTagCmd.Flags().StringVarP(&sessionTagFlagName, "name", "n", "", "new name for the session daemon")
sessionTagCmd.MarkFlagRequired("name")
}

func sessionCreateRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
sendActivity("session:create", rtnErr == nil)
}()

data := wshrpc.CommandSessionCreateData{
Name: sessionCreateFlagName,
Connection: sessionCreateFlagConnection,
IdleTimeout: sessionCreateFlagIdleTimeout,
}

info, err := wshclient.SessionCreateCommand(RpcClient, data, &wshrpc.RpcOpts{Timeout: 10000})
if err != nil {
return fmt.Errorf("creating session daemon: %w", err)
}

WriteStdout("session daemon %s created\n", info.DaemonId)
WriteStdout(" name: %s\n", info.Name)
WriteStdout(" connection: %s\n", info.Connection)
return nil
}

func sessionDeleteRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
sendActivity("session:delete", rtnErr == nil)
}()

daemonId := args[0]
err := wshclient.SessionDeleteCommand(RpcClient, wshrpc.CommandSessionDeleteData{DaemonId: daemonId}, &wshrpc.RpcOpts{Timeout: 10000})
if err != nil {
return fmt.Errorf("deleting session daemon: %w", err)
}
WriteStdout("session daemon %s deleted\n", daemonId)
return nil
}

func sessionListRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
sendActivity("session:list", rtnErr == nil)
}()

data := wshrpc.CommandSessionListData{ShowAll: sessionListFlagAll}
sessions, err := wshclient.SessionListCommand(RpcClient, data, &wshrpc.RpcOpts{Timeout: 10000})
if err != nil {
return fmt.Errorf("listing session daemons: %w", err)
}

if len(sessions) == 0 {
WriteStdout("no session daemons\n")
return nil
}

WriteStdout("%-36s %-20s %-30s %-12s %s\n", "daemonid", "name", "connection", "status", "blocks")
WriteStdout("----------------------------------------------------------------------\n")
for _, s := range sessions {
blocks := fmt.Sprintf("%d", len(s.Blocks))
if s.IsAnonymous {
blocks += " (anon)"
}
WriteStdout("%-36s %-20s %-30s %-12s %s\n", s.DaemonId, s.Name, s.Connection, s.Status, blocks)
}
return nil
}

func sessionAttachRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
sendActivity("session:attach", rtnErr == nil)
}()

daemonId := args[0]
fullORef, err := resolveBlockArg()
if err != nil {
return err
}

data := wshrpc.CommandSessionAttachData{
DaemonId: daemonId,
BlockId: fullORef.OID,
}
err = wshclient.SessionAttachCommand(RpcClient, data, &wshrpc.RpcOpts{Timeout: 10000})
if err != nil {
return fmt.Errorf("attaching block: %w", err)
}
WriteStdout("block %s attached to session daemon %s\n", fullORef.OID, daemonId)
return nil
}

func sessionDetachRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
sendActivity("session:detach", rtnErr == nil)
}()

fullORef, err := resolveBlockArg()
if err != nil {
return err
}
blockId := fullORef.OID

info, err := wshclient.BlockInfoCommand(RpcClient, blockId, &wshrpc.RpcOpts{Timeout: 5000})
if err != nil {
return fmt.Errorf("getting block info: %w", err)
}
if info.Block == nil {
return fmt.Errorf("block %s not found", blockId)
}

daemonId := info.Block.Meta.GetString(waveobj.MetaKey_SessionDaemonId, "")
if daemonId == "" {
return fmt.Errorf("block %s is not attached to any session daemon", blockId)
}

err = wshclient.SessionDetachCommand(RpcClient, wshrpc.CommandSessionDetachData{DaemonId: daemonId, BlockId: blockId}, &wshrpc.RpcOpts{Timeout: 10000})
if err != nil {
return fmt.Errorf("detaching block: %w", err)
}
WriteStdout("block %s detached from session daemon %s\n", blockId, daemonId)
return nil
}

func sessionInfoRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
sendActivity("session:info", rtnErr == nil)
}()

daemonId := args[0]
info, err := wshclient.SessionInfoCommand(RpcClient, wshrpc.CommandSessionInfoData{DaemonId: daemonId}, &wshrpc.RpcOpts{Timeout: 10000})
if err != nil {
return fmt.Errorf("getting session info: %w", err)
}

createdAt := time.UnixMilli(info.CreatedAt).Format("2006-01-02 15:04:05")
WriteStdout("daemonid: %s\n", info.DaemonId)
WriteStdout("name: %s\n", info.Name)
WriteStdout("connection: %s\n", info.Connection)
WriteStdout("jobid: %s\n", info.JobId)
WriteStdout("status: %s\n", info.Status)
WriteStdout("anonymous: %v\n", info.IsAnonymous)
WriteStdout("created: %s\n", createdAt)
WriteStdout("timeout: %ds\n", info.IdleTimeout)
if info.IdleSince > 0 {
WriteStdout("idle since: %s\n", time.UnixMilli(info.IdleSince).Format("2006-01-02 15:04:05"))
}
WriteStdout("blocks: %d\n", len(info.Blocks))
for _, b := range info.Blocks {
WriteStdout(" - %s\n", b)
}
return nil
}

func sessionTagRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
sendActivity("session:tag", rtnErr == nil)
}()

daemonId := args[0]
data := wshrpc.CommandSessionTagData{
DaemonId: daemonId,
Name: sessionTagFlagName,
}

err := wshclient.SessionTagCommand(RpcClient, data, &wshrpc.RpcOpts{Timeout: 10000})
if err != nil {
return fmt.Errorf("tagging session daemon: %w", err)
}
WriteStdout("session daemon %s tagged as %q\n", daemonId, sessionTagFlagName)
return nil
}
1 change: 1 addition & 0 deletions db/migrations-wstore/000012_sessiondaemon.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS db_sessiondaemon;
5 changes: 5 additions & 0 deletions db/migrations-wstore/000012_sessiondaemon.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS db_sessiondaemon (
oid varchar(36) PRIMARY KEY,
version int NOT NULL,
data json NOT NULL
);
Loading
Loading