diff --git a/db/migrations/000021_linestate.down.sql b/db/migrations/000021_linestate.down.sql new file mode 100644 index 00000000..064dfbf6 --- /dev/null +++ b/db/migrations/000021_linestate.down.sql @@ -0,0 +1 @@ +ALTER TABLE line DROP COLUMN linestate; diff --git a/db/migrations/000021_linestate.up.sql b/db/migrations/000021_linestate.up.sql new file mode 100644 index 00000000..f53e0b51 --- /dev/null +++ b/db/migrations/000021_linestate.up.sql @@ -0,0 +1 @@ +ALTER TABLE line ADD COLUMN linestate json NOT NULL DEFAULT '{}'; diff --git a/db/schema.sql b/db/schema.sql index dd445014..a2a8d9cd 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -68,7 +68,6 @@ CREATE TABLE history ( remoteid varchar(36) NOT NULL, remotename varchar(50) NOT NULL, haderror boolean NOT NULL, - cmdid varchar(36) NOT NULL, cmdstr text NOT NULL, ismetacmd boolean, incognito boolean @@ -160,36 +159,13 @@ CREATE TABLE IF NOT EXISTS "line" ( linetype varchar(10) NOT NULL, linelocal boolean NOT NULL, text text NOT NULL, - cmdid varchar(36) NOT NULL, ephemeral boolean NOT NULL, contentheight int NOT NULL, star int NOT NULL, archived boolean NOT NULL, - renderer varchar(50) NOT NULL, + renderer varchar(50) NOT NULL, linestate json NOT NULL DEFAULT '{}', PRIMARY KEY (screenid, lineid) ); -CREATE TABLE IF NOT EXISTS "cmd" ( - screenid varchar(36) NOT NULL, - cmdid varchar(36) NOT NULL, - remoteownerid varchar(36) NOT NULL, - remoteid varchar(36) NOT NULL, - remotename varchar(50) NOT NULL, - cmdstr text NOT NULL, - rawcmdstr text NOT NULL, - festate json NOT NULL, - statebasehash varchar(36) NOT NULL, - statediffhasharr json NOT NULL, - termopts json NOT NULL, - origtermopts json NOT NULL, - status varchar(10) NOT NULL, - startpk json NOT NULL, - doneinfo json NOT NULL, - runout json NOT NULL, - rtnstate boolean NOT NULL, - rtnbasehash varchar(36) NOT NULL, - rtndiffhasharr json NOT NULL, - PRIMARY KEY (screenid, cmdid) -); CREATE TABLE screenupdate ( updateid integer PRIMARY KEY, screenid varchar(36) NOT NULL, @@ -204,3 +180,40 @@ CREATE TABLE webptypos ( PRIMARY KEY (screenid, lineid) ); CREATE INDEX idx_screenupdate_ids ON screenupdate (screenid, lineid); +CREATE TABLE cmd_migration ( + screenid varchar(36) NOT NULL, + lineid varchar(36) NOT NULL, + cmdid varchar(36) NOT NULL, + PRIMARY KEY (screenid, lineid) +); +CREATE TABLE IF NOT EXISTS "cmd" ( + screenid varchar(36) NOT NULL, + lineid varchar(36) NOT NULL, + remoteownerid varchar(36) NOT NULL, + remoteid varchar(36) NOT NULL, + remotename varchar(50) NOT NULL, + cmdstr text NOT NULL, + rawcmdstr text NOT NULL, + festate json NOT NULL, + statebasehash varchar(36) NOT NULL, + statediffhasharr json NOT NULL, + termopts json NOT NULL, + origtermopts json NOT NULL, + status varchar(10) NOT NULL, + cmdpid int NOT NULL, + remotepid int NOT NULL, + donets bigint NOT NULL, + exitcode int NOT NULL, + durationms int NOT NULL, + rtnstate boolean NOT NULL, + rtnbasehash varchar(36) NOT NULL, + rtndiffhasharr json NOT NULL, + runout json NOT NULL, + PRIMARY KEY (screenid, lineid) +); +CREATE TABLE cmd_migrate20 ( + screenid varchar(36) NOT NULL, + lineid varchar(36) NOT NULL, + cmdid varchar(36) NOT NULL, + PRIMARY KEY (screenid, lineid) +); diff --git a/pkg/cmdrunner/cmdrunner.go b/pkg/cmdrunner/cmdrunner.go index 805546c6..49057e03 100644 --- a/pkg/cmdrunner/cmdrunner.go +++ b/pkg/cmdrunner/cmdrunner.go @@ -5,6 +5,7 @@ import ( "context" "crypto/rand" "encoding/base64" + "encoding/json" "fmt" "io/fs" "log" @@ -23,6 +24,7 @@ import ( "github.com/commandlinedev/apishell/pkg/packet" "github.com/commandlinedev/apishell/pkg/shexec" "github.com/commandlinedev/prompt-server/pkg/comp" + "github.com/commandlinedev/prompt-server/pkg/dbutil" "github.com/commandlinedev/prompt-server/pkg/pcloud" "github.com/commandlinedev/prompt-server/pkg/remote" "github.com/commandlinedev/prompt-server/pkg/remote/openai" @@ -63,6 +65,7 @@ const TsFormatStr = "2006-01-02 15:04:05" const ( KwArgRenderer = "renderer" KwArgView = "view" + KwArgState = "state" ) var ColorNames = []string{"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} @@ -2604,8 +2607,23 @@ func LineSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ssto } varsUpdated = append(varsUpdated, KwArgView) } + if stateJson, found := pk.Kwargs[KwArgState]; found { + if len(stateJson) > sstore.MaxLineStateSize { + return nil, fmt.Errorf("invalid state value (too large), size[%d], max[%d]", len(stateJson), sstore.MaxLineStateSize) + } + var stateMap map[string]any + err = json.Unmarshal([]byte(stateJson), &stateMap) + if err != nil { + return nil, fmt.Errorf("invalid state value, cannot parse json: %v", err) + } + err = sstore.UpdateLineState(ctx, ids.ScreenId, lineId, stateMap) + if err != nil { + return nil, fmt.Errorf("cannot update linestate: %v", err) + } + varsUpdated = append(varsUpdated, KwArgState) + } if len(varsUpdated) == 0 { - return nil, fmt.Errorf("/line:set requires a value to set: %s", formatStrs([]string{KwArgView}, "or", false)) + return nil, fmt.Errorf("/line:set requires a value to set: %s", formatStrs([]string{KwArgView, KwArgState}, "or", false)) } updatedLine, err := sstore.GetLineById(ctx, ids.ScreenId, lineId) if err != nil { @@ -2836,7 +2854,7 @@ func LineStarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst if starVal > 5 { return nil, fmt.Errorf("/line:star invalid star-value must be in the range of 0-5") } - err = sstore.UpdateLineStar(ctx, lineId, starVal) + err = sstore.UpdateLineStar(ctx, ids.ScreenId, lineId, starVal) if err != nil { return nil, fmt.Errorf("/line:star error updating star value: %v", err) } @@ -2991,6 +3009,11 @@ func LineShowCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sst buf.WriteString(fmt.Sprintf(" %-15s %dms\n", "duration", cmd.DurationMs)) } } + stateStr := dbutil.QuickJson(line.LineState) + if len(stateStr) > 80 { + stateStr = stateStr[0:77] + "..." + } + buf.WriteString(fmt.Sprintf(" %-15s %s\n", "state", stateStr)) update := &sstore.ModelUpdate{ Info: &sstore.InfoMsgType{ InfoTitle: fmt.Sprintf("line %d info", line.LineNum), diff --git a/pkg/dbutil/map.go b/pkg/dbutil/map.go index 76d1a09c..dffe922b 100644 --- a/pkg/dbutil/map.go +++ b/pkg/dbutil/map.go @@ -58,11 +58,11 @@ func GetMapGen[PT MapConverterPtr[T], T any](tx *txwrap.TxWrap, query string, ar } func GetMappable[PT DBMappablePtr[T], T any](tx *txwrap.TxWrap, query string, args ...interface{}) PT { - rtn := PT(new(T)) m := tx.GetMap(query, args...) if len(m) == 0 { return nil } + rtn := PT(new(T)) FromDBMap(rtn, m) return rtn } diff --git a/pkg/pcloud/pcloud.go b/pkg/pcloud/pcloud.go index 5e8f1e4b..89b69802 100644 --- a/pkg/pcloud/pcloud.go +++ b/pkg/pcloud/pcloud.go @@ -339,6 +339,9 @@ func makeWebShareUpdate(ctx context.Context, update *sstore.ScreenUpdateType) (* rtn.PtyData = &WebSharePtyData{PtyPos: realOffset, Data: data, Eof: true} } + case sstore.UpdateType_LineState: + // TODO implement! + default: return nil, fmt.Errorf("unsupported update type (pcloud/makeWebScreenUpdate): %s\n", update.UpdateType) } diff --git a/pkg/sstore/dbops.go b/pkg/sstore/dbops.go index 5c785282..1cb11dbd 100644 --- a/pkg/sstore/dbops.go +++ b/pkg/sstore/dbops.go @@ -471,7 +471,7 @@ func GetScreenLinesById(ctx context.Context, screenId string) (*ScreenLinesType, return nil, nil } query = `SELECT * FROM line WHERE screenid = ? ORDER BY linenum` - tx.Select(&screen.Lines, query, screen.ScreenId) + screen.Lines = dbutil.SelectMappable[*LineType](tx, query, screen.ScreenId) query = `SELECT * FROM cmd WHERE screenid = ?` screen.Cmds = dbutil.SelectMapsGen[*CmdType](tx, query, screen.ScreenId) return screen, nil @@ -761,16 +761,15 @@ func FindLineIdByArg(ctx context.Context, screenId string, lineArg string) (stri func GetLineCmdByLineId(ctx context.Context, screenId string, lineId string) (*LineType, *CmdType, error) { return WithTxRtn3(ctx, func(tx *TxWrap) (*LineType, *CmdType, error) { - var lineVal LineType query := `SELECT * FROM line WHERE screenid = ? AND lineid = ?` - found := tx.Get(&lineVal, query, screenId, lineId) - if !found { + lineVal := dbutil.GetMappable[*LineType](tx, query, screenId, lineId) + if lineVal == nil { return nil, nil, nil } var cmdRtn *CmdType query = `SELECT * FROM cmd WHERE screenid = ? AND lineid = ?` cmdRtn = dbutil.GetMapGen[*CmdType](tx, query, screenId, lineId) - return &lineVal, cmdRtn, nil + return lineVal, cmdRtn, nil }) } @@ -795,9 +794,9 @@ func InsertLine(ctx context.Context, line *LineType, cmd *CmdType) error { query = `SELECT nextlinenum FROM screen WHERE screenid = ?` nextLineNum := tx.GetInt(query, line.ScreenId) line.LineNum = int64(nextLineNum) - query = `INSERT INTO line ( screenid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, text, renderer, ephemeral, contentheight, star, archived) - VALUES (:screenid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:text,:renderer,:ephemeral,:contentheight,:star,:archived)` - tx.NamedExec(query, line) + query = `INSERT INTO line ( screenid, userid, lineid, ts, linenum, linenumtemp, linelocal, linetype, linestate, text, renderer, ephemeral, contentheight, star, archived) + VALUES (:screenid,:userid,:lineid,:ts,:linenum,:linenumtemp,:linelocal,:linetype,:linestate,:text,:renderer,:ephemeral,:contentheight,:star,:archived)` + tx.NamedExec(query, dbutil.ToDBMap(line, false)) query = `UPDATE screen SET nextlinenum = ? WHERE screenid = ?` tx.Exec(query, nextLineNum+1, line.ScreenId) if cmd != nil { @@ -1889,10 +1888,10 @@ func GetFullState(ctx context.Context, ssPtr ShellStatePtr) (*packet.ShellState, return state, nil } -func UpdateLineStar(ctx context.Context, lineId string, starVal int) error { +func UpdateLineStar(ctx context.Context, screenId string, lineId string, starVal int) error { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `UPDATE line SET star = ? WHERE lineid = ?` - tx.Exec(query, starVal, lineId) + query := `UPDATE line SET star = ? WHERE screenid = ? AND lineid = ?` + tx.Exec(query, starVal, screenId, lineId) return nil }) if txErr != nil { @@ -1903,8 +1902,8 @@ func UpdateLineStar(ctx context.Context, lineId string, starVal int) error { func UpdateLineHeight(ctx context.Context, screenId string, lineId string, heightVal int) error { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `UPDATE line SET contentheight = ? WHERE lineid = ?` - tx.Exec(query, heightVal, lineId) + query := `UPDATE line SET contentheight = ? WHERE screenid = ? AND lineid = ?` + tx.Exec(query, heightVal, screenId, lineId) if isWebShare(tx, screenId) { insertScreenLineUpdate(tx, screenId, lineId, UpdateType_LineContentHeight) } @@ -1918,8 +1917,8 @@ func UpdateLineHeight(ctx context.Context, screenId string, lineId string, heigh func UpdateLineRenderer(ctx context.Context, screenId string, lineId string, renderer string) error { return WithTx(ctx, func(tx *TxWrap) error { - query := `UPDATE line SET renderer = ? WHERE lineid = ?` - tx.Exec(query, renderer, lineId) + query := `UPDATE line SET renderer = ? WHERE screenid = ? AND lineid = ?` + tx.Exec(query, renderer, screenId, lineId) if isWebShare(tx, screenId) { insertScreenLineUpdate(tx, screenId, lineId, UpdateType_LineRenderer) } @@ -1927,28 +1926,34 @@ func UpdateLineRenderer(ctx context.Context, screenId string, lineId string, ren }) } -// can return nil, nil if line is not found -func GetLineById(ctx context.Context, screenId string, lineId string) (*LineType, error) { - var rtn *LineType - txErr := WithTx(ctx, func(tx *TxWrap) error { - var line LineType - query := `SELECT * FROM line WHERE screenid = ? AND lineid = ?` - found := tx.Get(&line, query, screenId, lineId) - if found { - rtn = &line +func UpdateLineState(ctx context.Context, screenId string, lineId string, lineState map[string]any) error { + qjs := dbutil.QuickJson(lineState) + if len(qjs) > MaxLineStateSize { + return fmt.Errorf("linestate for line[%s:%s] exceeds maxsize, size[%d] max[%d]", screenId, lineId, len(qjs), MaxLineStateSize) + } + return WithTx(ctx, func(tx *TxWrap) error { + query := `UPDATE line SET linestate = ? WHERE screenid = ? AND lineid = ?` + tx.Exec(query, qjs, screenId, lineId) + if isWebShare(tx, screenId) { + insertScreenLineUpdate(tx, screenId, lineId, UpdateType_LineState) } return nil }) - if txErr != nil { - return nil, txErr - } - return rtn, nil +} + +// can return nil, nil if line is not found +func GetLineById(ctx context.Context, screenId string, lineId string) (*LineType, error) { + return WithTxRtn(ctx, func(tx *TxWrap) (*LineType, error) { + query := `SELECT * FROM line WHERE screenid = ? AND lineid = ?` + line := dbutil.GetMappable[*LineType](tx, query, screenId, lineId) + return line, nil + }) } func SetLineArchivedById(ctx context.Context, screenId string, lineId string, archived bool) error { txErr := WithTx(ctx, func(tx *TxWrap) error { - query := `UPDATE line SET archived = ? WHERE lineid = ?` - tx.Exec(query, archived, lineId) + query := `UPDATE line SET archived = ? WHERE screenid = ? AND lineid = ?` + tx.Exec(query, archived, screenId, lineId) if isWebShare(tx, screenId) { if archived { insertScreenLineUpdate(tx, screenId, lineId, UpdateType_LineDel) @@ -2395,10 +2400,9 @@ func GetLineCmdsFromHistoryItems(ctx context.Context, historyItems []*HistoryIte return nil, nil, nil } return WithTxRtn3(ctx, func(tx *TxWrap) ([]*LineType, []*CmdType, error) { - var lineArr []*LineType lineIdsJsonArr := quickJsonArr(getLineIdsFromHistoryItems(historyItems)) query := `SELECT * FROM line WHERE lineid IN (SELECT value FROM json_each(?))` - tx.Select(&lineArr, query, lineIdsJsonArr) + lineArr := dbutil.SelectMappable[*LineType](tx, query, lineIdsJsonArr) query = `SELECT * FROM cmd WHERE lineid IN (SELECT value FROM json_each(?))` cmdArr := dbutil.SelectMapsGen[*CmdType](tx, query, lineIdsJsonArr) return lineArr, cmdArr, nil diff --git a/pkg/sstore/migrate.go b/pkg/sstore/migrate.go index 91469b92..ca09e505 100644 --- a/pkg/sstore/migrate.go +++ b/pkg/sstore/migrate.go @@ -17,7 +17,7 @@ import ( "github.com/golang-migrate/migrate/v4" ) -const MaxMigration = 20 +const MaxMigration = 21 const MigratePrimaryScreenVersion = 9 const CmdScreenSpecialMigration = 13 const CmdLineSpecialMigration = 20 diff --git a/pkg/sstore/sstore.go b/pkg/sstore/sstore.go index 47a4a164..216d3a5d 100644 --- a/pkg/sstore/sstore.go +++ b/pkg/sstore/sstore.go @@ -34,6 +34,7 @@ const DBFileName = "prompt.db" const DBFileNameBackup = "backup.prompt.db" const MaxWebShareLineCount = 50 const MaxWebShareScreenCount = 3 +const MaxLineStateSize = 4 * 1024 // 4k for now, can raise if needed const DefaultSessionName = "default" const LocalRemoteAlias = "local" @@ -113,6 +114,7 @@ const ( UpdateType_LineDel = "line:del" UpdateType_LineRenderer = "line:renderer" UpdateType_LineContentHeight = "line:contentheight" + UpdateType_LineState = "line:state" UpdateType_CmdStatus = "cmd:status" UpdateType_CmdTermOpts = "cmd:termopts" UpdateType_CmdExitCode = "cmd:exitcode" @@ -707,23 +709,26 @@ type ScreenUpdateType struct { func (ScreenUpdateType) UseDBMap() {} type LineType struct { - ScreenId string `json:"screenid"` - UserId string `json:"userid"` - LineId string `json:"lineid"` - Ts int64 `json:"ts"` - LineNum int64 `json:"linenum"` - LineNumTemp bool `json:"linenumtemp,omitempty"` - LineLocal bool `json:"linelocal"` - LineType string `json:"linetype"` - Renderer string `json:"renderer,omitempty"` - Text string `json:"text,omitempty"` - Ephemeral bool `json:"ephemeral,omitempty"` - ContentHeight int64 `json:"contentheight,omitempty"` - Star bool `json:"star,omitempty"` - Archived bool `json:"archived,omitempty"` - Remove bool `json:"remove,omitempty"` + ScreenId string `json:"screenid"` + UserId string `json:"userid"` + LineId string `json:"lineid"` + Ts int64 `json:"ts"` + LineNum int64 `json:"linenum"` + LineNumTemp bool `json:"linenumtemp,omitempty"` + LineLocal bool `json:"linelocal"` + LineType string `json:"linetype"` + LineState map[string]any `json:"linestate"` + Renderer string `json:"renderer,omitempty"` + Text string `json:"text,omitempty"` + Ephemeral bool `json:"ephemeral,omitempty"` + ContentHeight int64 `json:"contentheight,omitempty"` + Star bool `json:"star,omitempty"` + Archived bool `json:"archived,omitempty"` + Remove bool `json:"remove,omitempty"` } +func (LineType) UseDBMap() {} + type OpenAIUsage struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"`