Skip to content

Commit 573ac1b

Browse files
committed
feat: handle terminal errors in UI with proper feedback
1 parent 591e08e commit 573ac1b

1 file changed

Lines changed: 100 additions & 27 deletions

File tree

src/components/agents/remotebg/TerminalManager.vue

Lines changed: 100 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
inline
1414
color="primary"
1515
@update:model-value="onShellChange"
16+
@dblclick="onOptionDblClick"
1617
class="q-ml-sm q-gutter-lg"
1718
/>
1819
</div>
@@ -21,7 +22,7 @@
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

@@ -35,7 +36,7 @@
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>
@@ -46,7 +47,7 @@
4647
import { ref, onMounted, onBeforeUnmount, watch, Ref, computed } from "vue";
4748
import { Terminal } from "@xterm/xterm";
4849
import { FitAddon } from "@xterm/addon-fit";
49-
import { uid } from "quasar";
50+
import { uid, useQuasar } from "quasar";
5051
import { useResizeObserver, useDebounceFn } from "@vueuse/core";
5152
import { useTerminalWSConnection } from "@/websocket/websocket";
5253
import { 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
}
6366
interface ShellOption {
6467
label: string;
6568
value: string;
6669
disable?: boolean;
6770
}
6871
72+
const $q = useQuasar();
6973
const props = defineProps<{ agent_id: string; agentPlatform: string }>();
7074
const loading = ref(false);
7175
const customShellPath = ref<string | null>(null);
7276
const showCustomShellDialog = ref(false);
7377
const customShellInput = ref("");
78+
const invalidCustomShell = ref(false);
79+
const lastSelectedShell = ref<string>("");
80+
const pendingCustomShell = ref<string | null>(null);
7481
7582
const 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+
170212
async 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
187232
function 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+
244304
onMounted(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
266339
onBeforeUnmount(() => {

0 commit comments

Comments
 (0)