11// Copyright 2025, Command Line Inc.
22// SPDX-License-Identifier: Apache-2.0
33
4+ import { WaveAIModel } from "@/app/aipanel/waveai-model" ;
45import { BlockNodeModel } from "@/app/block/blocktypes" ;
56import { appHandleKeyDown } from "@/app/store/keymodel" ;
67import type { TabModel } from "@/app/store/tab-model" ;
@@ -14,6 +15,9 @@ import { VDomModel } from "@/app/view/vdom/vdom-model";
1415import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model" ;
1516import {
1617 atoms ,
18+ createBlock ,
19+ createBlockSplitHorizontally ,
20+ createBlockSplitVertically ,
1721 getAllBlockComponentModels ,
1822 getApi ,
1923 getBlockComponentModel ,
@@ -663,6 +667,97 @@ export class TermViewModel implements ViewModel {
663667 prtn . catch ( ( e ) => console . log ( "error controller resync (force restart)" , e ) ) ;
664668 }
665669
670+ getContextMenuItems ( ) : ContextMenuItem [ ] {
671+ const menu : ContextMenuItem [ ] = [ ] ;
672+ const hasSelection = this . termRef . current ?. terminal ?. hasSelection ( ) ;
673+ const selection = hasSelection ? this . termRef . current ?. terminal . getSelection ( ) : null ;
674+
675+ if ( hasSelection ) {
676+ menu . push ( {
677+ label : "Copy" ,
678+ click : ( ) => {
679+ if ( selection ) {
680+ navigator . clipboard . writeText ( selection ) ;
681+ }
682+ } ,
683+ } ) ;
684+ menu . push ( { type : "separator" } ) ;
685+ menu . push ( {
686+ label : "Send to Wave AI" ,
687+ click : ( ) => {
688+ if ( selection ) {
689+ const aiModel = WaveAIModel . getInstance ( ) ;
690+ aiModel . appendText ( selection , true , { scrollToBottom : true } ) ;
691+ const layoutModel = WorkspaceLayoutModel . getInstance ( ) ;
692+ if ( ! layoutModel . getAIPanelVisible ( ) ) {
693+ layoutModel . setAIPanelVisible ( true ) ;
694+ }
695+ aiModel . focusInput ( ) ;
696+ }
697+ } ,
698+ } ) ;
699+
700+ let selectionURL : URL = null ;
701+ if ( selection ) {
702+ try {
703+ const trimmedSelection = selection . trim ( ) ;
704+ const url = new URL ( trimmedSelection ) ;
705+ if ( url . protocol . startsWith ( "http" ) ) {
706+ selectionURL = url ;
707+ }
708+ } catch ( e ) {
709+ // not a valid URL
710+ }
711+ }
712+
713+ if ( selectionURL ) {
714+ menu . push ( { type : "separator" } ) ;
715+ menu . push ( {
716+ label : "Open URL (" + selectionURL . hostname + ")" ,
717+ click : ( ) => {
718+ createBlock ( {
719+ meta : {
720+ view : "web" ,
721+ url : selectionURL . toString ( ) ,
722+ } ,
723+ } ) ;
724+ } ,
725+ } ) ;
726+ menu . push ( {
727+ label : "Open URL in External Browser" ,
728+ click : ( ) => {
729+ getApi ( ) . openExternal ( selectionURL . toString ( ) ) ;
730+ } ,
731+ } ) ;
732+ }
733+ menu . push ( { type : "separator" } ) ;
734+ }
735+
736+ menu . push ( {
737+ label : "Paste" ,
738+ click : ( ) => {
739+ getApi ( ) . nativePaste ( ) ;
740+ } ,
741+ } ) ;
742+
743+ menu . push ( { type : "separator" } ) ;
744+
745+ const magnified = globalStore . get ( this . nodeModel . isMagnified ) ;
746+ menu . push ( {
747+ label : magnified ? "Un-Magnify Block" : "Magnify Block" ,
748+ click : ( ) => {
749+ this . nodeModel . toggleMagnify ( ) ;
750+ } ,
751+ } ) ;
752+
753+ menu . push ( { type : "separator" } ) ;
754+
755+ const settingsItems = this . getSettingsMenuItems ( ) ;
756+ menu . push ( ...settingsItems ) ;
757+
758+ return menu ;
759+ }
760+
666761 getSettingsMenuItems ( ) : ContextMenuItem [ ] {
667762 const fullConfig = globalStore . get ( atoms . fullConfigAtom ) ;
668763 const termThemes = fullConfig ?. termthemes ?? { } ;
@@ -677,7 +772,61 @@ export class TermViewModel implements ViewModel {
677772 termThemeKeys . sort ( ( a , b ) => {
678773 return ( termThemes [ a ] [ "display:order" ] ?? 0 ) - ( termThemes [ b ] [ "display:order" ] ?? 0 ) ;
679774 } ) ;
775+ const defaultTermBlockDef : BlockDef = {
776+ meta : {
777+ view : "term" ,
778+ controller : "shell" ,
779+ } ,
780+ } ;
781+
680782 const fullMenu : ContextMenuItem [ ] = [ ] ;
783+ fullMenu . push ( {
784+ label : "Split Horizontally" ,
785+ click : ( ) => {
786+ const blockData = globalStore . get ( this . blockAtom ) ;
787+ const blockDef : BlockDef = {
788+ meta : blockData ?. meta || defaultTermBlockDef . meta ,
789+ } ;
790+ createBlockSplitHorizontally ( blockDef , this . blockId , "after" ) ;
791+ } ,
792+ } ) ;
793+ fullMenu . push ( {
794+ label : "Split Vertically" ,
795+ click : ( ) => {
796+ const blockData = globalStore . get ( this . blockAtom ) ;
797+ const blockDef : BlockDef = {
798+ meta : blockData ?. meta || defaultTermBlockDef . meta ,
799+ } ;
800+ createBlockSplitVertically ( blockDef , this . blockId , "after" ) ;
801+ } ,
802+ } ) ;
803+ fullMenu . push ( { type : "separator" } ) ;
804+
805+ const shellIntegrationStatus = globalStore . get ( this . termRef ?. current ?. shellIntegrationStatusAtom ) ;
806+ const cwd = blockData ?. meta ?. [ "cmd:cwd" ] ;
807+ const canShowFileBrowser = shellIntegrationStatus === "ready" && cwd != null ;
808+
809+ if ( canShowFileBrowser ) {
810+ fullMenu . push ( {
811+ label : "File Browser" ,
812+ click : ( ) => {
813+ const blockData = globalStore . get ( this . blockAtom ) ;
814+ const connection = blockData ?. meta ?. connection ;
815+ const cwd = blockData ?. meta ?. [ "cmd:cwd" ] ;
816+ const meta : Record < string , any > = {
817+ view : "preview" ,
818+ file : cwd ,
819+ } ;
820+ if ( connection ) {
821+ meta . connection = connection ;
822+ }
823+ const blockDef : BlockDef = { meta } ;
824+ createBlock ( blockDef ) ;
825+ } ,
826+ } ) ;
827+ fullMenu . push ( { type : "separator" } ) ;
828+ }
829+
681830 const submenu : ContextMenuItem [ ] = termThemeKeys . map ( ( themeName ) => {
682831 return {
683832 label : termThemes [ themeName ] [ "display:name" ] ?? themeName ,
@@ -765,8 +914,10 @@ export class TermViewModel implements ViewModel {
765914 label : "Transparency" ,
766915 submenu : transparencySubMenu ,
767916 } ) ;
917+ fullMenu . push ( { type : "separator" } ) ;
918+ const advancedSubmenu : ContextMenuItem [ ] = [ ] ;
768919 const allowBracketedPaste = blockData ?. meta ?. [ "term:allowbracketedpaste" ] ;
769- fullMenu . push ( {
920+ advancedSubmenu . push ( {
770921 label : "Allow Bracketed Paste Mode" ,
771922 submenu : [
772923 {
@@ -804,13 +955,12 @@ export class TermViewModel implements ViewModel {
804955 } ,
805956 ] ,
806957 } ) ;
807- fullMenu . push ( { type : "separator" } ) ;
808- fullMenu . push ( {
958+ advancedSubmenu . push ( {
809959 label : "Force Restart Controller" ,
810960 click : this . forceRestartController . bind ( this ) ,
811961 } ) ;
812962 const isClearOnStart = blockData ?. meta ?. [ "cmd:clearonstart" ] ;
813- fullMenu . push ( {
963+ advancedSubmenu . push ( {
814964 label : "Clear Output On Restart" ,
815965 submenu : [
816966 {
@@ -838,7 +988,7 @@ export class TermViewModel implements ViewModel {
838988 ] ,
839989 } ) ;
840990 const runOnStart = blockData ?. meta ?. [ "cmd:runonstart" ] ;
841- fullMenu . push ( {
991+ advancedSubmenu . push ( {
842992 label : "Run On Startup" ,
843993 submenu : [
844994 {
@@ -865,17 +1015,8 @@ export class TermViewModel implements ViewModel {
8651015 } ,
8661016 ] ,
8671017 } ) ;
868- if ( blockData ?. meta ?. [ "term:vdomtoolbarblockid" ] ) {
869- fullMenu . push ( { type : "separator" } ) ;
870- fullMenu . push ( {
871- label : "Close Toolbar" ,
872- click : ( ) => {
873- RpcApi . DeleteSubBlockCommand ( TabRpcClient , { blockid : blockData . meta [ "term:vdomtoolbarblockid" ] } ) ;
874- } ,
875- } ) ;
876- }
8771018 const debugConn = blockData ?. meta ?. [ "term:conndebug" ] ;
878- fullMenu . push ( {
1019+ advancedSubmenu . push ( {
8791020 label : "Debug Connection" ,
8801021 submenu : [
8811022 {
@@ -913,6 +1054,19 @@ export class TermViewModel implements ViewModel {
9131054 } ,
9141055 ] ,
9151056 } ) ;
1057+ fullMenu . push ( {
1058+ label : "Advanced" ,
1059+ submenu : advancedSubmenu ,
1060+ } ) ;
1061+ if ( blockData ?. meta ?. [ "term:vdomtoolbarblockid" ] ) {
1062+ fullMenu . push ( { type : "separator" } ) ;
1063+ fullMenu . push ( {
1064+ label : "Close Toolbar" ,
1065+ click : ( ) => {
1066+ RpcApi . DeleteSubBlockCommand ( TabRpcClient , { blockid : blockData . meta [ "term:vdomtoolbarblockid" ] } ) ;
1067+ } ,
1068+ } ) ;
1069+ }
9161070 return fullMenu ;
9171071 }
9181072}
0 commit comments