V1.2.0 #17

Merged
jamie merged 7 commits from v1.2.0 into main 2026-06-19 20:03:17 +01:00
2 changed files with 45 additions and 1 deletions
Showing only changes of commit 6539a7f2c4 - Show all commits
+32 -1
View File
@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
import { Folder, Pencil, Trash2 } from "lucide-vue-next"; import { Folder, Pencil, Trash2, Radio } from "lucide-vue-next";
import { import {
api, api,
type HostRow, type HostRow,
@@ -37,6 +37,22 @@ const tabs = ref<{ id: string; hostId: number; label: string }[]>([]);
const activePanes = ref<string[]>([]); const activePanes = ref<string[]>([]);
const draggedTabId = ref<string | null>(null); const draggedTabId = ref<string | null>(null);
const broadcastMode = ref(false);
const tabRefs = ref<Record<string, any>>({});
function setTabRef(el: any, id: string) {
if (el) tabRefs.value[id] = el;
else delete tabRefs.value[id];
}
function handleBroadcast(data: string, sourceId: string) {
if (!broadcastMode.value) return;
for (const paneId of activePanes.value) {
if (paneId !== sourceId && tabRefs.value[paneId]) {
tabRefs.value[paneId].sendData(data);
}
}
}
function onTabDragStart(id: string) { function onTabDragStart(id: string) {
draggedTabId.value = id; draggedTabId.value = id;
} }
@@ -744,6 +760,19 @@ async function deleteIdentityRow(id: number) {
</div> </div>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button
v-if="activePanes.length > 1"
type="button"
class="hidden rounded-lg px-3 py-1.5 text-xs md:inline-flex border transition-colors"
:class="broadcastMode ? 'border-accent bg-accent/10 text-accent' : 'border-slate-800 text-slate-400 hover:border-slate-700 hover:text-white'"
@click="broadcastMode = !broadcastMode"
title="Broadcast input to all visible terminals"
>
<span class="flex items-center gap-2">
<Radio class="h-3.5 w-3.5" />
Broadcast
</span>
</button>
<button <button
type="button" type="button"
class="hidden rounded-lg px-3 py-1.5 text-xs md:inline-flex" class="hidden rounded-lg px-3 py-1.5 text-xs md:inline-flex"
@@ -1067,9 +1096,11 @@ async function deleteIdentityRow(id: number) {
class="flex-1 min-w-0 h-full" class="flex-1 min-w-0 h-full"
> >
<TabContent <TabContent
:ref="(el) => setTabRef(el, t.id)"
:host-id="t.hostId" :host-id="t.hostId"
:visible="activePanes.includes(t.id)" :visible="activePanes.includes(t.id)"
:show-sftp="showSftpPanel" :show-sftp="showSftpPanel"
@broadcast-data="(data: string) => handleBroadcast(data, t.id)"
/> />
</div> </div>
+13
View File
@@ -17,6 +17,18 @@ const props = defineProps<{
showSftp: boolean; showSftp: boolean;
}>(); }>();
const emit = defineEmits<{
(e: 'broadcast-data', data: string): void;
}>();
defineExpose({
sendData: (data: string) => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(new TextEncoder().encode(data));
}
}
});
const termEl = ref<HTMLElement | null>(null); const termEl = ref<HTMLElement | null>(null);
const status = ref("Connecting…"); const status = ref("Connecting…");
const connId = ref<string | null>(null); const connId = ref<string | null>(null);
@@ -97,6 +109,7 @@ onMounted(async () => {
if (ws && ws.readyState === WebSocket.OPEN) { if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(new TextEncoder().encode(data)); ws.send(new TextEncoder().encode(data));
} }
emit("broadcast-data", data);
}); });
term.onResize(({ cols, rows }) => { term.onResize(({ cols, rows }) => {