1313 inline
1414 color =" primary"
1515 @update:model-value =" onShellChange"
16+ @dblclick =" onOptionDblClick"
1617 class =" q-ml-sm q-gutter-lg"
1718 />
1819 </div >
2122 <q-inner-loading :showing =" loading" color =" primary" />
2223 </div >
2324 </div >
24- <q-dialog v-model =" showCustomShellDialog" >
25+ <q-dialog v-model =" showCustomShellDialog" persistent >
2526 <q-card style =" min-width : 400px " >
2627 <q-card-section class =" text-h6" > Enter Custom Shell Path </q-card-section >
2728
3536 </q-card-section >
3637
3738 <q-card-actions align =" right" >
38- <q-btn flat label =" Cancel" v-close-popup />
39+ <q-btn flat label =" Cancel" @click = " cancelCustomShell " />
3940 <q-btn color =" primary" label =" Start" @click =" startCustomShell" />
4041 </q-card-actions >
4142 </q-card >
4647import { ref , onMounted , onBeforeUnmount , watch , Ref , computed } from " vue" ;
4748import { Terminal } from " @xterm/xterm" ;
4849import { FitAddon } from " @xterm/addon-fit" ;
49- import { uid } from " quasar" ;
50+ import { uid , useQuasar } from " quasar" ;
5051import { useResizeObserver , useDebounceFn } from " @vueuse/core" ;
5152import { useTerminalWSConnection } from " @/websocket/websocket" ;
5253import { fetchAgentShell } from " @/api/agents" ;
@@ -58,19 +59,25 @@ interface TerminalWSMessage {
5859 output? : string ;
5960 done? : boolean ;
6061 messageId? : string ;
62+ error? : string ;
6163 };
64+ error? : string ;
6265}
6366interface ShellOption {
6467 label: string ;
6568 value: string ;
6669 disable? : boolean ;
6770}
6871
72+ const $q = useQuasar ();
6973const props = defineProps <{ agent_id: string ; agentPlatform: string }>();
7074const loading = ref (false );
7175const customShellPath = ref <string | null >(null );
7276const showCustomShellDialog = ref (false );
7377const customShellInput = ref (" " );
78+ const invalidCustomShell = ref (false );
79+ const lastSelectedShell = ref <string >(" " );
80+ const pendingCustomShell = ref <string | null >(null );
7481
7582const shellOptions = computed <ShellOption []>(() => {
7683 const isWindows = props .agentPlatform === " windows" ;
@@ -82,10 +89,12 @@ const shellOptions = computed<ShellOption[]>(() => {
8289 : [{ label: " Bash" , value: " bash" }];
8390
8491 base .push ({
85- label: customShellPath .value
86- ? ` Custom (${customShellPath .value }) `
87- : " Custom Shell" ,
88- value: customShellPath .value || " custom" ,
92+ label: invalidCustomShell .value
93+ ? " Custom (Shell path doesn't exist)"
94+ : customShellPath .value
95+ ? ` Custom (${customShellPath .value }) `
96+ : " Custom Shell" ,
97+ value: " custom" ,
8998 });
9099
91100 return base ;
@@ -139,8 +148,30 @@ function initWS(shell: string) {
139148 watch (wsData , (msg ) => {
140149 if (! msg ?.action || ! term ) return ;
141150
142- if (msg .data ?.output ) term .write (msg .data .output );
143- if (msg .data ?.done ) term .write (" \r\n [Session Ended]\r\n " );
151+ if (msg .action === " terminal_error" ) {
152+ loading .value = false ;
153+ invalidCustomShell .value = true ;
154+ $q .notify ({
155+ type: " negative" ,
156+ message: msg .error || msg .data ?.error || " Shell path doesn't exist" ,
157+ });
158+ showCustomShellDialog .value = true ;
159+ pendingCustomShell .value = null ;
160+ return ;
161+ }
162+ if (msg .data ?.output ) {
163+ term .write (msg .data .output );
164+ if (pendingCustomShell .value ) {
165+ customShellPath .value = pendingCustomShell .value ;
166+ selectedShell .value = " custom" ;
167+ showCustomShellDialog .value = false ;
168+ pendingCustomShell .value = null ;
169+ invalidCustomShell .value = false ;
170+ }
171+ }
172+ if (msg .data ?.done ) {
173+ term .write (" \r\n [Session Ended]\r\n " );
174+ }
144175 });
145176
146177 dataDisposable = term ! .onData ((d ) => {
@@ -167,12 +198,26 @@ function initWS(shell: string) {
167198 }, 50 );
168199}
169200
201+ function onOptionDblClick() {
202+ if (selectedShell .value === " custom" ) {
203+ handleCustomEdit ();
204+ }
205+ }
206+
207+ function handleCustomEdit() {
208+ showCustomShellDialog .value = true ;
209+ customShellInput .value = customShellPath .value || " " ;
210+ }
211+
170212async function onShellChange(newShell : string ) {
171213 if (! term ) return ;
172214 if (newShell === " custom" ) {
173- if (! customShellPath .value ) {
215+ if (selectedShell .value !== " custom" ) {
216+ lastSelectedShell .value = selectedShell .value ;
217+ }
218+ if (! customShellPath .value || invalidCustomShell .value ) {
174219 showCustomShellDialog .value = true ;
175- selectedShell .value = " " ;
220+ customShellInput .value = " " ;
176221 return ;
177222 }
178223 newShell = customShellPath .value ;
@@ -186,11 +231,10 @@ async function onShellChange(newShell: string) {
186231
187232function startCustomShell() {
188233 if (! customShellInput .value ) return ;
189- customShellPath .value = customShellInput .value ;
190- showCustomShellDialog .value = false ;
191- selectedShell .value = " custom" ;
192234 loading .value = true ;
193235 started = false ;
236+ invalidCustomShell .value = false ;
237+ pendingCustomShell .value = customShellInput .value ;
194238 term ?.reset ();
195239 fit .fit ();
196240 initWS (customShellInput .value );
@@ -241,26 +285,55 @@ function disconnect() {
241285 term = null ;
242286}
243287
288+ function cancelCustomShell() {
289+ showCustomShellDialog .value = false ;
290+ selectedShell .value = lastSelectedShell .value ;
291+ loading .value = true ;
292+ started = false ;
293+ term ?.reset ();
294+ fit .fit ();
295+ initWS (lastSelectedShell .value );
296+ }
297+
298+ const BUILT_IN_SHELLS = [" cmd" , " powershell" , " bash" ] as const ;
299+ type BuiltInShell = (typeof BUILT_IN_SHELLS )[number ];
300+ const isBuiltInShell = (shell : string ): shell is BuiltInShell => {
301+ return (BUILT_IN_SHELLS as readonly string []).includes (shell );
302+ };
303+
244304onMounted (async () => {
245305 setupXTerm ();
246306 const { stop } = useResizeObserver (xtermContainer , resizeWindow );
247307 stopResizeObserver = stop ;
248308 const data = await fetchAgentShell (props .agent_id );
249- if (data ?.effective_default_shell ) {
250- const shell = data .effective_default_shell ;
251- if (
252- props .agentPlatform === " windows" &&
253- shell !== " cmd" &&
254- shell !== " powershell"
255- ) {
256- customShellPath .value = shell ;
257- }
258- if (props .agentPlatform !== " windows" && shell !== " bash" ) {
259- customShellPath .value = shell ;
309+ if (data ) {
310+ const { default_shell, effective_default_shell } = data ;
311+ const isWindows = props .agentPlatform === " windows" ;
312+ if (default_shell === " custom" ) {
313+ if (isBuiltInShell (effective_default_shell )) {
314+ selectedShell .value = effective_default_shell ;
315+ lastSelectedShell .value = effective_default_shell ;
316+ customShellPath .value = null ;
317+ invalidCustomShell .value = true ;
318+ } else {
319+ customShellPath .value = effective_default_shell ;
320+ selectedShell .value = " custom" ;
321+ lastSelectedShell .value = isWindows ? " cmd" : " bash" ;
322+ invalidCustomShell .value = false ;
323+ }
324+ } else {
325+ selectedShell .value = effective_default_shell ;
326+ lastSelectedShell .value = effective_default_shell ;
327+
328+ invalidCustomShell .value = false ;
329+ customShellPath .value = null ;
260330 }
261- selectedShell .value = shell ;
262331 }
263- initWS (selectedShell .value );
332+ const shellToStart =
333+ selectedShell .value === " custom"
334+ ? customShellPath .value !
335+ : selectedShell .value ;
336+ initWS (shellToStart );
264337});
265338
266339onBeforeUnmount (() => {
0 commit comments